adding validated services? patching forcad_local.py
This commit is contained in:
18
OmCTF-2025/services/jform/Dockerfile
Normal file
18
OmCTF-2025/services/jform/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM maven:3.9-eclipse-temurin-17 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pom.xml .
|
||||
COPY src ./src
|
||||
|
||||
RUN mvn clean package -DskipTests
|
||||
|
||||
FROM eclipse-temurin:17-jre-jammy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
37
OmCTF-2025/services/jform/docker-compose.yml
Normal file
37
OmCTF-2025/services/jform/docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
services:
|
||||
oracle-db:
|
||||
image: gvenzl/oracle-xe:21-slim
|
||||
container_name: jform-oracle
|
||||
environment:
|
||||
ORACLE_PASSWORD: oracle123
|
||||
APP_USER: jform
|
||||
APP_USER_PASSWORD: jform123
|
||||
ports:
|
||||
- "1521:1521"
|
||||
volumes:
|
||||
- oracle-data:/opt/oracle/oradata
|
||||
networks:
|
||||
- jform-network
|
||||
|
||||
app:
|
||||
build: .
|
||||
container_name: jform
|
||||
ports:
|
||||
- "5003:8080"
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:oracle:thin:@oracle-db:1521/XEPDB1
|
||||
SPRING_DATASOURCE_USERNAME: jform
|
||||
SPRING_DATASOURCE_PASSWORD: jform123
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 8.8.4.4
|
||||
networks:
|
||||
- jform-network
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
oracle-data:
|
||||
|
||||
networks:
|
||||
jform-network:
|
||||
driver: bridge
|
||||
101
OmCTF-2025/services/jform/pom.xml
Normal file
101
OmCTF-2025/services/jform/pom.xml
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.jform</groupId>
|
||||
<artifactId>jform</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>JForm</name>
|
||||
<description>JForm</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.oracle.database.jdbc</groupId>
|
||||
<artifactId>ojdbc11</artifactId>
|
||||
<version>23.3.0.23.09</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.jform;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.jform.config;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.Random;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Configuration
|
||||
public class ApplicationConfig {
|
||||
|
||||
private static Random seedGenerator;
|
||||
private static long jwtSeed;
|
||||
private static long formIdSeed;
|
||||
private static boolean initialized = false;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (!initialized) {
|
||||
long initialSeed = System.currentTimeMillis();
|
||||
seedGenerator = new Random(initialSeed);
|
||||
|
||||
jwtSeed = seedGenerator.nextLong();
|
||||
formIdSeed = seedGenerator.nextLong();
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static long getJwtSeed() {
|
||||
if (!initialized) {
|
||||
throw new IllegalStateException("ApplicationConfig not initialized");
|
||||
}
|
||||
return jwtSeed;
|
||||
}
|
||||
|
||||
public static long getFormIdSeed() {
|
||||
if (!initialized) {
|
||||
throw new IllegalStateException("ApplicationConfig not initialized");
|
||||
}
|
||||
return formIdSeed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.jform.controller;
|
||||
|
||||
import com.jform.service.UserService;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.Map;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/account")
|
||||
@RequiredArgsConstructor
|
||||
public class AccountController {
|
||||
|
||||
@NonNull
|
||||
private final UserService userService;
|
||||
|
||||
@PostMapping("/signup")
|
||||
public ResponseEntity<?> signup(@RequestBody Map<String, String> request,
|
||||
HttpServletResponse response) {
|
||||
try {
|
||||
String username = request.get("username");
|
||||
String password = request.get("password");
|
||||
|
||||
if (username == null || username.isEmpty() ||
|
||||
password == null || password.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Username and password are required"));
|
||||
}
|
||||
|
||||
var user = userService.signup(username, password);
|
||||
|
||||
String jwtToken = userService.generateToken(user);
|
||||
|
||||
setCookies(response, user.getUserId(), username, jwtToken);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"username", username,
|
||||
"userId", user.getUserId()
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody Map<String, String> request,
|
||||
HttpServletResponse response) {
|
||||
try {
|
||||
String username = request.get("username");
|
||||
String password = request.get("password");
|
||||
|
||||
if (username == null || username.isEmpty() ||
|
||||
password == null || password.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Username and password are required"));
|
||||
}
|
||||
|
||||
var user = userService.login(username, password);
|
||||
|
||||
String jwtToken = userService.generateToken(user);
|
||||
|
||||
setCookies(response, user.getUserId(), username, jwtToken);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"username", username,
|
||||
"userId", user.getUserId()
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<?> getCurrentUser(@CookieValue(value = "uid", required = false) String userIdStr,
|
||||
@CookieValue(value = "u", required = false) String username,
|
||||
@CookieValue(value = "auth", required = false) String authToken) {
|
||||
if (username == null || authToken == null || userIdStr == null) {
|
||||
return ResponseEntity.status(401)
|
||||
.body(Map.of("authenticated", false));
|
||||
}
|
||||
|
||||
if (userService.validateAuth(username, authToken)) {
|
||||
try {
|
||||
Long userId = Long.parseLong(userIdStr);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"authenticated", true,
|
||||
"userId", userId,
|
||||
"username", username
|
||||
));
|
||||
} catch (NumberFormatException e) {
|
||||
return ResponseEntity.status(401)
|
||||
.body(Map.of("authenticated", false));
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.status(401)
|
||||
.body(Map.of("authenticated", false));
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<?> logout(HttpServletResponse response) {
|
||||
clearCookies(response);
|
||||
return ResponseEntity.ok(Map.of("success", true));
|
||||
}
|
||||
|
||||
private void setCookies(HttpServletResponse response, Long userId, String username, String jwtToken) {
|
||||
Cookie userIdCookie = new Cookie("uid", String.valueOf(userId));
|
||||
userIdCookie.setHttpOnly(true);
|
||||
userIdCookie.setPath("/");
|
||||
userIdCookie.setMaxAge(7 * 24 * 60 * 60);
|
||||
|
||||
Cookie userCookie = new Cookie("u", username);
|
||||
userCookie.setHttpOnly(true);
|
||||
userCookie.setPath("/");
|
||||
userCookie.setMaxAge(7 * 24 * 60 * 60);
|
||||
|
||||
Cookie authCookie = new Cookie("auth", jwtToken);
|
||||
authCookie.setHttpOnly(true);
|
||||
authCookie.setPath("/");
|
||||
authCookie.setMaxAge(7 * 24 * 60 * 60);
|
||||
|
||||
response.addCookie(userIdCookie);
|
||||
response.addCookie(userCookie);
|
||||
response.addCookie(authCookie);
|
||||
}
|
||||
|
||||
private void clearCookies(HttpServletResponse response) {
|
||||
Cookie userIdCookie = new Cookie("uid", "");
|
||||
userIdCookie.setMaxAge(0);
|
||||
userIdCookie.setPath("/");
|
||||
|
||||
Cookie userCookie = new Cookie("u", "");
|
||||
userCookie.setMaxAge(0);
|
||||
userCookie.setPath("/");
|
||||
|
||||
Cookie authCookie = new Cookie("auth", "");
|
||||
authCookie.setMaxAge(0);
|
||||
authCookie.setPath("/");
|
||||
|
||||
response.addCookie(userIdCookie);
|
||||
response.addCookie(userCookie);
|
||||
response.addCookie(authCookie);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.jform.controller;
|
||||
|
||||
import com.jform.model.Form;
|
||||
import com.jform.model.Response;
|
||||
import com.jform.service.FormService;
|
||||
import com.jform.service.UserService;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/form")
|
||||
@RequiredArgsConstructor
|
||||
public class FormController {
|
||||
|
||||
@NonNull
|
||||
private final FormService formService;
|
||||
|
||||
@NonNull
|
||||
private final UserService userService;
|
||||
|
||||
@PostMapping("/new")
|
||||
public ResponseEntity<?> createForm(@RequestBody Map<String, Object> request,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
String username = getAuthenticatedUsername(httpRequest);
|
||||
if (username == null) {
|
||||
return ResponseEntity.status(401)
|
||||
.body(Map.of("error", "Authentication required"));
|
||||
}
|
||||
|
||||
Object schemaObj = request.get("schema");
|
||||
if (schemaObj == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Schema is required"));
|
||||
}
|
||||
|
||||
String schemaJson = convertToJson(schemaObj);
|
||||
|
||||
String formId = formService.createForm(username, schemaJson);
|
||||
|
||||
Optional<Form> createdForm = formService.getForm(formId);
|
||||
Long formNumber = createdForm.map(Form::getFormNumber).orElse(null);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"formId", formId,
|
||||
"formNumber", formNumber != null ? formNumber : 0L
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{formId}")
|
||||
public ResponseEntity<?> getForm(@PathVariable String formId) {
|
||||
Optional<Form> formOpt = formService.getForm(formId);
|
||||
|
||||
if (formOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Form form = formOpt.get();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"formId", form.getFormId(),
|
||||
"formNumber", form.getFormNumber(),
|
||||
"schema", form.getSchemaJson(),
|
||||
"owner", form.getOwnerUsername(),
|
||||
"createdAt", form.getCreatedAt().toString()
|
||||
));
|
||||
}
|
||||
|
||||
@PutMapping("/{formId}")
|
||||
public ResponseEntity<?> updateForm(@PathVariable String formId,
|
||||
@RequestBody Map<String, Object> request,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
String username = getAuthenticatedUsername(httpRequest);
|
||||
if (username == null) {
|
||||
return ResponseEntity.status(401)
|
||||
.body(Map.of("error", "Authentication required"));
|
||||
}
|
||||
|
||||
Object schemaObj = request.get("schema");
|
||||
if (schemaObj == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Schema is required"));
|
||||
}
|
||||
|
||||
String schemaJson = convertToJson(schemaObj);
|
||||
|
||||
formService.updateFormSchema(formId, username, schemaJson);
|
||||
|
||||
return ResponseEntity.ok(Map.of("success", true));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{formId}/submit")
|
||||
public ResponseEntity<?> submitResponse(@PathVariable String formId,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
List<Response> existingResponses = formService.getFormResponses(formId);
|
||||
if (existingResponses.size() >= 100) {
|
||||
return ResponseEntity.status(429)
|
||||
.body(Map.of(
|
||||
"error", "Достигнут лимит ответов на эту форму (максимум 100)",
|
||||
"limit", 100,
|
||||
"current", existingResponses.size()
|
||||
));
|
||||
}
|
||||
|
||||
String payloadJson = convertToJson(request);
|
||||
|
||||
formService.submitResponse(formId, payloadJson);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"responseCount", existingResponses.size() + 1
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/my-forms")
|
||||
public ResponseEntity<?> getMyForms(HttpServletRequest httpRequest) {
|
||||
try {
|
||||
String username = getAuthenticatedUsername(httpRequest);
|
||||
if (username == null) {
|
||||
return ResponseEntity.status(401)
|
||||
.body(Map.of("error", "Authentication required"));
|
||||
}
|
||||
|
||||
List<Form> forms = formService.getUserForms(username);
|
||||
|
||||
List<Map<String, Object>> result = forms.stream()
|
||||
.map(form -> {
|
||||
Map<String, Object> formMap = new java.util.HashMap<>();
|
||||
formMap.put("formId", form.getFormId());
|
||||
formMap.put("formNumber", form.getFormNumber());
|
||||
formMap.put("schema", form.getSchemaJson());
|
||||
formMap.put("owner", form.getOwnerUsername());
|
||||
formMap.put("createdAt", form.getCreatedAt().toString());
|
||||
return formMap;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping({"/{formId}/results", "/{formId}/results.json"})
|
||||
public ResponseEntity<?> getResults(@PathVariable String formId,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
String username = getAuthenticatedUsername(httpRequest);
|
||||
if (username == null) {
|
||||
return ResponseEntity.status(401)
|
||||
.body(Map.of("error", "Authentication required"));
|
||||
}
|
||||
|
||||
Optional<Form> formOpt = formService.getForm(formId);
|
||||
if (formOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Form form = formOpt.get();
|
||||
if (!form.getOwnerUsername().equals(username)) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(Map.of("error", "Access denied"));
|
||||
}
|
||||
|
||||
List<Response> responses = formService.getFormResponses(formId);
|
||||
|
||||
List<String> jsonResponses = responses.stream()
|
||||
.map(Response::getPayloadJson)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(jsonResponses);
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private String getAuthenticatedUsername(HttpServletRequest request) {
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String username = null;
|
||||
String authToken = null;
|
||||
|
||||
for (Cookie cookie : cookies) {
|
||||
if ("u".equals(cookie.getName())) {
|
||||
username = cookie.getValue();
|
||||
} else if ("auth".equals(cookie.getName())) {
|
||||
authToken = cookie.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (username == null || authToken == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (userService.validateAuth(username, authToken)) {
|
||||
return username;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String convertToJson(Object obj) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
return mapper.writeValueAsString(obj);
|
||||
} catch (Exception e) {
|
||||
return obj.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.jform.controller;
|
||||
|
||||
import com.jform.service.UserService;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Validated
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class PageController {
|
||||
|
||||
@NonNull
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping("/f/{formId}")
|
||||
public String formPage(@PathVariable String formId) {
|
||||
return "forward:/form.html";
|
||||
}
|
||||
|
||||
@GetMapping("/profile")
|
||||
public String profile(HttpServletRequest request) {
|
||||
String username = getAuthenticatedUsername(request);
|
||||
|
||||
if (username == null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
return "forward:/profile.html";
|
||||
}
|
||||
|
||||
@GetMapping("/healthz")
|
||||
@ResponseBody
|
||||
public String healthz() {
|
||||
return "OK";
|
||||
}
|
||||
|
||||
private String getAuthenticatedUsername(HttpServletRequest request) {
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String username = null;
|
||||
String authToken = null;
|
||||
|
||||
for (Cookie cookie : cookies) {
|
||||
if ("u".equals(cookie.getName())) {
|
||||
username = cookie.getValue();
|
||||
} else if ("auth".equals(cookie.getName())) {
|
||||
authToken = cookie.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (username == null || authToken == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (userService.validateAuth(username, authToken)) {
|
||||
return username;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.jform.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.Generated;
|
||||
import org.hibernate.annotations.GenerationTime;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "FORMS", indexes = {
|
||||
@Index(name = "idx_forms_owner", columnList = "owner_username"),
|
||||
@Index(name = "idx_forms_created", columnList = "created_at"),
|
||||
@Index(name = "idx_forms_number", columnList = "form_number")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@ToString
|
||||
@EqualsAndHashCode(of = {"formId"})
|
||||
public class Form implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Generated(GenerationTime.INSERT)
|
||||
@Column(name = "form_number", nullable = false, unique = true, insertable = false, updatable = false, columnDefinition = "NUMBER(19,0) GENERATED ALWAYS AS IDENTITY")
|
||||
private Long formNumber;
|
||||
|
||||
@Id
|
||||
@NotBlank
|
||||
@Size(min = 16, max = 16)
|
||||
@Column(name = "form_id", nullable = false, length = 16)
|
||||
private String formId;
|
||||
|
||||
@NotBlank
|
||||
@Size(min = 1, max = 255)
|
||||
@Column(name = "owner_username", nullable = false, length = 255)
|
||||
private String ownerUsername;
|
||||
|
||||
@Lob
|
||||
@NotBlank
|
||||
@Column(name = "schema_json", nullable = false)
|
||||
private String schemaJson;
|
||||
|
||||
@NotNull
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Form(String formId, String ownerUsername, String schemaJson) {
|
||||
this.formId = formId;
|
||||
this.ownerUsername = ownerUsername;
|
||||
this.schemaJson = schemaJson;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.jform.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "RESPONSES", indexes = {
|
||||
@Index(name = "idx_responses_form_created", columnList = "form_id,created_at"),
|
||||
@Index(name = "idx_responses_form", columnList = "form_id")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@ToString
|
||||
@EqualsAndHashCode(of = {"id"})
|
||||
public class Response implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(min = 16, max = 16)
|
||||
@Column(name = "form_id", nullable = false, length = 16)
|
||||
private String formId;
|
||||
|
||||
@Lob
|
||||
@NotBlank
|
||||
@Column(name = "payload_json", nullable = false)
|
||||
private String payloadJson;
|
||||
|
||||
@NotNull
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Response(String formId, String payloadJson) {
|
||||
this.formId = formId;
|
||||
this.payloadJson = payloadJson;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.jform.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "USERS", indexes = {
|
||||
@Index(name = "idx_users_username", columnList = "username", unique = true)
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@ToString(exclude = {"passHash", "authKey"})
|
||||
@EqualsAndHashCode(of = {"userId", "username"})
|
||||
public class User implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@NotBlank
|
||||
@Size(min = 1, max = 255)
|
||||
@Column(name = "username", nullable = false, unique = true, length = 255)
|
||||
private String username;
|
||||
|
||||
@NotBlank
|
||||
@Size(min = 1, max = 255)
|
||||
@Column(name = "pass_hash", nullable = false, length = 255)
|
||||
private String passHash;
|
||||
|
||||
@NotNull
|
||||
@Size(min = 8, max = 8)
|
||||
@Column(name = "auth_key", nullable = false, length = 8)
|
||||
private byte[] authKey;
|
||||
|
||||
|
||||
@NotNull
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public User(String username, String passHash, byte[] authKey) {
|
||||
this.username = username;
|
||||
this.passHash = passHash;
|
||||
this.authKey = authKey;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.jform.repository;
|
||||
|
||||
import com.jform.model.Form;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface FormRepository extends JpaRepository<Form, String> {
|
||||
|
||||
Optional<Form> findByFormId(String formId);
|
||||
|
||||
List<Form> findByOwnerUsername(String ownerUsername);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.jform.repository;
|
||||
|
||||
import com.jform.model.Response;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface ResponseRepository extends JpaRepository<Response, Long> {
|
||||
|
||||
List<Response> findByFormIdOrderByCreatedAtAsc(String formId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.jform.repository;
|
||||
|
||||
import com.jform.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, String> {
|
||||
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.jform.security;
|
||||
|
||||
import com.jform.util.IdProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import java.util.Random;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FormIdGenerator {
|
||||
|
||||
@NonNull
|
||||
private final IdProvider idProvider;
|
||||
|
||||
public String generateFormId() {
|
||||
int seed = idProvider.nextSeed();
|
||||
|
||||
Random idRandom = new Random(seed);
|
||||
long rawId = idRandom.nextLong();
|
||||
|
||||
String formId = String.format("%016x", rawId);
|
||||
|
||||
return formId;
|
||||
}
|
||||
|
||||
public boolean isValidFormId(String formId) {
|
||||
return idProvider.validateIdFormat(formId, 16);
|
||||
}
|
||||
|
||||
public String generateShortId() {
|
||||
return idProvider.generateStringId(8);
|
||||
}
|
||||
|
||||
public boolean checkUniqueness(String formId) {
|
||||
return formId != null && formId.length() == 16;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package com.jform.security;
|
||||
|
||||
import com.jform.util.KeyProvider;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
public class JwtService {
|
||||
|
||||
private static final long TOKEN_VALIDITY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
@NonNull
|
||||
private final KeyProvider keyProvider;
|
||||
|
||||
public long generateUserKey() {
|
||||
long key = keyProvider.generateValue();
|
||||
return key;
|
||||
}
|
||||
|
||||
public byte[] generateKeyData() {
|
||||
byte[] keyData = longToBytes(generateUserKey());
|
||||
return keyData;
|
||||
}
|
||||
|
||||
public byte[] longToBytes(long value) {
|
||||
return ByteBuffer.allocate(8).putLong(value).array();
|
||||
}
|
||||
|
||||
public long bytesToLong(@NotNull @Size(min = 8, max = 8) byte[] bytes) {
|
||||
if (bytes.length != 8) {
|
||||
throw new IllegalArgumentException("Key data must be exactly 8 bytes");
|
||||
}
|
||||
return ByteBuffer.wrap(bytes).getLong();
|
||||
}
|
||||
|
||||
private SecretKey createSigningKey(byte[] keyData) {
|
||||
byte[] expandedKey = new byte[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
expandedKey[i] = keyData[i % 8];
|
||||
}
|
||||
return Keys.hmacShaKeyFor(expandedKey);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String generateToken(@NotBlank String username,
|
||||
@NotNull @Size(min = 8, max = 8) byte[] keyData) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("u", username);
|
||||
|
||||
Date now = new Date();
|
||||
Date expiration = new Date(now.getTime() + TOKEN_VALIDITY_MS);
|
||||
|
||||
SecretKey key = createSigningKey(keyData);
|
||||
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiration)
|
||||
.signWith(key, SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String validateToken(@NotBlank String token,
|
||||
@NotNull @Size(min = 8, max = 8) byte[] keyData) throws Exception {
|
||||
try {
|
||||
SecretKey key = createSigningKey(keyData);
|
||||
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(key)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
String username = claims.get("u", String.class);
|
||||
|
||||
if (username == null || username.isEmpty()) {
|
||||
throw new Exception("Username not found in token");
|
||||
}
|
||||
|
||||
return username;
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Invalid JWT token");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTokenExpired(String token) {
|
||||
try {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length < 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String payload = new String(java.util.Base64.getUrlDecoder().decode(parts[1]));
|
||||
|
||||
return !payload.contains("\"exp\"");
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public Long getIssuedAt(String token) {
|
||||
try {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String payload = new String(java.util.Base64.getUrlDecoder().decode(parts[1]));
|
||||
|
||||
if (payload.contains("\"iat\"")) {
|
||||
int start = payload.indexOf("\"iat\"") + 6;
|
||||
int end = payload.indexOf(",", start);
|
||||
if (end == -1) end = payload.indexOf("}", start);
|
||||
if (end > start) {
|
||||
String iatStr = payload.substring(start, end).trim();
|
||||
return Long.parseLong(iatStr);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String extractUsernameUnsafe(String token) {
|
||||
try {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String payload = new String(java.util.Base64.getUrlDecoder().decode(parts[1]));
|
||||
|
||||
if (payload.contains("\"u\"")) {
|
||||
int start = payload.indexOf("\"u\"") + 5;
|
||||
int end = payload.indexOf("\"", start);
|
||||
if (end > start) {
|
||||
return payload.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.jform.service;
|
||||
|
||||
import com.jform.model.Form;
|
||||
import com.jform.model.Response;
|
||||
import com.jform.repository.FormRepository;
|
||||
import com.jform.repository.ResponseRepository;
|
||||
import com.jform.security.FormIdGenerator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class FormService {
|
||||
|
||||
@NonNull
|
||||
private final FormRepository formRepository;
|
||||
|
||||
@NonNull
|
||||
private final ResponseRepository responseRepository;
|
||||
|
||||
@NonNull
|
||||
private final FormIdGenerator formIdGenerator;
|
||||
|
||||
@NotNull
|
||||
public String createForm(@NotBlank String ownerUsername, @NotBlank String schemaJson) {
|
||||
String formId = formIdGenerator.generateFormId();
|
||||
|
||||
Form form = new Form(formId, ownerUsername, schemaJson);
|
||||
formRepository.save(form);
|
||||
|
||||
|
||||
return formId;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Optional<Form> getForm(@NotBlank String formId) {
|
||||
return formRepository.findByFormId(formId);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<Form> getUserForms(@NotBlank String username) {
|
||||
return formRepository.findByOwnerUsername(username);
|
||||
}
|
||||
|
||||
public void updateFormSchema(@NotBlank String formId,
|
||||
@NotBlank String ownerUsername,
|
||||
@NotBlank String newSchemaJson) throws Exception {
|
||||
Optional<Form> formOpt = formRepository.findByFormId(formId);
|
||||
|
||||
if (formOpt.isEmpty()) {
|
||||
throw new Exception("Form not found");
|
||||
}
|
||||
|
||||
Form form = formOpt.get();
|
||||
|
||||
if (!form.getOwnerUsername().equals(ownerUsername)) {
|
||||
throw new Exception("Access denied");
|
||||
}
|
||||
|
||||
form.setSchemaJson(newSchemaJson);
|
||||
formRepository.save(form);
|
||||
}
|
||||
|
||||
public void submitResponse(@NotBlank String formId, @NotBlank String payloadJson) throws Exception {
|
||||
Optional<Form> formOpt = formRepository.findByFormId(formId);
|
||||
|
||||
if (formOpt.isEmpty()) {
|
||||
throw new Exception("Form not found");
|
||||
}
|
||||
|
||||
Response response = new Response(formId, payloadJson);
|
||||
responseRepository.save(response);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<Response> getFormResponses(@NotBlank String formId) {
|
||||
return responseRepository.findByFormIdOrderByCreatedAtAsc(formId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.jform.service;
|
||||
|
||||
import com.jform.model.User;
|
||||
import com.jform.repository.UserRepository;
|
||||
import com.jform.security.JwtService;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
@Transactional
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtService jwtService;
|
||||
private final BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
public UserService(@NonNull UserRepository userRepository,
|
||||
@NonNull JwtService jwtService) {
|
||||
this.userRepository = userRepository;
|
||||
this.jwtService = jwtService;
|
||||
this.passwordEncoder = new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public User signup(@NotBlank String username, @NotBlank String password) throws Exception {
|
||||
if (userRepository.existsByUsername(username)) {
|
||||
throw new Exception("Username already exists");
|
||||
}
|
||||
|
||||
String passHash = passwordEncoder.encode(password);
|
||||
|
||||
byte[] keyData = jwtService.generateKeyData();
|
||||
|
||||
User user = new User(username, passHash, keyData);
|
||||
user = userRepository.save(user);
|
||||
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public User login(@NotBlank String username, @NotBlank String password) throws Exception {
|
||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||
|
||||
if (userOpt.isEmpty()) {
|
||||
throw new Exception("Invalid username or password");
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
if (!passwordEncoder.matches(password, user.getPassHash())) {
|
||||
throw new Exception("Invalid username or password");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String generateToken(@NotNull User user) {
|
||||
return jwtService.generateToken(user.getUsername(), user.getAuthKey());
|
||||
}
|
||||
|
||||
public boolean validateAuth(@NotBlank String username, @NotBlank String token) {
|
||||
try {
|
||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||
|
||||
if (userOpt.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
String tokenUsername = jwtService.validateToken(token, user.getAuthKey());
|
||||
|
||||
return username.equals(tokenUsername);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<User> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.jform.util;
|
||||
|
||||
import com.jform.config.ApplicationConfig;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.Random;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
@Component
|
||||
@DependsOn("applicationConfig")
|
||||
public class IdProvider {
|
||||
|
||||
private Random idGenerator;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
long seed = ApplicationConfig.getFormIdSeed();
|
||||
this.idGenerator = new Random(seed);
|
||||
}
|
||||
|
||||
public synchronized int nextSeed() {
|
||||
int seed = idGenerator.nextInt();
|
||||
return seed;
|
||||
}
|
||||
|
||||
public synchronized String generateStringId(int length) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String chars = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(chars.charAt(idGenerator.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public synchronized long generateNumericId(long bound) {
|
||||
return Math.abs(idGenerator.nextLong()) % bound;
|
||||
}
|
||||
|
||||
public boolean validateIdFormat(String id, int expectedLength) {
|
||||
return id != null && id.length() == expectedLength && id.matches("[0-9a-f]+");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.jform.util;
|
||||
|
||||
import com.jform.config.ApplicationConfig;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.Random;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
@Component
|
||||
@DependsOn("applicationConfig")
|
||||
public class KeyProvider {
|
||||
|
||||
private Random dataGenerator;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
long seed = ApplicationConfig.getJwtSeed();
|
||||
this.dataGenerator = new Random(seed);
|
||||
}
|
||||
|
||||
public synchronized long generateValue() {
|
||||
long value = dataGenerator.nextLong();
|
||||
return value;
|
||||
}
|
||||
|
||||
public synchronized int generateInt(int bound) {
|
||||
return dataGenerator.nextInt(bound);
|
||||
}
|
||||
|
||||
public synchronized byte[] generateBytes(int length) {
|
||||
byte[] bytes = new byte[length];
|
||||
dataGenerator.nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public synchronized boolean generateBoolean() {
|
||||
return dataGenerator.nextBoolean();
|
||||
}
|
||||
|
||||
public synchronized double generateDouble() {
|
||||
return dataGenerator.nextDouble();
|
||||
}
|
||||
}
|
||||
37
OmCTF-2025/services/jform/src/main/resources/application.yml
Normal file
37
OmCTF-2025/services/jform/src/main/resources/application.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
spring:
|
||||
application:
|
||||
name: jform
|
||||
|
||||
datasource:
|
||||
url: jdbc:oracle:thin:@oracle-db:1521/XEPDB1
|
||||
username: jform
|
||||
password: jform123
|
||||
driver-class-name: oracle.jdbc.OracleDriver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 5
|
||||
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.OracleDialect
|
||||
hibernate:
|
||||
ddl-auto: create
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
jdbc:
|
||||
batch_size: 20
|
||||
|
||||
web:
|
||||
resources:
|
||||
static-locations: classpath:/static/
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ошибка</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: #f0ebf8;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15);
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 80px;
|
||||
color: #ea4335;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Google Sans', sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: #202124;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #5f6368;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #673AB7;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
font-family: 'Google Sans', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background: #5E35B1;
|
||||
box-shadow: 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="error-icon">
|
||||
<span class="material-icons" style="font-size: inherit;">error_outline</span>
|
||||
</div>
|
||||
<h1>Ошибка</h1>
|
||||
<p>Страница не найдена или произошла ошибка при обработке запроса.</p>
|
||||
<a href="/">
|
||||
<span class="material-icons">arrow_back</span>
|
||||
Вернуться на главную
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
496
OmCTF-2025/services/jform/src/main/resources/static/form.html
Normal file
496
OmCTF-2025/services/jform/src/main/resources/static/form.html
Normal file
@@ -0,0 +1,496 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Форма</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: #f0ebf8;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
max-width: 760px;
|
||||
margin: 0 auto 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #673AB7 0%, #9C27B0 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Google Sans', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #1a73e8;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15);
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-top: 10px solid #673AB7;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-family: 'Google Sans', sans-serif;
|
||||
font-size: 32px;
|
||||
font-weight: 400;
|
||||
color: #202124;
|
||||
margin-bottom: 8px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.form-description {
|
||||
font-size: 14px;
|
||||
color: #5f6368;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.question-label {
|
||||
font-size: 16px;
|
||||
color: #202124;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #d93025;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 13px 16px;
|
||||
font-size: 16px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
border: none;
|
||||
border-bottom: 1px solid #dadce0;
|
||||
background: transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input[type="text"]:hover,
|
||||
input[type="email"]:hover,
|
||||
input[type="number"]:hover,
|
||||
input[type="tel"]:hover,
|
||||
input[type="url"]:hover,
|
||||
textarea:hover,
|
||||
select:hover {
|
||||
border-bottom: 1px solid #202124;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="tel"]:focus,
|
||||
input[type="url"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-bottom: 2px solid #673AB7;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 12px 16px;
|
||||
background: white;
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select:hover {
|
||||
border: 1px solid #202124;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
border: 2px solid #673AB7;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
background: #673AB7;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
font-family: 'Google Sans', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
letter-spacing: 0.25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background: #5E35B1;
|
||||
box-shadow: 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #5f6368;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fce8e6;
|
||||
color: #c5221f;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #e6f4ea;
|
||||
color: #137333;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.card-header,
|
||||
.card-content {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<div class="logo-icon">
|
||||
<span class="material-icons">description</span>
|
||||
</div>
|
||||
<span class="logo-text">Forms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a href="/" class="back-link">
|
||||
<span class="material-icons" style="font-size: 18px;">arrow_back</span>
|
||||
На главную
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="loading">
|
||||
<span class="material-icons" style="font-size: 48px; color: #673AB7;">hourglass_empty</span>
|
||||
<p style="margin-top: 16px;">Загрузка формы...</p>
|
||||
</div>
|
||||
|
||||
<form id="response-form" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="form-title" id="form-title">Форма</h1>
|
||||
<p class="form-description" id="form-description"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fields-container"></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<button type="submit">
|
||||
<span class="material-icons" style="font-size: 18px;">send</span>
|
||||
Отправить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="message"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const formId = pathParts[pathParts.length - 1];
|
||||
|
||||
if (!formId || formId === 'form.html') {
|
||||
document.getElementById('loading').innerHTML =
|
||||
'<span class="material-icons" style="font-size: 48px; color: #c5221f;">error</span>' +
|
||||
'<p style="margin-top: 16px; color: #5f6368;">ID формы не указан</p>';
|
||||
}
|
||||
|
||||
window.onload = async function() {
|
||||
if (formId && formId !== 'form.html') {
|
||||
await loadForm();
|
||||
}
|
||||
};
|
||||
|
||||
async function loadForm() {
|
||||
try {
|
||||
const response = await fetch('/api/form/' + formId);
|
||||
|
||||
if (!response.ok) {
|
||||
document.getElementById('loading').innerHTML =
|
||||
'<span class="material-icons" style="font-size: 48px; color: #c5221f;">error</span>' +
|
||||
'<p style="margin-top: 16px; color: #5f6368;">Форма не найдена</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const schema = JSON.parse(data.schema);
|
||||
|
||||
if (schema.title) {
|
||||
document.getElementById('form-title').textContent = schema.title;
|
||||
}
|
||||
|
||||
if (schema.description) {
|
||||
document.getElementById('form-description').textContent = schema.description;
|
||||
}
|
||||
|
||||
const container = document.getElementById('fields-container');
|
||||
|
||||
if (schema.fields && Array.isArray(schema.fields)) {
|
||||
schema.fields.forEach(field => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
|
||||
const cardContent = document.createElement('div');
|
||||
cardContent.className = 'card-content';
|
||||
|
||||
const formGroup = document.createElement('div');
|
||||
formGroup.className = 'form-group';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'question-label';
|
||||
label.innerHTML = (field.label || field.name) + (field.required ? '<span class="required">*</span>' : '');
|
||||
formGroup.appendChild(label);
|
||||
|
||||
let input;
|
||||
|
||||
switch (field.type) {
|
||||
case 'textarea':
|
||||
input = document.createElement('textarea');
|
||||
break;
|
||||
case 'select':
|
||||
input = document.createElement('select');
|
||||
const emptyOption = document.createElement('option');
|
||||
emptyOption.value = '';
|
||||
emptyOption.textContent = 'Выберите вариант';
|
||||
input.appendChild(emptyOption);
|
||||
|
||||
if (field.options && Array.isArray(field.options)) {
|
||||
field.options.forEach(opt => {
|
||||
const option = document.createElement('option');
|
||||
option.value = opt;
|
||||
option.textContent = opt;
|
||||
input.appendChild(option);
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type = field.type || 'text';
|
||||
}
|
||||
|
||||
input.name = field.name;
|
||||
input.id = 'field-' + field.name;
|
||||
|
||||
if (field.required) {
|
||||
input.required = true;
|
||||
}
|
||||
|
||||
if (field.placeholder) {
|
||||
input.placeholder = field.placeholder;
|
||||
}
|
||||
|
||||
formGroup.appendChild(input);
|
||||
cardContent.appendChild(formGroup);
|
||||
card.appendChild(cardContent);
|
||||
container.appendChild(card);
|
||||
});
|
||||
} else {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
const cardContent = document.createElement('div');
|
||||
cardContent.className = 'card-content';
|
||||
|
||||
const formGroup = document.createElement('div');
|
||||
formGroup.className = 'form-group';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'question-label';
|
||||
label.textContent = 'Ваш ответ (JSON):';
|
||||
formGroup.appendChild(label);
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.name = 'response';
|
||||
textarea.placeholder = 'Введите ваш ответ в формате JSON';
|
||||
textarea.style.minHeight = '200px';
|
||||
formGroup.appendChild(textarea);
|
||||
|
||||
cardContent.appendChild(formGroup);
|
||||
card.appendChild(cardContent);
|
||||
container.appendChild(card);
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('response-form').style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
document.getElementById('loading').innerHTML =
|
||||
'<span class="material-icons" style="font-size: 48px; color: #c5221f;">error</span>' +
|
||||
'<p style="margin-top: 16px; color: #5f6368;">Ошибка загрузки формы: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('response-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const response = {};
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
response[key] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/form/' + formId + '/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(response)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
document.getElementById('message').innerHTML =
|
||||
'<div class="success">' +
|
||||
'<span class="material-icons">check_circle</span>' +
|
||||
'Ваш ответ сохранён. Спасибо за участие!' +
|
||||
(data.responseCount ? ' (Ответ #' + data.responseCount + ')' : '') +
|
||||
'</div>';
|
||||
this.reset();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (res.status === 429) {
|
||||
document.getElementById('message').innerHTML =
|
||||
'<div class="error">' +
|
||||
'<span class="material-icons">block</span>' +
|
||||
'<strong>Форма закрыта</strong><br>' +
|
||||
(data.error || 'Достигнут лимит ответов на эту форму') +
|
||||
'</div>';
|
||||
|
||||
this.querySelectorAll('input, textarea, select, button').forEach(el => el.disabled = true);
|
||||
} else {
|
||||
document.getElementById('message').innerHTML =
|
||||
'<div class="error">' +
|
||||
'<span class="material-icons">error</span>' +
|
||||
'Ошибка: ' + (data.error || 'не удалось отправить ответ') +
|
||||
'</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('message').innerHTML =
|
||||
'<div class="error">' +
|
||||
'<span class="material-icons">error</span>' +
|
||||
'Ошибка соединения с сервером' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1099
OmCTF-2025/services/jform/src/main/resources/static/index.html
Normal file
1099
OmCTF-2025/services/jform/src/main/resources/static/index.html
Normal file
File diff suppressed because it is too large
Load Diff
1189
OmCTF-2025/services/jform/src/main/resources/static/profile.html
Normal file
1189
OmCTF-2025/services/jform/src/main/resources/static/profile.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user