initial commit
This commit is contained in:
commit
911c538fb0
6
backend/config.properties
Normal file
6
backend/config.properties
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Database Configuration
|
||||
db.host=localhost
|
||||
db.port=5432
|
||||
db.name=web-4
|
||||
db.username=postgres
|
||||
db.password=postgres
|
98
backend/pom.xml
Normal file
98
backend/pom.xml
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?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>
|
||||
|
||||
<groupId>ru.akarpov.web4</groupId>
|
||||
<artifactId>web-lab4</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>war</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jakarta.ee.version>10.0.0</jakarta.ee.version>
|
||||
<hibernate.version>6.2.7.Final</hibernate.version>
|
||||
<postgresql.version>42.6.0</postgresql.version>
|
||||
<jwt.version>4.4.0</jwt.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Swagger/OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.microprofile.openapi</groupId>
|
||||
<artifactId>microprofile-openapi-api</artifactId>
|
||||
<version>3.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>2.2.15</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-jaxrs2-jakarta</artifactId>
|
||||
<version>2.2.15</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>swagger-ui</artifactId>
|
||||
<version>5.10.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jakarta EE -->
|
||||
<dependency>
|
||||
<groupId>jakarta.platform</groupId>
|
||||
<artifactId>jakarta.jakartaee-api</artifactId>
|
||||
<version>${jakarta.ee.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Hibernate -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
<version>${hibernate.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgresql.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT for authentication -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON-B -->
|
||||
<dependency>
|
||||
<groupId>jakarta.json.bind</groupId>
|
||||
<artifactId>jakarta.json.bind-api</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>web-lab4</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
22
backend/postgres-ds.cli
Normal file
22
backend/postgres-ds.cli
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Connect to the running server
|
||||
connect
|
||||
|
||||
# Add PostgreSQL module
|
||||
module add --name=org.postgres --resources=${user.home}/.m2/repository/org/postgresql/postgresql/42.6.0/postgresql-42.6.0.jar --dependencies=javax.api,javax.transaction.api
|
||||
|
||||
# Add the PostgreSQL driver
|
||||
/subsystem=datasources/jdbc-driver=postgres:add(driver-name="postgres",driver-module-name="org.postgres",driver-class-name=org.postgresql.Driver)
|
||||
|
||||
# Add the datasource
|
||||
data-source add \
|
||||
--jndi-name=java:/PostgresDS \
|
||||
--name=PostgresDS \
|
||||
--connection-url=jdbc:postgresql://localhost:5432/web-4 \
|
||||
--driver-name=postgres \
|
||||
--user-name=postgres \
|
||||
--password=postgres \
|
||||
--min-pool-size=5 \
|
||||
--max-pool-size=15
|
||||
|
||||
# Reload the server configuration
|
||||
reload
|
18
backend/src/main/java/ru/akarpov/web4/config/CorsFilter.java
Normal file
18
backend/src/main/java/ru/akarpov/web4/config/CorsFilter.java
Normal file
|
@ -0,0 +1,18 @@
|
|||
package ru.akarpov.web4.config;
|
||||
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseFilter;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class CorsFilter implements ContainerResponseFilter {
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Origin", "http://localhost:3000");
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true");
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package ru.akarpov.web4.config;
|
||||
|
||||
import jakarta.ws.rs.ApplicationPath;
|
||||
import jakarta.ws.rs.core.Application;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.info.Contact;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.annotations.servers.Server;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
tags = {
|
||||
@Tag(name = "auth", description = "Authentication operations"),
|
||||
@Tag(name = "points", description = "Point operations")
|
||||
},
|
||||
info = @Info(
|
||||
title = "Web Lab 4 API",
|
||||
version = "1.0.0",
|
||||
contact = @Contact(
|
||||
name = "Alexander Karpov",
|
||||
email = "sanspie@akarpov.ru"
|
||||
)
|
||||
),
|
||||
servers = {
|
||||
@Server(
|
||||
url = "/web-lab4",
|
||||
description = "Web Lab 4 Server"
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityScheme(
|
||||
name = "bearerAuth",
|
||||
type = SecuritySchemeType.HTTP,
|
||||
scheme = "bearer",
|
||||
bearerFormat = "JWT"
|
||||
)
|
||||
@ApplicationPath("/api")
|
||||
public class RestApplication extends Application {
|
||||
}
|
54
backend/src/main/java/ru/akarpov/web4/ejb/PointService.java
Normal file
54
backend/src/main/java/ru/akarpov/web4/ejb/PointService.java
Normal file
|
@ -0,0 +1,54 @@
|
|||
package ru.akarpov.web4.ejb;
|
||||
|
||||
import jakarta.ejb.Stateless;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import ru.akarpov.web4.entity.Point;
|
||||
import ru.akarpov.web4.entity.User;
|
||||
import ru.akarpov.web4.util.AreaChecker;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Stateless
|
||||
public class PointService {
|
||||
@PersistenceContext(unitName = "web4PU")
|
||||
private EntityManager em;
|
||||
|
||||
@Inject
|
||||
private UserService userService;
|
||||
|
||||
public Point addPoint(double x, double y, double r, Long userId) {
|
||||
long startTime = System.nanoTime();
|
||||
|
||||
User user = em.find(User.class, userId); // Используем текущую сессию для загрузки
|
||||
if (user == null) {
|
||||
throw new RuntimeException("User not found");
|
||||
}
|
||||
|
||||
Point point = new Point(x, y, r);
|
||||
point.setUser(user);
|
||||
point.setHit(AreaChecker.checkHit(x, y, r));
|
||||
point.setCreatedAt(LocalDateTime.now());
|
||||
point.setExecutionTime((System.nanoTime() - startTime) / 1000000);
|
||||
|
||||
em.persist(point);
|
||||
return point;
|
||||
}
|
||||
|
||||
public List<Point> getUserPoints(Long userId) {
|
||||
return em.createQuery(
|
||||
"SELECT DISTINCT p FROM Point p LEFT JOIN FETCH p.user WHERE p.user.id = :userId ORDER BY p.createdAt DESC",
|
||||
Point.class
|
||||
)
|
||||
.setParameter("userId", userId)
|
||||
.getResultList();
|
||||
}
|
||||
|
||||
public void clearUserPoints(Long userId) {
|
||||
em.createQuery("DELETE FROM Point p WHERE p.user.id = :userId")
|
||||
.setParameter("userId", userId)
|
||||
.executeUpdate();
|
||||
}
|
||||
}
|
47
backend/src/main/java/ru/akarpov/web4/ejb/UserService.java
Normal file
47
backend/src/main/java/ru/akarpov/web4/ejb/UserService.java
Normal file
|
@ -0,0 +1,47 @@
|
|||
package ru.akarpov.web4.ejb;
|
||||
|
||||
import jakarta.ejb.Stateless;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.persistence.NoResultException;
|
||||
import ru.akarpov.web4.entity.User;
|
||||
import ru.akarpov.web4.security.PasswordHasher;
|
||||
import ru.akarpov.web4.exception.DuplicateUsernameException;
|
||||
|
||||
@Stateless
|
||||
public class UserService {
|
||||
@PersistenceContext(unitName = "web4PU")
|
||||
private EntityManager em;
|
||||
|
||||
public User register(String username, String password) {
|
||||
if (findByUsername(username) != null) {
|
||||
throw new DuplicateUsernameException("Username already exists");
|
||||
}
|
||||
|
||||
User user = new User(username, PasswordHasher.hash(password));
|
||||
em.persist(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
public User authenticate(String username, String password) {
|
||||
User user = findByUsername(username);
|
||||
if (user == null || !PasswordHasher.verify(password, user.getPassword())) {
|
||||
throw new DuplicateUsernameException("Invalid username or password");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public User findByUsername(String username) {
|
||||
try {
|
||||
return em.createQuery("SELECT u FROM User u WHERE u.username = :username", User.class)
|
||||
.setParameter("username", username)
|
||||
.getSingleResult();
|
||||
} catch (NoResultException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public User findById(Long id) {
|
||||
return em.find(User.class, id);
|
||||
}
|
||||
}
|
113
backend/src/main/java/ru/akarpov/web4/entity/Point.java
Normal file
113
backend/src/main/java/ru/akarpov/web4/entity/Point.java
Normal file
|
@ -0,0 +1,113 @@
|
|||
package ru.akarpov.web4.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "points")
|
||||
public class Point implements Serializable {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private double x;
|
||||
|
||||
@Column(nullable = false)
|
||||
private double y;
|
||||
|
||||
@Column(nullable = false)
|
||||
private double r;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean hit;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "execution_time")
|
||||
private long executionTime;
|
||||
|
||||
@JsonIgnore
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private User user;
|
||||
|
||||
public Point() {}
|
||||
|
||||
public Point(double x, double y, double r) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.r = r;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setX(double x) {
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setY(double y) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public double getR() {
|
||||
return r;
|
||||
}
|
||||
|
||||
public void setR(double r) {
|
||||
this.r = r;
|
||||
}
|
||||
|
||||
public boolean isHit() {
|
||||
return hit;
|
||||
}
|
||||
|
||||
public void setHit(boolean hit) {
|
||||
this.hit = hit;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public long getExecutionTime() {
|
||||
return executionTime;
|
||||
}
|
||||
|
||||
public void setExecutionTime(long executionTime) {
|
||||
this.executionTime = executionTime;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
}
|
71
backend/src/main/java/ru/akarpov/web4/entity/User.java
Normal file
71
backend/src/main/java/ru/akarpov/web4/entity/User.java
Normal file
|
@ -0,0 +1,71 @@
|
|||
package ru.akarpov.web4.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User implements Serializable {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
private String username;
|
||||
|
||||
@JsonIgnore
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<Point> points = new ArrayList<>();
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public List<Point> getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
public void setPoints(List<Point> points) {
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public void addPoint(Point point) {
|
||||
points.add(point);
|
||||
point.setUser(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package ru.akarpov.web4.exception;
|
||||
|
||||
import jakarta.ejb.ApplicationException;
|
||||
|
||||
@ApplicationException(rollback = true)
|
||||
public class DuplicateUsernameException extends RuntimeException {
|
||||
public DuplicateUsernameException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
113
backend/src/main/java/ru/akarpov/web4/rest/AuthResource.java
Normal file
113
backend/src/main/java/ru/akarpov/web4/rest/AuthResource.java
Normal file
|
@ -0,0 +1,113 @@
|
|||
package ru.akarpov.web4.rest;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ejb.EJB;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import ru.akarpov.web4.ejb.UserService;
|
||||
import ru.akarpov.web4.entity.User;
|
||||
import ru.akarpov.web4.security.JwtUtil;
|
||||
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "auth")
|
||||
public class AuthResource {
|
||||
@EJB
|
||||
private UserService userService;
|
||||
|
||||
@POST
|
||||
@Path("/login")
|
||||
@Operation(
|
||||
summary = "Login user",
|
||||
description = "Authenticates user and returns JWT token"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Authentication successful",
|
||||
content = @Content(schema = @Schema(implementation = AuthResponse.class))
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "401",
|
||||
description = "Authentication failed"
|
||||
)
|
||||
public Response login(LoginRequest request) {
|
||||
try {
|
||||
User user = userService.authenticate(request.username, request.password);
|
||||
String token = JwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
return Response.ok(new AuthResponse(token)).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.UNAUTHORIZED)
|
||||
.entity(new ErrorResponse("Invalid credentials"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/register")
|
||||
@Operation(
|
||||
summary = "Register new user",
|
||||
description = "Creates new user and returns JWT token"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Registration successful",
|
||||
content = @Content(schema = @Schema(implementation = AuthResponse.class))
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Registration failed"
|
||||
)
|
||||
public Response register(RegisterRequest request) {
|
||||
try {
|
||||
User user = userService.register(request.username, request.password);
|
||||
String token = JwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
return Response.ok(new AuthResponse(token)).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "LoginRequest")
|
||||
public static class LoginRequest {
|
||||
@Schema(required = true, example = "user123")
|
||||
public String username;
|
||||
@Schema(required = true, example = "password123")
|
||||
public String password;
|
||||
}
|
||||
|
||||
@Schema(name = "RegisterRequest")
|
||||
public static class RegisterRequest {
|
||||
@Schema(required = true, example = "user123")
|
||||
public String username;
|
||||
@Schema(required = true, example = "password123")
|
||||
public String password;
|
||||
}
|
||||
|
||||
@Schema(name = "AuthResponse")
|
||||
public static class AuthResponse {
|
||||
@Schema(description = "JWT token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||
public String token;
|
||||
|
||||
public AuthResponse(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "ErrorResponse")
|
||||
public static class ErrorResponse {
|
||||
@Schema(description = "Error message")
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
140
backend/src/main/java/ru/akarpov/web4/rest/PointResource.java
Normal file
140
backend/src/main/java/ru/akarpov/web4/rest/PointResource.java
Normal file
|
@ -0,0 +1,140 @@
|
|||
package ru.akarpov.web4.rest;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ejb.EJB;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import ru.akarpov.web4.ejb.PointService;
|
||||
import ru.akarpov.web4.entity.Point;
|
||||
import ru.akarpov.web4.security.JwtUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Path("/points")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "points")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
public class PointResource {
|
||||
@EJB
|
||||
private PointService pointService;
|
||||
|
||||
@POST
|
||||
@Operation(
|
||||
summary = "Add new point",
|
||||
description = "Adds a new point and checks if it hits the area"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Point added successfully",
|
||||
content = @Content(schema = @Schema(implementation = Point.class))
|
||||
)
|
||||
public Response addPoint(
|
||||
@Context HttpHeaders headers,
|
||||
@Parameter(schema = @Schema(implementation = PointRequest.class))
|
||||
PointRequest request
|
||||
) {
|
||||
try {
|
||||
String token = extractToken(headers);
|
||||
Long userId = JwtUtil.getUserIdFromToken(token);
|
||||
|
||||
Point point = pointService.addPoint(
|
||||
request.x,
|
||||
request.y,
|
||||
request.r,
|
||||
userId
|
||||
);
|
||||
|
||||
return Response.ok(point).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Operation(
|
||||
summary = "Get all points",
|
||||
description = "Returns all points for the authenticated user"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Points retrieved successfully",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = Point.class)))
|
||||
)
|
||||
public Response getPoints(@Context HttpHeaders headers) {
|
||||
try {
|
||||
String token = extractToken(headers);
|
||||
Long userId = JwtUtil.getUserIdFromToken(token);
|
||||
|
||||
List<Point> points = pointService.getUserPoints(userId);
|
||||
return Response.ok(points).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Operation(
|
||||
summary = "Clear all points",
|
||||
description = "Deletes all points for the authenticated user"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Points cleared successfully"
|
||||
)
|
||||
public Response clearPoints(@Context HttpHeaders headers) {
|
||||
try {
|
||||
String token = extractToken(headers);
|
||||
Long userId = JwtUtil.getUserIdFromToken(token);
|
||||
|
||||
pointService.clearUserPoints(userId);
|
||||
return Response.ok().build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private String extractToken(HttpHeaders headers) {
|
||||
String authHeader = headers.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
throw new RuntimeException("No valid authorization header found");
|
||||
}
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
|
||||
@Schema(name = "PointRequest")
|
||||
public static class PointRequest {
|
||||
@Schema(required = true, example = "1.5", minimum = "-5", maximum = "3")
|
||||
public double x;
|
||||
@Schema(required = true, example = "2.0", minimum = "-5", maximum = "3")
|
||||
public double y;
|
||||
@Schema(required = true, example = "2.0", minimum = "1", maximum = "4")
|
||||
public double r;
|
||||
}
|
||||
|
||||
@Schema(name = "ErrorResponse")
|
||||
public static class ErrorResponse {
|
||||
@Schema(description = "Error message")
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package ru.akarpov.web4.rest.exception;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class BadRequestExceptionMapper implements ExceptionMapper<BadRequestException> {
|
||||
|
||||
public static class ErrorResponse {
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response toResponse(BadRequestException exception) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(exception.getMessage()))
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package ru.akarpov.web4.rest.exception;
|
||||
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import ru.akarpov.web4.exception.DuplicateUsernameException;
|
||||
|
||||
@Provider
|
||||
public class DuplicateUsernameExceptionMapper implements ExceptionMapper<DuplicateUsernameException> {
|
||||
|
||||
public static class ErrorResponse {
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response toResponse(DuplicateUsernameException exception) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse(exception.getMessage()))
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package ru.akarpov.web4.rest.exception;
|
||||
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class GeneralExceptionMapper implements ExceptionMapper<Exception> {
|
||||
|
||||
public static class ErrorResponse {
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response toResponse(Exception exception) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(exception.getMessage()))
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.build();
|
||||
}
|
||||
}
|
39
backend/src/main/java/ru/akarpov/web4/security/JwtUtil.java
Normal file
39
backend/src/main/java/ru/akarpov/web4/security/JwtUtil.java
Normal file
|
@ -0,0 +1,39 @@
|
|||
package ru.akarpov.web4.security;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class JwtUtil {
|
||||
private static final String SECRET = "please-change-this-secret";
|
||||
private static final Algorithm ALGORITHM = Algorithm.HMAC256(SECRET);
|
||||
private static final long EXPIRATION_TIME = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
public static String generateToken(Long userId, String username) {
|
||||
return JWT.create()
|
||||
.withClaim("userId", userId)
|
||||
.withClaim("username", username)
|
||||
.withIssuedAt(new Date())
|
||||
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
|
||||
.sign(ALGORITHM);
|
||||
}
|
||||
|
||||
public static DecodedJWT verifyToken(String token) {
|
||||
try {
|
||||
JWTVerifier verifier = JWT.require(ALGORITHM)
|
||||
.build();
|
||||
return verifier.verify(token);
|
||||
} catch (JWTVerificationException e) {
|
||||
throw new RuntimeException("Invalid token", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Long getUserIdFromToken(String token) {
|
||||
DecodedJWT jwt = verifyToken(token);
|
||||
return jwt.getClaim("userId").asLong();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package ru.akarpov.web4.security;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PasswordHasher {
|
||||
public static String hash(String password) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Error hashing password", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verify(String password, String hashedPassword) {
|
||||
String newHash = hash(password);
|
||||
return newHash.equals(hashedPassword);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package ru.akarpov.web4.servlet;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.annotation.WebServlet;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
@WebServlet("/swagger")
|
||||
public class SwaggerRedirectServlet extends HttpServlet {
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
response.sendRedirect(request.getContextPath() + "/swagger-ui.html");
|
||||
}
|
||||
}
|
20
backend/src/main/java/ru/akarpov/web4/util/AreaChecker.java
Normal file
20
backend/src/main/java/ru/akarpov/web4/util/AreaChecker.java
Normal file
|
@ -0,0 +1,20 @@
|
|||
package ru.akarpov.web4.util;
|
||||
|
||||
public class AreaChecker {
|
||||
public static boolean checkHit(double x, double y, double r) {
|
||||
// Triangle in -x, -y quadrant
|
||||
if (x <= 0 && y <= 0) {
|
||||
return x >= -r/2 && y >= -r && y >= -2*x - r;
|
||||
}
|
||||
// Square in -x, +y quadrant
|
||||
if (x <= 0 && y >= 0) {
|
||||
return x >= -r/2 && y <= r;
|
||||
}
|
||||
// Quarter circle in +x, -y quadrant
|
||||
if (x >= 0 && y <= 0) {
|
||||
return x*x + y*y <= r*r;
|
||||
}
|
||||
// Nothing in +x, +y quadrant
|
||||
return false;
|
||||
}
|
||||
}
|
18
backend/src/main/resources/META-INF/persistence.xml
Normal file
18
backend/src/main/resources/META-INF/persistence.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
|
||||
version="3.0">
|
||||
<persistence-unit name="web4PU" transaction-type="JTA">
|
||||
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
|
||||
<jta-data-source>java:/PostgresDS</jta-data-source>
|
||||
<class>ru.akarpov.web4.entity.User</class>
|
||||
<class>ru.akarpov.web4.entity.Point</class>
|
||||
<properties>
|
||||
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
|
||||
<property name="hibernate.hbm2ddl.auto" value="update"/>
|
||||
<property name="hibernate.show_sql" value="true"/>
|
||||
<property name="hibernate.format_sql" value="true"/>
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
</persistence>
|
33
backend/src/main/webapp/WEB-INF/web.xml
Normal file
33
backend/src/main/webapp/WEB-INF/web.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
|
||||
version="5.0">
|
||||
|
||||
<welcome-file-list>
|
||||
<welcome-file>index.html</welcome-file>
|
||||
</welcome-file-list>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>jakarta.ws.rs.core.Application</servlet-name>
|
||||
<url-pattern>/api/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Add mime type mappings for Swagger UI -->
|
||||
<mime-mapping>
|
||||
<extension>css</extension>
|
||||
<mime-type>text/css</mime-type>
|
||||
</mime-mapping>
|
||||
<mime-mapping>
|
||||
<extension>js</extension>
|
||||
<mime-type>application/javascript</mime-type>
|
||||
</mime-mapping>
|
||||
|
||||
<!-- Allow access to Swagger resources -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>default</servlet-name>
|
||||
<url-pattern>/swagger-ui.html</url-pattern>
|
||||
<url-pattern>/webjars/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
</web-app>
|
65
backend/src/main/webapp/index.jsp
Normal file
65
backend/src/main/webapp/index.jsp
Normal file
|
@ -0,0 +1,65 @@
|
|||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Приветственная страница</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #1a73e8;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #5f6368;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Вы кто такие?</h2>
|
||||
<p>Я вас не звал, идите нахуй!</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
59
backend/src/main/webapp/swagger-ui.html
Normal file
59
backend/src/main/webapp/swagger-ui.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@latest/swagger-ui.css">
|
||||
<script src="https://unpkg.com/swagger-ui-dist@latest/swagger-ui-bundle.js"></script>
|
||||
<style>
|
||||
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
|
||||
*, *:before, *:after { box-sizing: inherit; }
|
||||
body { margin: 0; background: #fafafa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Get the base URL from the current path
|
||||
const pathArray = window.location.pathname.split('/');
|
||||
const baseUrl = '/' + pathArray[1]; // Should be "/web-lab4"
|
||||
|
||||
const ui = SwaggerUIBundle({
|
||||
url: baseUrl + "/api/openapi",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
docExpansion: 'list',
|
||||
defaultModelsExpandDepth: 1,
|
||||
defaultModelExpandDepth: 1,
|
||||
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
|
||||
tryItOutEnabled: true,
|
||||
persistAuthorization: true,
|
||||
displayRequestDuration: true,
|
||||
filter: true,
|
||||
servers: [
|
||||
{
|
||||
url: baseUrl,
|
||||
description: "Web Lab 4 API Server"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Override the Swagger UI's URL builder to include the correct base path
|
||||
ui.getConfigs().preFetch = (req) => {
|
||||
req.url = baseUrl + req.url;
|
||||
return req;
|
||||
};
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
46
frontend/README.md
Normal file
46
frontend/README.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
17
frontend/craco.config.js
Normal file
17
frontend/craco.config.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
style: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
18789
frontend/package-lock.json
generated
Normal file
18789
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
frontend/package.json
Normal file
50
frontend/package.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "web-lab4-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.7",
|
||||
"@types/node": "^16.18.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"axios": "^1.6.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"proxy": "http://localhost:8080",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"react-scripts": "5.0.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
43
frontend/public/index.html
Normal file
43
frontend/public/index.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
frontend/public/logo192.png
Normal file
BIN
frontend/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/logo512.png
Normal file
BIN
frontend/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
frontend/public/manifest.json
Normal file
25
frontend/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
frontend/public/robots.txt
Normal file
3
frontend/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
38
frontend/src/App.css
Normal file
38
frontend/src/App.css
Normal file
|
@ -0,0 +1,38 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
17
frontend/src/App.tsx
Normal file
17
frontend/src/App.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import StartPage from './pages/StartPage';
|
||||
import MainPage from './pages/MainPage';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<StartPage />} />
|
||||
<Route path="/main" element={<MainPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
138
frontend/src/components/GraphCanvas.tsx
Normal file
138
frontend/src/components/GraphCanvas.tsx
Normal file
|
@ -0,0 +1,138 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState, Point } from '../types';
|
||||
|
||||
interface Props {
|
||||
onPointClick: (x: number, y: number) => void;
|
||||
}
|
||||
|
||||
const GraphCanvas: React.FC<Props> = ({ onPointClick }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const { currentR, points } = useSelector((state: RootState) => state.points);
|
||||
|
||||
const CANVAS_SIZE = 400;
|
||||
const CENTER = CANVAS_SIZE / 2;
|
||||
const SCALE = CANVAS_SIZE / 3;
|
||||
const DOT_SIZE = 4;
|
||||
|
||||
const getRelativeCoordinates = (point: Point) => {
|
||||
// Convert absolute coordinates to relative (based on point's R value)
|
||||
const relativeX = (point.x / point.r) * currentR;
|
||||
const relativeY = (point.y / point.r) * currentR;
|
||||
|
||||
return {
|
||||
x: CENTER + relativeX * (SCALE / currentR),
|
||||
y: CENTER - relativeY * (SCALE / currentR)
|
||||
};
|
||||
};
|
||||
|
||||
const drawGrid = (ctx: CanvasRenderingContext2D) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, CENTER);
|
||||
ctx.lineTo(CANVAS_SIZE, CENTER);
|
||||
ctx.moveTo(CENTER, 0);
|
||||
ctx.lineTo(CENTER, CANVAS_SIZE);
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.stroke();
|
||||
|
||||
// Draw axis labels
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
[-currentR, -currentR/2, currentR/2, currentR].forEach(value => {
|
||||
const x = CENTER + value * (SCALE / currentR);
|
||||
const y = CENTER - value * (SCALE / currentR);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, CENTER - 5);
|
||||
ctx.lineTo(x, CENTER + 5);
|
||||
ctx.moveTo(CENTER - 5, y);
|
||||
ctx.lineTo(CENTER + 5, y);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillText(value.toString(), x, CENTER + 20);
|
||||
ctx.fillText(value.toString(), CENTER - 20, y);
|
||||
});
|
||||
};
|
||||
|
||||
const drawAreas = (ctx: CanvasRenderingContext2D) => {
|
||||
const scaledR = SCALE;
|
||||
ctx.fillStyle = 'rgba(100, 149, 237, 0.5)';
|
||||
|
||||
// Triangle in -x, -y quadrant
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(CENTER, CENTER);
|
||||
ctx.lineTo(CENTER - scaledR/2, CENTER);
|
||||
ctx.lineTo(CENTER, CENTER + scaledR);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Square in -x, +y quadrant
|
||||
ctx.fillRect(
|
||||
CENTER - scaledR/2,
|
||||
CENTER - scaledR,
|
||||
scaledR/2,
|
||||
scaledR
|
||||
);
|
||||
|
||||
// Quarter circle in +x, -y quadrant
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(CENTER, CENTER);
|
||||
ctx.arc(CENTER, CENTER, scaledR, 0, Math.PI/2, false);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
const drawPoints = (ctx: CanvasRenderingContext2D) => {
|
||||
points.forEach(point => {
|
||||
const { x, y } = getRelativeCoordinates(point);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, DOT_SIZE, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = point.hit ? 'green' : 'red';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.stroke();
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
const graphX = Number(((x - CENTER) * (currentR / SCALE)).toFixed(2));
|
||||
const graphY = Number(((CENTER - y) * (currentR / SCALE)).toFixed(2));
|
||||
|
||||
onPointClick(graphX, graphY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
|
||||
drawGrid(ctx);
|
||||
drawAreas(ctx);
|
||||
drawPoints(ctx);
|
||||
}, [currentR, points]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={CANVAS_SIZE}
|
||||
height={CANVAS_SIZE}
|
||||
onClick={handleClick}
|
||||
className="bg-white rounded shadow-md cursor-crosshair"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphCanvas;
|
162
frontend/src/components/LoginForm.tsx
Normal file
162
frontend/src/components/LoginForm.tsx
Normal file
|
@ -0,0 +1,162 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {login, clearError, register} from '../store/authSlice';
|
||||
import { AppDispatch, RootState } from '../store';
|
||||
|
||||
export const LoginForm: React.FC = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { loading, error } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Clear error when unmounting
|
||||
return () => {
|
||||
dispatch(clearError());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
dispatch(login({ username, password }));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// RegisterForm.tsx
|
||||
export const RegisterForm: React.FC = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { loading, error } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Clear error when unmounting
|
||||
return () => {
|
||||
dispatch(clearError());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setValidationError('');
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setValidationError('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
setValidationError('Password must be at least 6 characters long');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(register({ username, password }));
|
||||
};
|
||||
|
||||
const displayError = validationError || error;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{displayError && (
|
||||
<div className="text-red-500 text-sm">
|
||||
{displayError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Registering...' : 'Register'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
121
frontend/src/components/PointForm.tsx
Normal file
121
frontend/src/components/PointForm.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { addPoint, setR } from '../store/pointsSlice';
|
||||
import { AppDispatch, RootState } from '../store';
|
||||
|
||||
const PointForm: React.FC = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { currentR, loading } = useSelector((state: RootState) => state.points);
|
||||
|
||||
const [x, setX] = useState<string>('');
|
||||
const [y, setY] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const validateInputs = () => {
|
||||
const xNum = Number(x);
|
||||
const yNum = Number(y);
|
||||
const rNum = Number(currentR);
|
||||
|
||||
if (isNaN(xNum) || isNaN(yNum) || isNaN(rNum)) {
|
||||
setError('All values must be numbers');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xNum < -5 || xNum > 3) {
|
||||
setError('X must be between -5 and 3');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (yNum < -5 || yNum > 3) {
|
||||
setError('Y must be between -5 and 3');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rNum < 1 || rNum > 4) {
|
||||
setError('R must be between 1 and 4');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!validateInputs()) return;
|
||||
|
||||
dispatch(addPoint({
|
||||
x: Number(x),
|
||||
y: Number(y),
|
||||
r: Number(currentR)
|
||||
}));
|
||||
|
||||
setX('');
|
||||
setY('');
|
||||
};
|
||||
|
||||
const handleRChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = Number(e.target.value);
|
||||
if (!isNaN(value) && value >= 1 && value <= 4) {
|
||||
dispatch(setR(value));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">X:</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={x}
|
||||
onChange={(e) => setX(e.target.value)}
|
||||
placeholder="Enter X (-5 to 3)"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Y:</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={y}
|
||||
onChange={(e) => setY(e.target.value)}
|
||||
placeholder="Enter Y (-5 to 3)"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">R:</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={currentR}
|
||||
onChange={handleRChange}
|
||||
placeholder="Enter R (1 to 4)"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-sm">{error}</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Adding...' : 'Add Point'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PointForm;
|
87
frontend/src/components/PointsTable.tsx
Normal file
87
frontend/src/components/PointsTable.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import React from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../hooks/redux';
|
||||
import { clearPoints } from '../store/pointsSlice';
|
||||
|
||||
const PointsTable: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { points, loading } = useAppSelector(state => state.points);
|
||||
|
||||
const handleClear = () => {
|
||||
dispatch(clearPoints());
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
// Parse the backend date format (assuming ISO format from LocalDateTime)
|
||||
try {
|
||||
const date = new Date(dateStr.replace('[UTC]', ''));
|
||||
return new Intl.DateTimeFormat('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}).format(date);
|
||||
} catch (e) {
|
||||
console.error('Error formatting date:', dateStr, e);
|
||||
return dateStr; // Return original string if parsing fails
|
||||
}
|
||||
};
|
||||
|
||||
const formatExecutionTime = (time: number) => {
|
||||
return time.toFixed(3);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold">Results</h2>
|
||||
<button
|
||||
onClick={handleClear}
|
||||
disabled={loading || points.length === 0}
|
||||
className="bg-red-500 text-white py-1 px-3 rounded hover:bg-red-600 disabled:opacity-50"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">X</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Y</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">R</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Hit</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Execution (ms)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{points.map((point) => (
|
||||
<tr key={point.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{point.x}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{point.y}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{point.r}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`${point.hit ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{point.hit ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(point.createdAt)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatExecutionTime(point.executionTime)}</td>
|
||||
</tr>
|
||||
))}
|
||||
{points.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-4 text-center text-gray-500">
|
||||
No points added yet
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PointsTable;
|
90
frontend/src/components/RegisterForm.tsx
Normal file
90
frontend/src/components/RegisterForm.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { register } from '../store/authSlice';
|
||||
import { AppDispatch, RootState } from '../store';
|
||||
|
||||
const RegisterForm: React.FC = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { loading, error } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setValidationError('');
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setValidationError('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
setValidationError('Password must be at least 6 characters long');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(register({ username, password }));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(error || validationError) && (
|
||||
<div className="text-red-500 text-sm">
|
||||
{validationError || error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Registering...' : 'Register'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterForm;
|
5
frontend/src/hooks/redux.ts
Normal file
5
frontend/src/hooks/redux.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from '../store';
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
3
frontend/src/index.css
Normal file
3
frontend/src/index.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
18
frontend/src/index.tsx
Normal file
18
frontend/src/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './store';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
1
frontend/src/logo.svg
Normal file
1
frontend/src/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
73
frontend/src/pages/MainPage.tsx
Normal file
73
frontend/src/pages/MainPage.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppDispatch, RootState } from '../store';
|
||||
import { logout } from '../store/authSlice';
|
||||
import { addPoint, fetchPoints } from '../store/pointsSlice';
|
||||
import GraphCanvas from '../components/GraphCanvas';
|
||||
import PointForm from '../components/PointForm';
|
||||
import PointsTable from '../components/PointsTable';
|
||||
|
||||
const MainPage: React.FC = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { token } = useSelector((state: RootState) => state.auth);
|
||||
const { currentR } = useSelector((state: RootState) => state.points);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
dispatch(fetchPoints());
|
||||
}
|
||||
}, [dispatch, token]);
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
const handleCanvasClick = (x: number, y: number) => {
|
||||
dispatch(addPoint({ x, y, r: currentR }));
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logout());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8 flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Web Lab 4</h1>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<GraphCanvas onPointClick={handleCanvasClick} />
|
||||
</div>
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<PointForm />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<PointsTable />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="bg-white shadow mt-8">
|
||||
<div className="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 text-center text-gray-600">
|
||||
<p>Карпов Александр Дмитриевич | P3213 | Вариант 443</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPage;
|
63
frontend/src/pages/StartPage.tsx
Normal file
63
frontend/src/pages/StartPage.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@/store';
|
||||
import {LoginForm} from '../components/LoginForm';
|
||||
import RegisterForm from '../components/RegisterForm';
|
||||
|
||||
const StartPage: React.FC = () => {
|
||||
const { token } = useSelector((state: RootState) => state.auth);
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
|
||||
if (token) {
|
||||
return <Navigate to="/main" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<h1 className="text-center text-3xl font-bold text-gray-900 mb-8">
|
||||
Web Lab 4
|
||||
</h1>
|
||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<div className="text-center mb-6">
|
||||
<p className="text-sm text-gray-600">
|
||||
Карпов Александр Дмитриевич<br />
|
||||
Группа: P3213<br />
|
||||
Вариант: 443
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-center space-x-4">
|
||||
<button
|
||||
onClick={() => setIsLogin(true)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||
isLogin
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-700 bg-gray-100 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsLogin(false)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||
!isLogin
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-700 bg-gray-100 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLogin ? <LoginForm /> : <RegisterForm />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartPage;
|
15
frontend/src/reportWebVitals.ts
Normal file
15
frontend/src/reportWebVitals.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
47
frontend/src/services/api.ts
Normal file
47
frontend/src/services/api.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import axios from 'axios';
|
||||
import { AuthResponse, LoginRequest, RegisterRequest, Point, PointRequest } from '../types';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:8080/web-lab4/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
export const authApi = {
|
||||
login: async (data: LoginRequest): Promise<AuthResponse> => {
|
||||
const response = await api.post<AuthResponse>('/auth/login', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
register: async (data: RegisterRequest): Promise<AuthResponse> => {
|
||||
const response = await api.post<AuthResponse>('/auth/register', data);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const pointsApi = {
|
||||
addPoint: async (data: PointRequest): Promise<Point> => {
|
||||
const response = await api.post<Point>('/points', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPoints: async (): Promise<Point[]> => {
|
||||
const response = await api.get<Point[]>('/points');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
clearPoints: async (): Promise<void> => {
|
||||
await api.delete('/points');
|
||||
}
|
||||
};
|
||||
|
||||
export default api;
|
5
frontend/src/setupTests.ts
Normal file
5
frontend/src/setupTests.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
93
frontend/src/store/authSlice.ts
Normal file
93
frontend/src/store/authSlice.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
import { authApi } from '../services/api';
|
||||
import { AuthState, LoginRequest, RegisterRequest } from '../types';
|
||||
|
||||
const initialState: AuthState = {
|
||||
token: localStorage.getItem('token'),
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
export const login = createAsyncThunk(
|
||||
'auth/login',
|
||||
async (data: LoginRequest, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await authApi.login(data);
|
||||
localStorage.setItem('token', response.token);
|
||||
return response;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err) && err.response?.data?.message) {
|
||||
return rejectWithValue(err.response.data.message);
|
||||
}
|
||||
return rejectWithValue('Authentication failed');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const register = createAsyncThunk(
|
||||
'auth/register',
|
||||
async (data: RegisterRequest, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await authApi.register(data);
|
||||
localStorage.setItem('token', response.token);
|
||||
return response;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err) && err.response?.data?.message) {
|
||||
return rejectWithValue(err.response.data.message);
|
||||
}
|
||||
return rejectWithValue('Registration failed');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
logout: (state) => {
|
||||
state.token = null;
|
||||
state.user = null;
|
||||
state.error = null;
|
||||
localStorage.removeItem('token');
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Login
|
||||
builder
|
||||
.addCase(login.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(login.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.token = action.payload.token;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(login.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string || 'Login failed';
|
||||
})
|
||||
// Register
|
||||
.addCase(register.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(register.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.token = action.payload.token;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(register.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string || 'Registration failed';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { logout, clearError } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
15
frontend/src/store/index.ts
Normal file
15
frontend/src/store/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import authReducer from './authSlice';
|
||||
import pointsReducer from './pointsSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
points: pointsReducer
|
||||
}
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export default store;
|
88
frontend/src/store/pointsSlice.ts
Normal file
88
frontend/src/store/pointsSlice.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { pointsApi } from '../services/api';
|
||||
import { PointsState, PointRequest } from '../types';
|
||||
|
||||
const initialState: PointsState = {
|
||||
points: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
currentR: 2
|
||||
};
|
||||
|
||||
export const addPoint = createAsyncThunk(
|
||||
'points/add',
|
||||
async (data: PointRequest) => {
|
||||
const response = await pointsApi.addPoint(data);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchPoints = createAsyncThunk(
|
||||
'points/fetch',
|
||||
async () => {
|
||||
const response = await pointsApi.getPoints();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const clearPoints = createAsyncThunk(
|
||||
'points/clear',
|
||||
async () => {
|
||||
await pointsApi.clearPoints();
|
||||
}
|
||||
);
|
||||
|
||||
const pointsSlice = createSlice({
|
||||
name: 'points',
|
||||
initialState,
|
||||
reducers: {
|
||||
setR: (state, action) => {
|
||||
state.currentR = action.payload;
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(addPoint.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(addPoint.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.points.unshift(action.payload);
|
||||
})
|
||||
.addCase(addPoint.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error.message || 'Failed to add point';
|
||||
})
|
||||
.addCase(fetchPoints.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchPoints.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.points = action.payload;
|
||||
})
|
||||
.addCase(fetchPoints.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error.message || 'Failed to fetch points';
|
||||
})
|
||||
.addCase(clearPoints.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(clearPoints.fulfilled, (state) => {
|
||||
state.loading = false;
|
||||
state.points = [];
|
||||
})
|
||||
.addCase(clearPoints.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error.message || 'Failed to clear points';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { setR, clearError } = pointsSlice.actions;
|
||||
export default pointsSlice.reducer;
|
57
frontend/src/types/index.ts
Normal file
57
frontend/src/types/index.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
export interface Point {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
r: number;
|
||||
hit: boolean;
|
||||
createdAt: string;
|
||||
executionTime: number;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
token: string | null;
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface PointsState {
|
||||
points: Point[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
currentR: number;
|
||||
}
|
||||
|
||||
export interface RootState {
|
||||
auth: AuthState;
|
||||
points: PointsState;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface PointRequest {
|
||||
x: number;
|
||||
y: number;
|
||||
r: number;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
message: string;
|
||||
}
|
15
frontend/tailwind.config.js
Normal file
15
frontend/tailwind.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
'sm': '703px',
|
||||
'lg': '1219px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
26
frontend/tsconfig.json
Normal file
26
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"extends": "./tsconfig.paths.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
8
frontend/tsconfig.paths.json
Normal file
8
frontend/tsconfig.paths.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
4
wildfly/.galleon/hashes/appclient/configuration/hashes
Normal file
4
wildfly/.galleon/hashes/appclient/configuration/hashes
Normal file
|
@ -0,0 +1,4 @@
|
|||
appclient.xml
|
||||
532495c876209d571f74d5a3300d22536f9746a1
|
||||
logging.properties
|
||||
f23d8fbead4340131038bf8f07af607f9b73134b
|
8
wildfly/.galleon/hashes/bin/client/hashes
Normal file
8
wildfly/.galleon/hashes/bin/client/hashes
Normal file
|
@ -0,0 +1,8 @@
|
|||
README-CLI-JCONSOLE.txt
|
||||
4170faae58c767cc5b7f82c1aff27184eef5a4a0
|
||||
README-EJB-JMS.txt
|
||||
a996dcda2c002b59d2728c263d5f105a8c79c2bd
|
||||
jboss-cli-client.jar
|
||||
acd8deaa6b74f761fb74ff0dffe53a0183b9f46f
|
||||
jboss-client.jar
|
||||
411c2afa8448065abf23c104160edf00ad01bb58
|
106
wildfly/.galleon/hashes/bin/hashes
Normal file
106
wildfly/.galleon/hashes/bin/hashes
Normal file
|
@ -0,0 +1,106 @@
|
|||
.jbossclirc
|
||||
dcd83889155492eb6078e137231699a4f02cc552
|
||||
add-user.bat
|
||||
5ecac88c71bb96f0bb623a81b3daf72c7854ab7a
|
||||
add-user.properties
|
||||
e14860abc742b3b256685cd0121733a3aaeaed5b
|
||||
add-user.ps1
|
||||
3f0f984395185e1ee1684e18daaf71b0288a75cc
|
||||
add-user.sh
|
||||
7f2d9924da3e1fc95265afcc1e8af48a257be423
|
||||
appclient.bat
|
||||
a014071131d24038fc312a47c1fe1f3d2b6bf20a
|
||||
appclient.conf
|
||||
f6448abb8fa09fdf93ee2e9cdd34820c3443fccc
|
||||
appclient.conf.bat
|
||||
848ea40a23dc00c5e924a245946994b33a4e47fd
|
||||
appclient.conf.ps1
|
||||
84b82401b63782760bee661ed65903e43b678402
|
||||
appclient.ps1
|
||||
7f09b3d6124071f3f36bfed8d8d79468cec84717
|
||||
appclient.sh
|
||||
412b60d883aaf53eda49c43ed9366a9512693021
|
||||
common.bat
|
||||
ead9b9128bf85c1412d21a4da96ee38baaf3bf9b
|
||||
common.ps1
|
||||
c592c86ff9bc310cb255a4626ea75e52606127e2
|
||||
common.sh
|
||||
ef48be67bf154d62ad36834715e297ec7bfb3a1a
|
||||
domain.bat
|
||||
651ad7f14cfea8236c0ddf31349a48de0cc70bfb
|
||||
domain.conf
|
||||
9d16c52d5426b19746f33234e2361a3260f683e7
|
||||
domain.conf.bat
|
||||
a4acf634a062c87fb3ef2866380a7310c562295b
|
||||
domain.conf.ps1
|
||||
2e0b4a55fa57fa9942ce0f912c1a4485da13b50b
|
||||
domain.ps1
|
||||
624b8d9d0c78516cbc7747620012a0f296f725f9
|
||||
domain.sh
|
||||
b59e40c7d89ae7152669f43f9e31be919c94c883
|
||||
elytron-tool.bat
|
||||
2a279a07892f2f177924cb431d2d3fd5a7a0b186
|
||||
elytron-tool.ps1
|
||||
d6e55c48a41ffe8b9cffd3f675e7f6b9bb61e34e
|
||||
elytron-tool.sh
|
||||
2bf4b2bcbb69498d93e766a0cda78ace217e2e18
|
||||
installation-manager.bat
|
||||
1a94bb4b1e80254f81db27d25bf0f37d23aca111
|
||||
installation-manager.properties
|
||||
a9443c682329c0b30089dcc0bdaade5abb6f968c
|
||||
installation-manager.ps1
|
||||
ed7c3535cf9b08f8b06b6efd2666afb1e3dd7f44
|
||||
installation-manager.sh
|
||||
d4015d7070719287b9392d3513b43c82313e8dde
|
||||
jboss-cli-logging.properties
|
||||
d2f5e1ee67a12ab2856137af249e19c57d9e7e00
|
||||
jboss-cli.bat
|
||||
55d0e127970fad2139c681d7e50e532bc2901f48
|
||||
jboss-cli.ps1
|
||||
e5baf2bc003162f0cb4c61e7c65619b03589814e
|
||||
jboss-cli.sh
|
||||
2dbf86ae0132fd1b0958640fef9789e4474b2e0e
|
||||
jboss-cli.xml
|
||||
e06123048694c6624fc7827ec458dd4447e474f6
|
||||
jconsole.bat
|
||||
c3077b2e130c8690f806663aa0d4e93fb0dbe773
|
||||
jconsole.ps1
|
||||
7f022379decba2efa2b02faf03cf66b2e5e94eb4
|
||||
jconsole.sh
|
||||
d237aca74d0c04026387fc7c0c7896a7bb6d21ed
|
||||
jdr.bat
|
||||
5fc483ce9df1f50ee71d953276381265a77ad508
|
||||
jdr.ps1
|
||||
1d953c5f1a1f8f589928c96a417d45da76883deb
|
||||
jdr.sh
|
||||
916490961d30ce4c34ce78782409ed267bfeefbe
|
||||
launcher.jar
|
||||
8984f477b583f9a8a171bfb1d29f98fc9968287f
|
||||
product.conf
|
||||
ba237d5eb52965bcbf1d6fa558f1566c0157b22b
|
||||
standalone.bat
|
||||
594ba0bcffee68f04f2189ce41258ffdfb616754
|
||||
standalone.conf
|
||||
9ea4032accd5bfe61aebe8c7e401ab31a1fbf40e
|
||||
standalone.conf.bat
|
||||
85710239019fdb1da6a50b77584261107800519e
|
||||
standalone.conf.ps1
|
||||
68e6a7206195e28475302ed2eed238e0c0ab958c
|
||||
standalone.ps1
|
||||
6e1f68fef7203212d8df61c2da557471dcd80514
|
||||
standalone.sh
|
||||
c5a306e784dc0e13d73780e0925d9e2df71bec38
|
||||
wildfly-elytron-tool.jar
|
||||
badaa021e8f2fac88632bcde8c68eea72e4f0b35
|
||||
wsconsume.bat
|
||||
0df6b935bd43dc5767d13b9cbbb37c6014dccb66
|
||||
wsconsume.ps1
|
||||
d1553e0c54fe247ae3bdd72f3799bb343c898fe4
|
||||
wsconsume.sh
|
||||
4a8a87300ea670f7c25c4bd3fd69923bd5c9ebf2
|
||||
wsprovide.bat
|
||||
2c548ce81210f684f3dc0f6646bacd7e9156c556
|
||||
wsprovide.ps1
|
||||
94798618662f981d21e285067fc5c0f5e46cb708
|
||||
wsprovide.sh
|
||||
c6dda8ca7a6c9d49a522f9b15b5bde6b8b2cc5d3
|
2
wildfly/.galleon/hashes/docs/contrib/scripts/hashes
Normal file
2
wildfly/.galleon/hashes/docs/contrib/scripts/hashes
Normal file
|
@ -0,0 +1,2 @@
|
|||
README.md
|
||||
a503e1f53e876dff2dabbb5e0dfccd4c9b07192f
|
|
@ -0,0 +1,6 @@
|
|||
wildfly-init-debian.sh
|
||||
fa501ea0a2a20b75eb537c3ba0f56c8be6dbd6a3
|
||||
wildfly-init-redhat.sh
|
||||
62b0e3f39cf470646f4ac06a2e9e903f95bb5054
|
||||
wildfly.conf
|
||||
c9137d05357cfb2c33feb3b01ae666d254057623
|
|
@ -0,0 +1,2 @@
|
|||
wildfly-service.exe
|
||||
b62a0082e9780327ff7681b9c05da2d706476e42
|
|
@ -0,0 +1,6 @@
|
|||
service.bat
|
||||
d849c6b71473cb85d5079cfc5eb2bc6735ed5650
|
||||
wildfly-mgr.exe
|
||||
39cd9893df296428257366dce25110f0eff4c07b
|
||||
wildfly-service.exe
|
||||
6e4a3a72fb9bd66b6f6755ea3c1fa922a3073453
|
|
@ -0,0 +1,8 @@
|
|||
README
|
||||
ea7d8cf2c8a88751a1681275d4d7e3191140647d
|
||||
launch.sh
|
||||
296ca556f9627ca313528d8e53a400d42241b5e5
|
||||
wildfly.conf
|
||||
41f6c8dcfe4fad4aa43f8aed8f1eb78c58ffb71f
|
||||
wildfly.service
|
||||
152a4416c489fca0f8b6a4b474fc8f7efd484665
|
30
wildfly/.galleon/hashes/docs/examples/configs/hashes
Normal file
30
wildfly/.galleon/hashes/docs/examples/configs/hashes
Normal file
|
@ -0,0 +1,30 @@
|
|||
domain-ec2.xml
|
||||
e96f4a06598c812dae96ae3564aa0b9d9fb7d0fe
|
||||
standalone-activemq-colocated.xml
|
||||
5f1570cf40a906228aac7b72ae6516a13b8cb356
|
||||
standalone-azure-full-ha.xml
|
||||
016f30a17a8d718917bbd334a2e2020eb2ca280c
|
||||
standalone-azure-ha.xml
|
||||
d53551afe579e2b698d1f00bee968c41838b358c
|
||||
standalone-core-microprofile.xml
|
||||
5fbd781fee684f74f9d06e9504e087ee117df40d
|
||||
standalone-core.xml
|
||||
6e354b6db6b11e5458606da11c85dfe425d48bc7
|
||||
standalone-ec2-full-ha.xml
|
||||
68e8eacbcc8fee7ab4a5df643a66980d77e899ee
|
||||
standalone-ec2-ha.xml
|
||||
b60e10d467d12b629aaefb323b9da3dab2ea04fe
|
||||
standalone-genericjms.xml
|
||||
87b88458623939078129c148d74328cfef6e0cd3
|
||||
standalone-gossip-full-ha.xml
|
||||
9d1f0893b328d55a882c277e8be607cd12ff2a83
|
||||
standalone-gossip-ha.xml
|
||||
fea615fa07d0914d884af32be1eadb05f5eef35a
|
||||
standalone-jts.xml
|
||||
17587b6dece2646dacce5b36fa023b5a2eac324f
|
||||
standalone-minimalistic.xml
|
||||
5c218b78a243c0ccd02b0bbce90a946a0981bf8a
|
||||
standalone-rts.xml
|
||||
34dc75ebc42e844df9f23be9775d614b1a763b25
|
||||
standalone-xts.xml
|
||||
f2861b8fdd8d5654dfdcddc5c6c678deb5e8476a
|
2
wildfly/.galleon/hashes/docs/examples/hashes
Normal file
2
wildfly/.galleon/hashes/docs/examples/hashes
Normal file
|
@ -0,0 +1,2 @@
|
|||
enable-microprofile.cli
|
||||
2dd0552bcfc0007fd139808b3fc99fc741f17de1
|
56
wildfly/.galleon/hashes/docs/licenses/hashes
Normal file
56
wildfly/.galleon/hashes/docs/licenses/hashes
Normal file
|
@ -0,0 +1,56 @@
|
|||
MIT_CONTRIBUTORS.txt
|
||||
c40dbd2dbcd064fd5e2e6e48e2c986ed9db9f1f1
|
||||
apache license 2.0.txt
|
||||
2b8b815229aa8a61e483fb4ba0588b8b6c491890
|
||||
bsd 2-clause simplified license.html
|
||||
257a88e5266e7383d43e66a62b25d14f8e9d5731
|
||||
bsd 3-clause new or revised license.html
|
||||
ea037166f736172dfcb91ab34e211b325c787823
|
||||
common public license 1.0.txt
|
||||
7cdb9c36e1d419e07f88628cd016c0481796b89e
|
||||
creative commons attribution 2.5.html
|
||||
a3f15d06f44729420d7ab2d87ca9bee7cf2820fe
|
||||
eclipse distribution license, version 1.0.txt
|
||||
d520e5d0b7b10f2d36f17ba00b7ea38704f3eed7
|
||||
eclipse public license 1.0.txt
|
||||
77a188d578cd71a45134542ea192f93064d1ebf1
|
||||
eclipse public license 2.0.txt
|
||||
b086d72d0fe9af38418dab524fe76eea3cb1eec3
|
||||
fsf all permissive license.html
|
||||
fcf4e258a3d90869c3a2e7fc55154559318b8eb4
|
||||
gnu general public license v2.0 only, with classpath exception.txt
|
||||
cef61f92dbf47fb27b16d36a4146ec7df7dced6d
|
||||
gnu lesser general public license v2.1 only.txt
|
||||
01a6b4bf79aca9b556822601186afab86e8c4fbf
|
||||
gnu lesser general public license v2.1 or later.txt
|
||||
46dee26f31cce329fa13eacb74f8ac5e52723380
|
||||
gnu lesser general public license v3.0 only.txt
|
||||
e7d563f52bf5295e6dba1d67ac23e9f6a160fab9
|
||||
gnu lesser general public license v3.0 or later.txt
|
||||
e7d563f52bf5295e6dba1d67ac23e9f6a160fab9
|
||||
gnu library general public license v2 only.txt
|
||||
44f7289042b71631acac29b2f143330d2da2479e
|
||||
indiana university extreme lab software license 1.1.1.html
|
||||
451bc145d38a999eab098ca1e10ef74a5db08654
|
||||
licenses.css
|
||||
b4bdad965c7c9487b8390ea9d910df591b62a680
|
||||
licenses.html
|
||||
8fe1f76aa075e6092e83ce75aa916cf0c26edfe1
|
||||
licenses.xml
|
||||
e0d5b4737ca5ebab305d095575f1c2c0363feaeb
|
||||
licenses.xsl
|
||||
bdf6fbb28dce0e2eed2a2bf1a3e4e030f03536b2
|
||||
mit license.txt
|
||||
19eff4fc59155a9343582148816eeb9ffb244279
|
||||
mit-0.html
|
||||
50ac12745b7d3674089f3dc1cf326ee0365ef249
|
||||
mozilla public license 2.0.html
|
||||
c541f06ec7085d6074c6599f38b11ae4a2a31050
|
||||
wildfly-ee-feature-pack-licenses.html
|
||||
6c2d8d05967257aec7bbd2071b84dca54dc1cc28
|
||||
wildfly-ee-feature-pack-licenses.xml
|
||||
099f289f65f4c8c6e4bcdbfda679159a3c82d5b8
|
||||
wildfly-feature-pack-licenses.html
|
||||
97c018e62d71ee84ade52b82ce6ec367d3fb11e8
|
||||
wildfly-feature-pack-licenses.xml
|
||||
44445407a62b9ec9c0b5e50ba32a2396eaa5d67f
|
1072
wildfly/.galleon/hashes/docs/schema/hashes
Normal file
1072
wildfly/.galleon/hashes/docs/schema/hashes
Normal file
File diff suppressed because it is too large
Load Diff
20
wildfly/.galleon/hashes/domain/configuration/hashes
Normal file
20
wildfly/.galleon/hashes/domain/configuration/hashes
Normal file
|
@ -0,0 +1,20 @@
|
|||
application-roles.properties
|
||||
2339b5b3b5544b9a54b2a17a077a43277f4becac
|
||||
application-users.properties
|
||||
0cdb9ca548eca6503a3cb0e4c7ce80f5336a1c82
|
||||
default-server-logging.properties
|
||||
f79da20ce3f13ecf153f75c77bf9d9cb7870509d
|
||||
domain.xml
|
||||
daaecccdabe0ae4bd03815b2bbae7f922803efb9
|
||||
host-primary.xml
|
||||
925039b4ddb39e39840e77ae069ca505c93392d4
|
||||
host-secondary.xml
|
||||
a1e167d1161fa1dac7793729144d850514726e9f
|
||||
host.xml
|
||||
730c4b5bac2ef744b0127ac57df7e526c5e1ab17
|
||||
logging.properties
|
||||
1138aa7e1af0a0a93f0dc8754773b19dcbc7a68e
|
||||
mgmt-groups.properties
|
||||
8a5ca3eeb904c2b6f2bc60f12e7cefed4b5d09b0
|
||||
mgmt-users.properties
|
||||
b79a69eb57c60d16a626e20256c7531e2e22fe21
|
8
wildfly/.galleon/hashes/hashes
Normal file
8
wildfly/.galleon/hashes/hashes
Normal file
|
@ -0,0 +1,8 @@
|
|||
LICENSE.txt
|
||||
58853eb8199b5afe72a73a25fd8cf8c94285174b
|
||||
README.txt
|
||||
4e02800d2609dc82e957cfcf5eae12eeb1120349
|
||||
copyright.txt
|
||||
165a59628b050f2faa2477a2a62a92aa024ca1e3
|
||||
jboss-modules.jar
|
||||
ad33a0ca75c8189ebbfd8eedbb489a1453b96dea
|
|
@ -0,0 +1,6 @@
|
|||
asm-9.7.jar
|
||||
073d7b3086e14beb604ced229c302feff6449723
|
||||
asm-util-9.7.jar
|
||||
c0655519f24d92af2202cb681cd7c1569df6ead6
|
||||
module.xml
|
||||
2fdd92766986752fe05bf9e499a5b89fbfed4b5a
|
|
@ -0,0 +1,4 @@
|
|||
hppc-0.8.1.jar
|
||||
ffc7ba8f289428b9508ab484b8001dea944ae603
|
||||
module.xml
|
||||
7ff9b09d68ddb811bca586f99c7e4895fc11cc99
|
|
@ -0,0 +1,4 @@
|
|||
classmate-1.5.1.jar
|
||||
3fe0bed568c62df5e89f4f174c101eab25345b6c
|
||||
module.xml
|
||||
2c2407d2d12a8b2cd853b25ca0592659cf961c23
|
|
@ -0,0 +1,4 @@
|
|||
jackson-annotations-2.17.0.jar
|
||||
880a742337010da4c851f843d8cac150e22dff9f
|
||||
module.xml
|
||||
e4a8dbea35ecf4bdb108eefc4f7a5fefc2bd8ad0
|
|
@ -0,0 +1,4 @@
|
|||
jackson-core-2.17.0.jar
|
||||
a6e5058ef9720623c517252d17162f845306ff3a
|
||||
module.xml
|
||||
68cdc0d521772e10b6bd0ed00f8e9d5d1a8fa58c
|
|
@ -0,0 +1,4 @@
|
|||
jackson-databind-2.17.0.jar
|
||||
7173e9e1d4bc6d7ca03bc4eeedcd548b8b580b34
|
||||
module.xml
|
||||
afae2e2a7304f640bd4c77db1e7013cb48e6fbce
|
|
@ -0,0 +1,4 @@
|
|||
jackson-dataformat-yaml-2.17.0.jar
|
||||
57a963c6258c49febc11390082d8503f71bb15a9
|
||||
module.xml
|
||||
43440d6ca1b0a4d926a413a73f59c4adae389922
|
|
@ -0,0 +1,4 @@
|
|||
jackson-datatype-jdk8-2.17.0.jar
|
||||
95519a116d909faec29da76cf6b944b4a84c2c26
|
||||
module.xml
|
||||
4f672ef5b562b6de8be01280e228d87f3cdab036
|
|
@ -0,0 +1,4 @@
|
|||
jackson-datatype-jsr310-2.17.0.jar
|
||||
3fab507bba9d477e52ed2302dc3ddbd23cbae339
|
||||
module.xml
|
||||
f3e236cbeb6c19474f74c1798dd38dbc66b7a4a6
|
|
@ -0,0 +1,8 @@
|
|||
jackson-jakarta-rs-base-2.17.0.jar
|
||||
191c316a3951956fb982b7965cea7e142d9fb87e
|
||||
jackson-jakarta-rs-json-provider-2.17.0.jar
|
||||
2f86fc40907f018a45c4d5dd2a5ba43b526fa0cb
|
||||
jackson-module-jakarta-xmlbind-annotations-2.17.0.jar
|
||||
0e652f73b9c3d897b51336d9e17c2267bb716917
|
||||
module.xml
|
||||
68d13970e8c3a97f5e048798554507fd12b72081
|
|
@ -0,0 +1,2 @@
|
|||
module.xml
|
||||
09257ddbe3ca160f3ccae184177749efb82a72ce
|
|
@ -0,0 +1,4 @@
|
|||
caffeine-3.1.8.jar
|
||||
24795585df8afaf70a2cd534786904ea5889c047
|
||||
module.xml
|
||||
b5d5c01752d3997d117ec900faa1f167a9b1c285
|
|
@ -0,0 +1,4 @@
|
|||
btf-1.2.jar
|
||||
9e66651022eb86301b348d57e6f59459effc343b
|
||||
module.xml
|
||||
25ed39b0eb6131b1b16f5ad02344a2c4a6bce80c
|
|
@ -0,0 +1,4 @@
|
|||
jackson-coreutils-1.8.jar
|
||||
491a6e1130a180c153df9f2b7aabd7a700282c67
|
||||
module.xml
|
||||
edd282e5b4a283be2818d75ca8d9f12147a5f241
|
|
@ -0,0 +1,4 @@
|
|||
json-patch-1.9.jar
|
||||
0a4c3c97a0f5965dec15795acf40d3fbc897af4b
|
||||
module.xml
|
||||
d617d8be6e99f49dbf75754fb3b4568b38237e6b
|
|
@ -0,0 +1,4 @@
|
|||
module.xml
|
||||
55470daae6c253ebf9e114335255bd209e7a4c1e
|
||||
msg-simple-1.1.jar
|
||||
f261263e13dd4cfa93cc6b83f1f58f619097a2c4
|
|
@ -0,0 +1,4 @@
|
|||
module.xml
|
||||
d614911cd191a7d479ffa4ac976e043eb963a02e
|
||||
zstd-jni-1.5.6-5.jar
|
||||
6b0abf5f2e68df5ffac02cba09fc2d84f7ebd631
|
|
@ -0,0 +1,4 @@
|
|||
gson-2.8.9.jar
|
||||
8a432c1d6825781e21a02db2e2c33c5fde2833b9
|
||||
module.xml
|
||||
e9fece80c079e4436e45267429813501f4852b2d
|
|
@ -0,0 +1,4 @@
|
|||
failureaccess-1.0.2.jar
|
||||
c4a06a64e650562f30b7bf9aaec1bfed43aca12b
|
||||
module.xml
|
||||
7d7ad1fd8299119798c90c5915e4993f56711804
|
|
@ -0,0 +1,4 @@
|
|||
guava-33.0.0-jre.jar
|
||||
161ba27964a62f241533807a46b8711b13c1d94b
|
||||
module.xml
|
||||
5b9a85ab34e0fe19c3a3b31f4954ae40db2b600f
|
|
@ -0,0 +1,10 @@
|
|||
module.xml
|
||||
eca834efdc39015b90e7129b01d7b1fa6ba5d064
|
||||
perfmark-api-0.23.0.jar
|
||||
0b813b7539fae6550541da8caafd6add86d4e22f
|
||||
proto-google-common-protos-2.0.1.jar
|
||||
20827628ea2b9f69ae22987b2aedb0050e9c470d
|
||||
protobuf-java-3.25.5.jar
|
||||
5ae5c9ec39930ae9b5a61b32b93288818ec05ec1
|
||||
protobuf-java-util-3.25.5.jar
|
||||
38cc5ce479603e36466feda2a9f1dfdb2210ef00
|
|
@ -0,0 +1,4 @@
|
|||
JavaEWAH-1.2.3.jar
|
||||
13a27c856e0c8808cee9a64032c58eee11c3adc9
|
||||
module.xml
|
||||
cbc42f2e507993437ba185fdd71dd24fc070c32d
|
|
@ -0,0 +1,4 @@
|
|||
h2-2.2.224.jar
|
||||
7bdade27d8cd197d9b5ce9dc251f41d2edc5f7ad
|
||||
module.xml
|
||||
8f9bda63c7655da79d2d54a359bcadc474b66143
|
|
@ -0,0 +1,4 @@
|
|||
asyncutil-0.1.0.jar
|
||||
440941c382166029a299602e6c9ff5abde1b5143
|
||||
module.xml
|
||||
4a7d1e02072bbab6c935aa93fc915c26b7c5feae
|
|
@ -0,0 +1,4 @@
|
|||
azure-storage-8.6.6.jar
|
||||
49d84b103a4700134ce56d73b4195f18fd226729
|
||||
module.xml
|
||||
0c37ed43810e653e80519241466019e2ee2d8f82
|
|
@ -0,0 +1,4 @@
|
|||
module.xml
|
||||
0d381cde567d8896a94fe44993a10758ed433b49
|
||||
nimbus-jose-jwt-9.37.3.jar
|
||||
700f71ffefd60c16bd8ce711a956967ea9071cec
|
|
@ -0,0 +1,4 @@
|
|||
module.xml
|
||||
29f238d58ebc8744f45165df41858bf9c0ca4782
|
||||
protoparser-4.0.3.jar
|
||||
e61ee0b108059d97f43143eb2ee7a1be8059a30e
|
|
@ -0,0 +1,2 @@
|
|||
module.xml
|
||||
863fd9eefeafd585ab00c7c1d91a8b16c82f7ab9
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user