initial commit

This commit is contained in:
Alexander Karpov 2024-12-24 01:21:02 +03:00
commit 911c538fb0
2742 changed files with 647729 additions and 0 deletions

View 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
View 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
View 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

View 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");
}
}

View File

@ -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 {
}

View 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();
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View File

@ -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);
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View 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();
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View 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;
}
}

View 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>

View 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>

View 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>

View 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
View 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
View 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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
View 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

File diff suppressed because it is too large Load Diff

50
frontend/package.json Normal file
View 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"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
frontend/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
frontend/src/App.css Normal file
View 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
View 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;

View 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;

View 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>
);
};

View 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;

View 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;

View 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;

View 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
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

18
frontend/src/index.tsx Normal file
View 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
View 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

View 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;

View 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;

View 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;

View 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;

View 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';

View 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;

View 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;

View 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;

View 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;
}

View 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
View 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"]
}

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
}
}

View File

@ -0,0 +1,4 @@
appclient.xml
532495c876209d571f74d5a3300d22536f9746a1
logging.properties
f23d8fbead4340131038bf8f07af607f9b73134b

View File

@ -0,0 +1,8 @@
README-CLI-JCONSOLE.txt
4170faae58c767cc5b7f82c1aff27184eef5a4a0
README-EJB-JMS.txt
a996dcda2c002b59d2728c263d5f105a8c79c2bd
jboss-cli-client.jar
acd8deaa6b74f761fb74ff0dffe53a0183b9f46f
jboss-client.jar
411c2afa8448065abf23c104160edf00ad01bb58

View 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

View File

@ -0,0 +1,2 @@
README.md
a503e1f53e876dff2dabbb5e0dfccd4c9b07192f

View File

@ -0,0 +1,6 @@
wildfly-init-debian.sh
fa501ea0a2a20b75eb537c3ba0f56c8be6dbd6a3
wildfly-init-redhat.sh
62b0e3f39cf470646f4ac06a2e9e903f95bb5054
wildfly.conf
c9137d05357cfb2c33feb3b01ae666d254057623

View File

@ -0,0 +1,2 @@
wildfly-service.exe
b62a0082e9780327ff7681b9c05da2d706476e42

View File

@ -0,0 +1,6 @@
service.bat
d849c6b71473cb85d5079cfc5eb2bc6735ed5650
wildfly-mgr.exe
39cd9893df296428257366dce25110f0eff4c07b
wildfly-service.exe
6e4a3a72fb9bd66b6f6755ea3c1fa922a3073453

View File

@ -0,0 +1,8 @@
README
ea7d8cf2c8a88751a1681275d4d7e3191140647d
launch.sh
296ca556f9627ca313528d8e53a400d42241b5e5
wildfly.conf
41f6c8dcfe4fad4aa43f8aed8f1eb78c58ffb71f
wildfly.service
152a4416c489fca0f8b6a4b474fc8f7efd484665

View 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

View File

@ -0,0 +1,2 @@
enable-microprofile.cli
2dd0552bcfc0007fd139808b3fc99fc741f17de1

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@ -0,0 +1,8 @@
LICENSE.txt
58853eb8199b5afe72a73a25fd8cf8c94285174b
README.txt
4e02800d2609dc82e957cfcf5eae12eeb1120349
copyright.txt
165a59628b050f2faa2477a2a62a92aa024ca1e3
jboss-modules.jar
ad33a0ca75c8189ebbfd8eedbb489a1453b96dea

View File

@ -0,0 +1,6 @@
asm-9.7.jar
073d7b3086e14beb604ced229c302feff6449723
asm-util-9.7.jar
c0655519f24d92af2202cb681cd7c1569df6ead6
module.xml
2fdd92766986752fe05bf9e499a5b89fbfed4b5a

View File

@ -0,0 +1,4 @@
hppc-0.8.1.jar
ffc7ba8f289428b9508ab484b8001dea944ae603
module.xml
7ff9b09d68ddb811bca586f99c7e4895fc11cc99

View File

@ -0,0 +1,4 @@
classmate-1.5.1.jar
3fe0bed568c62df5e89f4f174c101eab25345b6c
module.xml
2c2407d2d12a8b2cd853b25ca0592659cf961c23

View File

@ -0,0 +1,4 @@
jackson-annotations-2.17.0.jar
880a742337010da4c851f843d8cac150e22dff9f
module.xml
e4a8dbea35ecf4bdb108eefc4f7a5fefc2bd8ad0

View File

@ -0,0 +1,4 @@
jackson-core-2.17.0.jar
a6e5058ef9720623c517252d17162f845306ff3a
module.xml
68cdc0d521772e10b6bd0ed00f8e9d5d1a8fa58c

View File

