Skip to main content

Overview

The Chat Server API is built with Spring Boot 4.0.2 and follows a layered architecture pattern with clear separation of concerns. The application uses JWT-based authentication, SQLite database for persistence, and follows RESTful API design principles.

Architecture layers

The application is organized into the following layers:
Package: org.uwgb.compsci330.server.controllerControllers handle HTTP requests and responses, delegating business logic to services.

UserController

Located at /users, handles user authentication and account management.
@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;
    
    @PostMapping("/register")
    public String register(@RequestBody RegisterUserRequest req) {
        return userService.register(req);
    }
    
    @PostMapping("/login")
    public String login(@RequestBody LoginUserRequest req) {
        return userService.login(req);
    }
    
    @GetMapping("/@me")
    public SafeUser getMe(@RequestHeader("Authorization") String auth) {
        return userService.getMe(auth);
    }
}

RelationshipController

Located at /users/@me/relationships, manages friend relationships.
@RestController
@RequestMapping("/users/@me/relationships")
public class RelationshipController {
    @Autowired
    private RelationshipService relationshipService;
    
    @GetMapping
    public List<SafeRelationship> getRelationships(Authentication auth) {
        return relationshipService.getRelationships(auth.getName());
    }
    
    @PostMapping("/{username}")
    public SafeRelationship addRelationship(
        @PathVariable String username, 
        Authentication auth
    ) {
        return relationshipService.createRelationship(
            auth.getName(), 
            username
        );
    }
}

BaseAPIController

Provides basic health check and testing endpoints.
@RestController
public class BaseAPIController {
    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
        return String.format("Hello %s!", name);
    }
}
The /hello endpoint is a simple test endpoint that returns a greeting. It accepts an optional name query parameter (defaults to “World”).

Data models

The application uses three primary entities to model the domain:

User entity

Represents a user account in the system.
@Entity(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    private String username;
    
    private String password;  // BCrypt hashed
    
    private int status;
    
    // Relationships
    @OneToMany(mappedBy = "requester", cascade = CascadeType.ALL)
    private Set<Relationship> sentRequests = new HashSet<>();
    
    @OneToMany(mappedBy = "requestee", cascade = CascadeType.ALL)
    private Set<Relationship> receivedRequests = new HashSet<>();
}
Fields:
FieldTypeDescription
idString (UUID)Unique identifier generated automatically
usernameStringUser’s unique username (2-8 characters)
passwordStringBCrypt hashed password (never returned to client)
statusintUser status code (default: 0)
sentRequestsSet<Relationship>Friend requests sent by this user
receivedRequestsSet<Relationship>Friend requests received by this user
Passwords are hashed using BCryptPasswordEncoder with a default strength of 10 rounds.

SafeUser DTO

The API never returns the User entity directly. Instead, it uses SafeUser to hide sensitive information:
public class SafeUser {
    private final String id;
    private final String username;
    private final int status;
    
    public SafeUser(User user) {
        this.id = user.getId();
        this.username = user.getUsername();
        this.status = user.getStatus();
    }
}

Relationship entity

Represents a friendship or friend request between two users.
@Entity(name = "relationships")
@Table(
    name = "relationships",
    uniqueConstraints = {
        @UniqueConstraint(columnNames = {"requester", "requestee"})
    }
)
public class Relationship {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "requester")
    private User requester;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "requestee")
    private User requestee;
    
    @Enumerated(EnumType.STRING)
    private RelationshipStatus status;
}
Fields:
FieldTypeDescription
idString (UUID)Unique relationship identifier
requesterUserUser who initiated the friend request
requesteeUserUser who received the friend request
statusRelationshipStatusCurrent status: PENDING or ACCEPTED
Relationship lifecycle:
  1. PENDING: Initial state when a friend request is sent
  2. ACCEPTED: State after the requestee accepts the request
The unique constraint on (requester, requestee) ensures no duplicate relationships exist between the same pair of users.

SafeRelationship DTO

Returned by the API to provide relationship information:
public class SafeRelationship {
    private SafeUser requester;
    private SafeUser requestee;
    private RelationshipStatus status;
    
    public SafeRelationship(Relationship relationship) {
        this.requester = new SafeUser(relationship.getRequester());
        this.requestee = new SafeUser(relationship.getRequestee());
        this.status = relationship.getStatus();
    }
}

Conversation entity

The Conversation entity is currently not implemented but is planned for future releases.

Authentication flow

The application uses JWT (JSON Web Token) authentication with Spring Security.
1

User registration or login

When a user registers or logs in, the server:
  1. Validates credentials
  2. Generates a JWT token using JwtUtil.generateToken()
  3. Returns the token to the client
public static String generateToken(String id, String username) {
    Key key = Keys.hmacShaKeyFor(Configuration.JWT_SECRET.getBytes());
    
    return Jwts.builder()
        .setSubject(id)
        .claim("userId", id)
        .claim("username", username)
        .setIssuedAt(new Date())
        .setExpiration(new Date(
            System.currentTimeMillis() + Configuration.JWT_EXPIRATION_MS
        ))
        .signWith(key)
        .compact();
}
Token contains:
  • sub: User ID (subject)
  • userId: User ID (claim)
  • username: Username (claim)
  • iat: Issued at timestamp
  • exp: Expiration timestamp (30 days from issue)
