Skip to main content

Overview

The EMS implements role-based access control (RBAC) using JPA’s Single Table Inheritance strategy. All users inherit from a base User entity, with role-specific attributes stored in the same table using a discriminator column.

Student

Browse and register for events

Event Organizer

Create proposals and manage events

Administrator

Approve proposals and manage the system

Single Table Inheritance

Architecture

All user types are stored in a single users table with a user_type discriminator column:
User.java:15-58
@Entity
@Table(name = "users")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "user_type", discriminatorType = DiscriminatorType.STRING)
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public abstract class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long userID;

    @Column(unique = true, nullable = false)
    private String email;

    @JsonIgnore
    @Column(name = "password_hash", nullable = false)
    private String passwordHash;

    @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name", nullable = false)
    private String lastName;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserRole role;

    @Enumerated(EnumType.STRING)
    @Column(name = "account_status")
    @Builder.Default
    private AccountStatus accountStatus = AccountStatus.ACTIVE;

    @Column(name = "created_at")
    @Builder.Default
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(name = "last_login")
    private LocalDateTime lastLogin;
}
The User class is abstract and cannot be instantiated directly. All users must be created as one of the three concrete subclasses.

Benefits of Single Table Inheritance

  • Single table queries are faster (no joins required)
  • Polymorphic queries are efficient
  • Simplified database schema
  • Easy to add new user types
  • Supports polymorphic associations
  • Type-safe casting with instanceof checks
  • One table to manage
  • Straightforward migrations
  • Easy to understand data model

User Roles

Student

Students can browse events and register for activities.
Student.java:12-28
@Entity
@DiscriminatorValue("STUDENT")
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class Student extends User {

    @Column(name = "student_id", unique = true)
    private String studentID; // e.g., "S2024001"

    private String major;

    @Column(name = "year_of_study")
    private Integer yearOfStudy;
}
studentID
string
Unique student identifier (e.g., “S2024001”)
major
string
Student’s major or field of study
yearOfStudy
integer
Current year of study (1-4)

Permissions

Browse Events

View all approved and published events

Register for Events

Register for events with capacity and conflict checking

View Registrations

View personal registration history

Cancel Registrations

Cancel registrations with 24-hour notice

Event Organizer

Organizers can create event proposals and manage approved events.
EventOrganizer.java:17-37
@Entity
@DiscriminatorValue("ORGANIZER")
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class EventOrganizer extends User {

    @Column(name = "organization_name")
    private String organizationName;

    @Column(name = "department_affiliation")
    private String departmentAffiliation;

    @OneToMany(mappedBy = "organizer")
    @JsonIgnore
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    private List<Proposal> myProposals;
}
organizationName
string
Name of the organization (e.g., “Computer Science Club”)
departmentAffiliation
string
Department affiliation (e.g., “Department of Computer Science”)
myProposals
List<Proposal>
Lazy-loaded list of proposals created by this organizer
The myProposals field is marked with @JsonIgnore to prevent circular JSON references when serializing organizers and their proposals.

Permissions

Create Proposals

Submit event proposals with document uploads

View Own Proposals

View all proposals they’ve created

Manage Events

Manage approved events (view participants, post updates)

Resubmit Proposals

Resubmit rejected proposals with changes

Administrator

Administrators approve proposals and manage the system.
Administrator.java:11-23
@Entity
@DiscriminatorValue("ADMIN")
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class Administrator extends User {

    @Enumerated(EnumType.STRING)
    @Column(name = "admin_level")
    private AdminLevel adminLevel;
}
adminLevel
enum
One of: STANDARD_ADMIN, SUPER_ADMIN

Admin Levels

Permissions:
  • Review and approve/reject proposals
  • View department-specific proposals
  • Monitor system activity
  • Generate reports
Limitations:
  • Cannot create SUPER_ADMIN users
  • Cannot modify system-wide settings
Public registration always creates STANDARD_ADMIN users. SUPER_ADMIN can only be created through the admin user management endpoint by an existing SUPER_ADMIN.

Permission Enforcement

Method-Level Security

Permissions are enforced using Spring Security’s @PreAuthorize annotation:
ProposalController.java:31-46
@GetMapping
@PreAuthorize("hasRole('ORGANIZER')")
public ResponseEntity<Response<List<ProposalDTO>>> getMyProposals(
        @AuthenticationPrincipal CustomUserDetails userDetails) {

    Long organizerID = userDetails.getUser().getUserID();
    List<ProposalDTO> proposals = proposalService.getProposalsByOrganizer(organizerID);

    Response<List<ProposalDTO>> response = Response.<List<ProposalDTO>>builder()
            .statusCode(HttpStatus.OK.value())
            .message("Proposals fetched successfully")
            .data(proposals)
            .build();

    return ResponseEntity.ok(response);
}
The @PreAuthorize annotation is evaluated before the method executes. If the user doesn’t have the required role, a 403 Forbidden response is returned.

Multiple Roles

