A role-based desktop course-management application for schools, colleges and training institutes. Built with Java 21, Swing and MySQL, it provides three separate portals — Administrator, Instructor and Student — over a shared set of academic records.
Version 2.0 is a security-driven rewrite. Authentication is hash-based, every query is parameterised, portals are role-scoped, and credentials live outside the repository. See What changed in 2.0.
- What changed in 2.0
- Features
- Architecture
- Tech Stack
- Project Structure
- Quick Start
- Configuration
- Database
- Testing
- Security
- Deployment Checklist
- Upgrading from 1.x
- Roadmap
- Contributing
- License
The 1.x codebase was a functional coursework project with a set of vulnerabilities that made it unsafe for real student data. Each has been addressed:
| Area | 1.x | 2.0 |
|---|---|---|
| Login | Username and password concatenated into SQL — ' OR '1'='1 signed you in as administrator |
Parameterised lookup; the password never reaches the SQL at all |
| Queries | ~30 concatenated statements; a crafted delete value could wipe a table | Every statement is a PreparedStatement with bound parameters |
| Passwords | Stored and compared as plaintext | PBKDF2-HMAC-SHA256, 210 000 iterations, random 16-byte salt per password |
| Authorization | A successful login just opened a window; any portal could be constructed directly | Session + Role guards enforced in the portal base constructor |
| Data scoping | Students saw every student via SELECT * FROM student_info |
Results and modules are scoped to the signed-in user's own record |
| Credentials | root with an empty password hardcoded in source |
External config via file, system properties or environment variables |
| Default logins | Admin/Admin, Teacher/Teacher, student/student in source comments |
Nothing is seeded; the first administrator is created interactively |
| Password entry | JTextField — typed in cleartext on screen |
JPasswordField, held as char[] and zeroed after use |
| Brute force | Unlimited attempts | Configurable lockout (default 5 attempts / 15 minutes) |
| JDBC driver | Connector/J 8.0.27, affected by CVE-2022-21363 | Connector/J 9.7.0, resolved by Maven |
| Connections | New connection per action, never closed | HikariCP pool, try-with-resources everywhere |
| Transport | Unencrypted | sslMode=REQUIRED, public-key retrieval disabled |
| Errors | printStackTrace() leaking schema details |
Logged via SLF4J/Logback; users see plain messages |
| Layout | Fixed pixel setBounds, broken on high-DPI |
Layout managers that scale with the system font |
| Table limits | Fixed String[20][n] arrays, capped at 20 rows |
SimpleTableModel over an unbounded list, with sorting and search |
| Marks | Five subjects out of a hardcoded 500 | Unbounded subjects, per-subject maximums, configurable pass percentage |
| Transactions | Two independent writes; failure left orphaned rows | Exam and its marks written in one transaction |
Three functional bugs were fixed along the way: Add Module always failed (a positional INSERT supplied 4 values to a 5-column table), Add Instructor threw a NullPointerException after a successful insert, and marks entry silently discarded the semester field.
Full CRUD across students, instructors, courses, modules, enrollments and results. Optionally provisions portal logins for students and instructors, linking each account to its person record.
Sees the modules they teach and the students on their assigned course; records and reviews marks. Scoped to the instructor record linked to their account.
Sees their own details, the modules on their course, their instructors' contact details, and their own results with a per-subject breakdown, totals and pass/fail outcome.
Four layers, each depending only on the one beneath it:
┌─────────────────────────────────────────────────────────┐
│ ui/ Swing screens │
│ MainFrame → LoginDialog → PortalFrame │
│ Role guard enforced in the base ctor │
├─────────────────────────────────────────────────────────┤
│ security/ AuthService · PasswordHasher · Session │
│ util/ Validation │
├─────────────────────────────────────────────────────────┤
│ repository/ PreparedStatement data access │
│ model/ Immutable records │
├─────────────────────────────────────────────────────────┤
│ db/ Database (HikariCP pool + migration) │
│ config/ AppConfig (layered configuration) │
└─────────────────────────────────────────────────────────┘
│
MySQL 8.0 over TLS
AppContext wires the graph once at startup and hands collaborators to the UI. Screens never construct their own connection, which is precisely what let 1.x screens bypass authentication.
Authentication flow. LoginDialog collects a char[] password → AuthService looks the user up by (username, role) with bound parameters → verifies the PBKDF2 hash in Java → on success opens a Session; on failure increments a counter that locks the account after N attempts. Unknown usernames still perform a dummy hash comparison so response time does not reveal whether an account exists.
| Layer | Technology |
|---|---|
| Language | Java 21 (LTS) |
| GUI | Java Swing |
| Database | MySQL 8.0+ |
| Driver | MySQL Connector/J 9.7.0 |
| Pooling | HikariCP 7.1.0 |
| Logging | SLF4J 2.0.17 + Logback 1.5.18 |
| Testing | JUnit 5.11.4 |
| Build | Maven (wrapper included) |
No JARs are vendored in the repository; Maven resolves everything, so a driver upgrade is a one-line change in pom.xml.
Course-Management-System/
├── mvnw, mvnw.cmd, .mvn/ # Maven wrapper — no local Maven needed
├── pom.xml
└── src/
├── main/
│ ├── java/com/cms/
│ │ ├── CourseManagementApp.java # entry point
│ │ ├── AppContext.java # dependency wiring
│ │ ├── config/ # AppConfig, ConfigurationException
│ │ ├── db/ # Database (pool + migration)
│ │ ├── security/ # AuthService, PasswordHasher, Session, Role
│ │ ├── model/ # Student, Instructor, Course, CourseModule, ExamResult…
│ │ ├── repository/ # one per aggregate, all parameterised
│ │ ├── util/ # Validation
│ │ └── ui/
│ │ ├── MainFrame, LoginDialog, FirstRunDialog, PortalFrame
│ │ ├── TablePanel, SimpleTableModel, FormBuilder, Dialogs, UiTheme
│ │ ├── admin/ # AdminPortalFrame + entity form dialogs
│ │ ├── instructor/ # InstructorPortalFrame
│ │ └── student/ # StudentPortalFrame
│ └── resources/
│ ├── application.properties # defaults, no credentials
│ ├── logback.xml
│ └── db/schema.sql # applied automatically at startup
└── test/java/com/cms/ # unit tests + gated integration tests
- JDK 21+ —
java -version - MySQL 8.0+ reachable and running
Maven is not required; the bundled wrapper downloads it.
Do not point the application at root.
CREATE DATABASE msystem CHARACTER SET utf8mb4;
CREATE USER 'cms'@'localhost' IDENTIFIED BY 'a-strong-password-here';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, REFERENCES ON msystem.* TO 'cms'@'localhost';
FLUSH PRIVILEGES;CREATE, INDEX and REFERENCES are only needed for the automatic migration on first run. Revoke them afterwards, or set app.db.auto-migrate=false and apply schema.sql manually.
Create config/application.properties next to where you run the app — this path is gitignored:
app.db.url=jdbc:mysql://localhost:3306/msystem?sslMode=REQUIRED&allowPublicKeyRetrieval=false&serverTimezone=UTC
app.db.username=cms
app.db.password=a-strong-password-hereEnvironment variables work too, and take precedence:
export APP_DB_USERNAME=cms
export APP_DB_PASSWORD='a-strong-password-here'# Linux / macOS
./mvnw clean package
java -jar target/course-management-system.jar# Windows
.\mvnw.cmd clean package
java -jar target\course-management-system.jarThe build produces a self-contained runnable jar. Tables are created automatically on first start.
There are no default credentials. On an empty database, choosing Administrator opens a one-time setup dialog to create the first account (minimum 12 characters, stored only as a PBKDF2 hash). Further accounts are created from the admin portal, which can also issue logins to students and instructors.
A login only becomes useful once an administrator links it to a person record via Students → Edit or Instructors → Edit. Until then the portal explains that the account is unlinked.
Sources are layered; later ones win:
- packaged
application.propertiesdefaults ./config/application.properties, or the file named byCMS_CONFIG_FILE- JVM system properties —
-Dapp.db.password=… - environment variables —
APP_DB_PASSWORD(dots and dashes become underscores, uppercased)
| Key | Default | Purpose |
|---|---|---|
app.db.url |
jdbc:mysql://localhost:3306/msystem?sslMode=REQUIRED… |
JDBC URL |
app.db.username |
(required) | Database user |
app.db.password |
(empty) | Database password |
app.db.pool.max-size |
8 |
Maximum pooled connections |
app.db.auto-migrate |
true |
Apply schema.sql at startup |
app.security.max-failed-attempts |
5 |
Failures before lockout |
app.security.lockout-minutes |
15 |
Lockout duration |
app.security.pbkdf2-iterations |
210000 |
KDF cost; raising it re-hashes on next login |
app.exam.pass-percentage |
50 |
Pass threshold |
sslMode=REQUIRED encrypts the connection and works with MySQL's self-signed certificate. Use VERIFY_CA with a truststore against a real CA. allowPublicKeyRetrieval stays false deliberately — enabling it lets a MITM substitute an RSA key during caching_sha2_password authentication.
Eight tables with real foreign keys, unique constraints and check constraints — see schema.sql.
users ──┬── students ──┬── enrollments ── courses
│ └── exams ── exam_marks
└── instructors ── modules ── courses
Two structural changes over 1.x are worth calling out:
- Marks are normalised.
exam_marksholds one row per subject instead ofsubject1..subject5/marks1..marks5columns, so a semester can have any number of subjects, each with its own maximum. - Identity is by surrogate key. Deletes target
id, not a concatenated name. In 1.x, entering' OR '1'='1into a delete box emptied the table.
./mvnw test38 unit tests covering password hashing, validation and result arithmetic. They need no database.
A further 14 integration tests exercise the security properties against a live MySQL instance. They are skipped unless a database is supplied:
./mvnw test \
-Dapp.db.url="jdbc:mysql://127.0.0.1:3306/msystem_test?sslMode=REQUIRED" \
-Dapp.db.username=cms -Dapp.db.password=…These write to the configured schema. Point them at a throwaway database.
They assert that seven classic injection payloads never authenticate in either the username or password field, that roles cannot cross portals, that lockout fires on the configured attempt and not earlier, that payloads stored in data fields round-trip as literal text, and that a failed marks write leaves no partial rows.
Verified in this state: the full clean package build, all 52 tests against MySQL 8.0.40 over TLS, automatic schema creation, and both startup failure paths (missing configuration and unreachable database). The Swing screens themselves have not been exercised by a human on a real display — they compile, and their data access is covered by the tests, but the interaction has not been manually walked through.
- No secrets in the repository. The only credential ever committed was a local
root/empty-password dev default, now removed.config/application.propertiesand*.envare gitignored. - Injection. Every statement is parameterised. User input reaches the database only as a bound value.
- Passwords. PBKDF2-HMAC-SHA256, 210 000 iterations, per-password 16-byte salt, constant-time comparison. The iteration count is stored with each hash and upgraded transparently on the next successful login. Plaintext is held in
char[]and zeroed. - Authorization.
PortalFramecallsSession.requireRolebefore the window is built, so a portal cannot be opened without the matching role. - Least privilege. The app expects a dedicated database user, never
root. - Transport. TLS required; public-key retrieval disabled.
Found something? Open a private security advisory on the repository rather than a public issue.
- Dedicated database user,
rootnot used -
CREATE/INDEX/REFERENCESrevoked after first run, orauto-migratedisabled - Credentials supplied via environment or an out-of-tree file — never committed
-
sslMode=VERIFY_CAwith a proper certificate - First administrator created with a strong, unique password
- Every login linked to a student or instructor record
-
logs/rotated and access-controlled — it records usernames and login outcomes - Database backups scheduled and a restore rehearsed
The 2.0 schema is not backward compatible and 1.x data does not migrate automatically.
1.x stored passwords in plaintext and joined records by free-text name; 2.0 uses hashed credentials and surrogate keys. There is no lossless automated path, and rehashing plaintext passwords would preserve credentials that should be considered compromised.
Recommended path:
- Stand up 2.0 against a fresh
msystemdatabase. - Export the 1.x tables and import the reference data — courses, students, instructors — mapping names to the new foreign keys.
- Issue new credentials to everyone. Treat every 1.x password as compromised: they were stored in plaintext and the login screen was bypassable.
The 1.x sources remain in git history at tag/commit b2b31e1 if you need them for reference.
- Self-service password change and administrator-initiated reset
- Flyway or Liquibase for versioned migrations
- Audit log of privileged actions
- CSV/PDF export of results and transcripts
- Attendance tracking
- Coverage reporting in CI
- Fork and branch —
git checkout -b feature/your-feature - Keep
./mvnw testgreen and add tests for new behaviour - All data access goes through a repository using
PreparedStatement; never concatenate user input into SQL - Commit, push, and open a Pull Request
No license file is included, so all rights are reserved by default. Others cannot legally reuse this code. To make it open source, add a LICENSE file.
- MySQL Connector/J — JDBC driver
- HikariCP — connection pooling
- Logback — logging