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:
Controllers
Services
Repositories
Entities
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”).
Package: org.uwgb.compsci330.server.serviceServices contain business logic and coordinate between controllers and repositories. UserService Handles user registration, authentication, and account operations. @ Service
public class UserService {
@ Autowired
private UserRepository userRepository ;
@ Autowired
private PasswordEncoder encoder ;
@ Transactional
public String register ( RegisterUserRequest userRequest ) {
// Validate username length
if (usernameLen < Configuration . MIN_USERNAME_LENGTH ) {
throw new UsernameTooShortException (username);
}
// Check for existing user
if ( userRepository . existsByUsername (username)) {
throw new UserAlreadyExistsException (username);
}
// Hash password and create user
User newUser = new User (
username,
encoder . encode ( userRequest . getPassword ())
);
userRepository . save (newUser);
// Generate JWT token
return JwtUtil . generateToken (
newUser . getId (),
newUser . getUsername ()
);
}
}
RelationshipService Manages friend relationships including creation, acceptance, and deletion. Key logic:
Validates users exist before creating relationships
Prevents self-friending
Handles pending request acceptance
Ensures unique relationships between users
Package: org.uwgb.compsci330.server.repositoryRepositories handle database operations using Spring Data JPA. UserRepository @ Repository
public interface UserRepository extends JpaRepository < User , String > {
boolean existsByUsername ( String username );
Optional < User > findByUsername ( String username );
Optional < User > findUserById ( String id );
}
RelationshipRepository Extends JpaRepository to provide relationship queries and management. Package: org.uwgb.compsci330.server.entityEntities represent database tables and domain models. See the Data Models section below for detailed entity schemas.
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:
Field Type Description idString (UUID) Unique identifier generated automatically usernameString User’s unique username (2-8 characters) passwordString BCrypt hashed password (never returned to client) statusint User 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:
Field Type Description idString (UUID) Unique relationship identifier requesterUser User who initiated the friend request requesteeUser User who received the friend request statusRelationshipStatus Current status: PENDING or ACCEPTED
Relationship lifecycle:
PENDING : Initial state when a friend request is sent
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.
User registration or login
When a user registers or logs in, the server:
Validates credentials
Generates a JWT token using JwtUtil.generateToken()
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)
Client stores token
The client stores the JWT token securely (e.g., in memory, secure storage).
Client sends token with requests
For protected endpoints, the client includes the token in the Authorization header: Authorization : Bearer eyJhbGciOiJIUzI1NiJ9...
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);
}
}
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:
Token signature using the JWT_SECRET
Token expiration timestamp
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 rootFor production deployments, consider migrating to PostgreSQL or MySQL: spring.datasource.url =jdbc:postgresql://localhost:5432/chatdb
spring.datasource.driver-class-name =org.postgresql.Driver
spring.jpa.database-platform =org.hibernate.dialect.PostgreSQLDialect
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:
Spring Boot
Database
JWT
Documentation
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