Some endpoints allow multiple roles:
RegistrationController.java:65-76
@GetMapping("/event/{eventID}/participants")
@PreAuthorize("hasAnyRole('ADMIN','ORGANIZER')")
public ResponseEntity<Response<List<Map<String, String>>>> getParticipants(
        @PathVariable Long eventID,
        @AuthenticationPrincipal CustomUserDetails userDetails) {

    List<Map<String, String>> participants =
            registrationService.getParticipantDetails(eventID, userDetails.getUser());

    return ResponseEntity.ok(new Response<>(200, "Participants fetched", participants));
}

Runtime Permission Checks

Some operations require additional runtime checks beyond role verification:
RegistrationServiceImpl.java:185-194
@Override
@Transactional(readOnly = true)
public List<Map<String, String>> getParticipantDetails(Long eventID, User requester) {
    Event event = eventRepository.findById(eventID)
            .orElseThrow(() -> new NotFoundException("Event not found with ID: " + eventID));

    if (requester.getRole() == UserRole.ORGANIZER) {
        Long organizerId = event.getOrganizer() != null ? event.getOrganizer().getUserID() : null;
        if (organizerId == null || !organizerId.equals(requester.getUserID())) {
            throw new IllegalStateException("Unauthorized to view participants for this event.");
        }
    }
    // ... continue with logic
}
Organizers can only view participants for events they manage. Admins can view participants for any event.

Type Casting

Safe Casting with instanceof

When working with polymorphic entities, use instanceof checks before casting:
RegistrationServiceImpl.java:52-57
User user = userRepository.findById(studentID)
        .orElseThrow(() -> new NotFoundException("Student not found with ID: " + studentID));
if (!(user instanceof Student)) {
    throw new IllegalStateException("User is not a student.");
}
Student student = (Student) user;

Pattern Matching (Java 17+)

Modern Java supports pattern matching for instanceof:
AuthServiceImpl.java:119-128
if (user instanceof Administrator admin) {
    if (admin.getDepartment() != null) {
        adminDepartment = admin.getDepartment().name();
    }
    if (admin.getAdminLevel() != null) {
        adminLevel = admin.getAdminLevel().name();
    }
}

Database Schema

users Table Structure

user_id
BIGINT
Primary key, auto-increment
user_type
VARCHAR
Discriminator column: “STUDENT”, “ORGANIZER”, or “ADMIN”
email
VARCHAR
Unique, not null
password_hash
VARCHAR
BCrypt hashed password, not null
first_name
VARCHAR
Not null
last_name
VARCHAR
Not null
role
VARCHAR
Enum: “STUDENT”, “ORGANIZER”, “ADMIN”
account_status
VARCHAR
Enum: “ACTIVE”, “SUSPENDED”, “DELETED”
created_at
TIMESTAMP
Account creation timestamp
last_login
TIMESTAMP
Last login timestamp (nullable)
student_id
VARCHAR
Student-specific: unique student identifier (nullable)
major
VARCHAR
Student-specific: major or field of study (nullable)
year_of_study
INTEGER
Student-specific: current year (nullable)
organization_name
VARCHAR
Organizer-specific: organization name (nullable)
department_affiliation
VARCHAR
Organizer-specific: department affiliation (nullable)
admin_level
VARCHAR
Admin-specific: “STANDARD_ADMIN” or “SUPER_ADMIN” (nullable)
Role-specific columns will be NULL for users of other types. Ensure your queries handle NULL values appropriately.

Common Patterns

Retrieving Current User

@GetMapping("/profile")
public ResponseEntity<Response<UserDTO>> getProfile(
        @AuthenticationPrincipal CustomUserDetails userDetails) {
    
    User currentUser = userDetails.getUser();
    // Process based on role
    if (currentUser instanceof Student student) {
        // Handle student-specific logic
    } else if (currentUser instanceof EventOrganizer organizer) {
        // Handle organizer-specific logic
    } else if (currentUser instanceof Administrator admin) {
        // Handle admin-specific logic
    }
}

Role-Specific Queries

// Find all students
List<Student> students = userRepository.findAll().stream()
    .filter(user -> user instanceof Student)
    .map(user -> (Student) user)
    .toList();

// Find organizer by ID
EventOrganizer organizer = (EventOrganizer) userRepository.findById(id)
    .filter(user -> user instanceof EventOrganizer)
    .orElseThrow(() -> new NotFoundException("Organizer not found"));

Security Best Practices

1

Always Verify Ownership

Even if a user has the correct role, verify they own the resource they’re trying to access.
2

Use Type-Safe Casting

Always use instanceof checks before casting to concrete user types.
3

Protect Admin Endpoints

Use @PreAuthorize("hasRole('ADMIN')") for sensitive operations.
4

Validate User Status

Check accountStatus to ensure the user is ACTIVE before processing requests.

Authentication

Learn about JWT-based authentication

Proposals

Understand the proposal workflow

Build docs developers (and LLMs) love