feat(admin): init

This commit is contained in:
ulitsaRaskolnikova 2026-02-09 13:10:09 +03:00
parent 0ea72e749d
commit 1a774ffc9e
14 changed files with 363 additions and 1 deletions

25
admin-service/Dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM gradle:8.5-jdk17 AS build
WORKDIR /app
ENV GRADLE_OPTS="-Dhttps.protocols=TLSv1.2,TLSv1.3"
COPY build.gradle.kts settings.gradle.kts ./
COPY admin-service/build.gradle.kts ./admin-service/
COPY common/build.gradle.kts ./common/
COPY admin-service/src ./admin-service/src
COPY common/src ./common/src
RUN gradle :admin-service:clean :admin-service:bootJar --no-daemon --no-build-cache
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
RUN groupadd -r spring && useradd -r -g spring spring
USER spring:spring
COPY --from=build /app/admin-service/build/libs/admin-service.jar app.jar
EXPOSE 8086
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@ -0,0 +1,57 @@
plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
id("org.openapi.generator")
}
configure<io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension> {
imports {
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
}
}
openApiGenerate {
generatorName.set("spring")
inputSpec.set("${project.projectDir}/src/main/resources/static/openapi.yaml")
outputDir.set(project.layout.buildDirectory.get().asFile.resolve("generated/openapi").absolutePath)
apiPackage.set("ru.itmo.admin.generated.api")
modelPackage.set("ru.itmo.admin.generated.model")
invokerPackage.set("ru.itmo.admin.generated")
configOptions.set(
mapOf(
"library" to "spring-boot",
"useSpringBoot3" to "true",
"useBeanValidation" to "true",
"openApiNullable" to "false",
"useTags" to "true",
"configPackage" to "ru.itmo.admin.generated.config",
"interfaceOnly" to "true"
)
)
}
tasks.named("compileJava") {
dependsOn("openApiGenerate")
}
sourceSets["main"].java.srcDir(project.layout.buildDirectory.get().asFile.resolve("generated/openapi/src/main/java").absolutePath)
dependencies {
implementation("org.springframework.boot:spring-boot")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0")
implementation(project(":common"))
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.bootJar {
archiveFileName.set("admin-service.jar")
mainClass.set("ru.itmo.admin.AdminServiceApplication")
}

View File

@ -0,0 +1,12 @@
package ru.itmo.admin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AdminServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServiceApplication.class, args);
}
}

View File

@ -0,0 +1,18 @@
package ru.itmo.admin.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.itmo.common.security.JwtUtil;
@Configuration
public class JwtConfig {
@Value("${jwt.secret}")
private String secret;
@Bean
public JwtUtil jwtUtil() {
return new JwtUtil(secret);
}
}

View File

@ -0,0 +1,27 @@
package ru.itmo.admin.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
private static final String BEARER_AUTH = "bearerAuth";
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes(BEARER_AUTH,
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Введите JWT access token (получить через POST /api/auth/login)")))
.addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH));
}
}

View File

@ -0,0 +1,34 @@
package ru.itmo.admin.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import ru.itmo.admin.security.JwtAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/**", "/health", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
// All admin endpoints require ADMIN role
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

View File

@ -0,0 +1,18 @@
package ru.itmo.admin.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import ru.itmo.admin.generated.api.HealthApi;
import ru.itmo.admin.generated.model.HealthResponse;
@RestController
@org.springframework.web.bind.annotation.RequestMapping("${openapi.adminService.base-path:/admin}")
public class HealthApiController implements HealthApi {
@Override
public ResponseEntity<HealthResponse> healthCheck() {
HealthResponse response = new HealthResponse();
response.setStatus("UP");
return ResponseEntity.ok(response);
}
}

View File

@ -0,0 +1,22 @@
package ru.itmo.admin.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<List<String>> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(List.of(ex.getMessage()));
}
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<List<String>> handleIllegalState(IllegalStateException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(List.of(ex.getMessage()));
}
}

View File

@ -0,0 +1,64 @@
package ru.itmo.admin.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import ru.itmo.common.security.JwtUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
if (jwtUtil.validateToken(token, "access")) {
UUID userId = jwtUtil.getUserIdFromToken(token);
Boolean isAdmin = jwtUtil.getIsAdminFromToken(token);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
if (Boolean.TRUE.equals(isAdmin)) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userId,
null,
authorities
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ignored) {
// leave context empty, will result in 403 for protected endpoints
}
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,2 @@
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-must-be-at-least-32-characters-long-for-security}

View File

@ -0,0 +1,31 @@
server:
port: 8086
spring:
application:
name: admin-service
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
- org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
openapi:
adminService:
base-path: /admin
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-must-be-at-least-32-characters-long-for-security}
logging:
level:
ru.itmo: INFO

View File

@ -0,0 +1,38 @@
openapi: 3.0.0
info:
title: Admin Service API
description: API сервиса администрирования
version: 0.1.0
servers:
- url: http://localhost:8086/admin
description: Dev server
paths:
/health:
get:
summary: Проверка состояния сервиса
description: Возвращает статус работоспособности admin-service
operationId: healthCheck
tags:
- Health
responses:
'200':
description: Сервис работает
content:
application/json:
schema:
$ref: '#/components/schemas/HealthResponse'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
HealthResponse:
type: object
properties:
status:
type: string
example: "UP"

View File

@ -153,6 +153,20 @@ services:
networks:
- domain-registrar-network
admin-service:
build:
context: .
dockerfile: admin-service/Dockerfile
container_name: admin-service
ports:
- "8086:8086"
env_file:
- .env
environment:
- SPRING_PROFILES_ACTIVE=docker
networks:
- domain-registrar-network
exdns:
build:
context: ./exdns

View File

@ -11,4 +11,4 @@ pluginManagement {
rootProject.name = "domain-registrar"
include("api-gateway", "common", "domain-service", "notification-service", "auth-service", "order-service")
include("api-gateway", "common", "domain-service", "notification-service", "auth-service", "order-service", "admin-service")