@ -0,0 +1,4 @@
jackson-databind-2.17.0.jar
7173e9e1d4bc6d7ca03bc4eeedcd548b8b580b34
module.xml
afae2e2a7304f640bd4c77db1e7013cb48e6fbce

View File

@ -0,0 +1,4 @@
jackson-dataformat-yaml-2.17.0.jar
57a963c6258c49febc11390082d8503f71bb15a9
module.xml
43440d6ca1b0a4d926a413a73f59c4adae389922

View File

@ -0,0 +1,4 @@
jackson-datatype-jdk8-2.17.0.jar
95519a116d909faec29da76cf6b944b4a84c2c26
module.xml
4f672ef5b562b6de8be01280e228d87f3cdab036

View File

@ -0,0 +1,4 @@
jackson-datatype-jsr310-2.17.0.jar
3fab507bba9d477e52ed2302dc3ddbd23cbae339
module.xml
f3e236cbeb6c19474f74c1798dd38dbc66b7a4a6

View File

@ -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

View File

@ -0,0 +1,2 @@
module.xml
09257ddbe3ca160f3ccae184177749efb82a72ce

View File

@ -0,0 +1,4 @@
caffeine-3.1.8.jar
24795585df8afaf70a2cd534786904ea5889c047
module.xml
b5d5c01752d3997d117ec900faa1f167a9b1c285

View File

@ -0,0 +1,4 @@
btf-1.2.jar
9e66651022eb86301b348d57e6f59459effc343b
module.xml
25ed39b0eb6131b1b16f5ad02344a2c4a6bce80c

View File

@ -0,0 +1,4 @@
jackson-coreutils-1.8.jar
491a6e1130a180c153df9f2b7aabd7a700282c67
module.xml
edd282e5b4a283be2818d75ca8d9f12147a5f241

View File

@ -0,0 +1,4 @@
json-patch-1.9.jar
0a4c3c97a0f5965dec15795acf40d3fbc897af4b
module.xml
d617d8be6e99f49dbf75754fb3b4568b38237e6b

View File

@ -0,0 +1,4 @@
module.xml
55470daae6c253ebf9e114335255bd209e7a4c1e
msg-simple-1.1.jar
f261263e13dd4cfa93cc6b83f1f58f619097a2c4

View File

@ -0,0 +1,4 @@
module.xml
d614911cd191a7d479ffa4ac976e043eb963a02e
zstd-jni-1.5.6-5.jar
6b0abf5f2e68df5ffac02cba09fc2d84f7ebd631

View File

@ -0,0 +1,4 @@
gson-2.8.9.jar
8a432c1d6825781e21a02db2e2c33c5fde2833b9
module.xml
e9fece80c079e4436e45267429813501f4852b2d

View File

@ -0,0 +1,4 @@
failureaccess-1.0.2.jar
c4a06a64e650562f30b7bf9aaec1bfed43aca12b
module.xml
7d7ad1fd8299119798c90c5915e4993f56711804

View File

@ -0,0 +1,4 @@
guava-33.0.0-jre.jar
161ba27964a62f241533807a46b8711b13c1d94b
module.xml
5b9a85ab34e0fe19c3a3b31f4954ae40db2b600f

View File

@ -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

View File

@ -0,0 +1,4 @@
JavaEWAH-1.2.3.jar
13a27c856e0c8808cee9a64032c58eee11c3adc9
module.xml
cbc42f2e507993437ba185fdd71dd24fc070c32d

View File

@ -0,0 +1,4 @@
h2-2.2.224.jar
7bdade27d8cd197d9b5ce9dc251f41d2edc5f7ad
module.xml
8f9bda63c7655da79d2d54a359bcadc474b66143

View File

@ -0,0 +1,4 @@
asyncutil-0.1.0.jar
440941c382166029a299602e6c9ff5abde1b5143
module.xml
4a7d1e02072bbab6c935aa93fc915c26b7c5feae

View File

@ -0,0 +1,4 @@
azure-storage-8.6.6.jar
49d84b103a4700134ce56d73b4195f18fd226729
module.xml
0c37ed43810e653e80519241466019e2ee2d8f82

View File

@ -0,0 +1,4 @@
module.xml
0d381cde567d8896a94fe44993a10758ed433b49
nimbus-jose-jwt-9.37.3.jar
700f71ffefd60c16bd8ce711a956967ea9071cec

View File

@ -0,0 +1,4 @@
module.xml
29f238d58ebc8744f45165df41858bf9c0ca4782
protoparser-4.0.3.jar
e61ee0b108059d97f43143eb2ee7a1be8059a30e

View File

@ -0,0 +1,2 @@
module.xml
863fd9eefeafd585ab00c7c1d91a8b16c82f7ab9

Some files were not shown because too many files have changed in this diff Show More