2

Client stores token

The client stores the JWT token securely (e.g., in memory, secure storage).
3

Client sends token with requests

For protected endpoints, the client includes the token in the Authorization header:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
4

Server validates token

The JwtAuthenticationFilter intercepts all requests and validates the JWT:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");
        
        try {
            if (authHeader != null && !authHeader.isBlank()) {
                String userId = JwtUtil.getUserIdFromToken(authHeader);
                
                SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(
                        userId, 
                        null, 
                        List.of(new SimpleGrantedAuthority("ROLE_USER"))
                    )
                );
            }
        } catch (Exception e) {
            logger.warn("Continuing without authentication: " + e.getMessage());
        }
        
        filterChain.doFilter(request, response);
    }
}
5

Authorization decision

Spring Security checks if the authenticated user has access to the requested endpoint:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/users/register").permitAll()
        .requestMatchers("/users/login").permitAll()
        .requestMatchers("/v3/*").permitAll()
        .anyRequest().authenticated()
    );
    
    return http.build();
}
Public endpoints:
  • /users/register
  • /users/login
  • /v3/* (OpenAPI documentation)
Protected endpoints:
  • All other endpoints require authentication

Token validation

The JwtUtil.validateToken() method verifies:
  1. Token signature using the JWT_SECRET
  2. Token expiration timestamp
  3. Token structure and claims
public static Claims validateToken(String token) {
    return Jwts.parserBuilder()
        .setSigningKey(Configuration.JWT_SECRET.getBytes())
        .build()
        .parseClaimsJws(token)
        .getBody();
}
If token validation fails, the request continues without authentication. Protected endpoints will return 401 Unauthorized.

Configuration

The application is configured through multiple files:

Configuration.java

Central configuration constants:
public class Configuration {
    public static final String SERVER_VERSION = "0.0.1";
    
    // Validation constraints
    public static final int MIN_PASSWORD_LENGTH = 6;
    public static final int MIN_USERNAME_LENGTH = 2;
    public static final int MAX_USERNAME_LENGTH = 8;
    
    // JWT configuration
    public static final String JWT_SECRET = 
        System.getenv("JWT_SECRET") == null 
            ? "<default-base64-secret>" 
            : System.getenv("JWT_SECRET");
    
    public static final long JWT_EXPIRATION_MS = 1000 * 60 * 60 * 30; // 30 days
}

application.properties

Database and Spring Boot configuration:
spring.application.name=server

# SQLite Database
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
spring.datasource.url=jdbc:sqlite:./main.db
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.jpa.hibernate.ddl-auto=update

# Connection pool optimization
spring.datasource.hikari.connection-init-sql=\
  PRAGMA journal_mode=WAL;\
  PRAGMA synchronous=NORMAL;\
  PRAGMA cache_size=-10000;\
  PRAGMA temp_store=MEMORY;
The server uses SQLite with WAL (Write-Ahead Logging) mode for better concurrency.Benefits:
  • Zero configuration
  • Portable database file
  • Sufficient for development and small deployments
Database location: ./main.db in the project root

Security features

Password hashing

Passwords are hashed using BCrypt:
@Bean
public PasswordEncoder encoder() {
    return new BCryptPasswordEncoder();
}
BCrypt automatically:
  • Generates unique salts for each password
  • Uses 2^10 iterations by default
  • Produces 60-character hash strings

CSRF protection

CSRF is disabled for the REST API since it uses JWT authentication:
http.csrf(AbstractHttpConfigurer::disable)

Stateless sessions

The API does not use server-side sessions:
http.sessionManagement(session -> 
    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)

Exception handling

The application uses custom exceptions with global and controller-specific handlers:

Custom exceptions

Package: org.uwgb.compsci330.server.exception
  • UserAlreadyExistsException (409 Conflict)
  • UsernameOrPasswordIncorrectException (400 Bad Request)
  • UnauthorizedException (401 Unauthorized)
  • UsernameTooShortException (400 Bad Request)
  • UsernameTooLongException (400 Bad Request)
  • PasswordTooShortException (400 Bad Request)
  • InvalidFriendRequestException (400 Bad Request)
  • SelfFriendException (400 Bad Request)
  • ExistingRelationshipException (400 Bad Request)
  • RelationshipDoesNotExistException (400 Bad Request)

Exception handlers

Controllers handle exceptions with @ExceptionHandler:
@ExceptionHandler(value = UserAlreadyExistsException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ErrorResponse handleUserAlreadyExistsException(
    UserAlreadyExistsException ex
) {
    return new ErrorResponse(ex.getMessage());
}
ErrorResponse format:
{
  "message": "User with username 'alice' already exists"
}

Dependencies

Key dependencies from build.gradle.kts:
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.boot:spring-boot-starter-validation")

Next steps

Authentication API

Explore user authentication endpoints

Error Handling

Learn how to handle API errors

Relationships API

Manage user relationships and friend requests

Server Setup

Install and configure the server

Build docs developers (and LLMs) love