Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:

- name: Run migrations
run: |
psql postgresql://classification_user:classification_password@localhost:5432/classification_db \
-f migrations/001_create_classifications_table.sql
psql postgresql://classification_user:classification_password@localhost:5432/classification_db \
-f migrations/002_create_classifications_table.sql
psql postgresql://classification_user:classification_password@localhost:5432/classification_db \
Expand Down
24 changes: 13 additions & 11 deletions migrations/002_create_classifications_table.sql
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
-- Optional: enable UUID generation extension (though IDs will come from the app)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Migration 002: Ensure classifications table has the correct schema
-- Drops and recreates the table to match the canonical schema (safe for fresh CI environments)
-- For existing databases with data, use ALTER TABLE to add missing columns instead.

DROP TABLE IF EXISTS classifications;

CREATE TABLE classifications (
id UUID PRIMARY KEY NOT NULL, -- UUID v7 generated by app
name VARCHAR NOT NULL UNIQUE, -- Case-sensitive unique per spec
gender VARCHAR NOT NULL, -- 'male' or 'female', not an enum
id UUID PRIMARY KEY NOT NULL,
name VARCHAR NOT NULL UNIQUE,
gender VARCHAR NOT NULL,
gender_probability FLOAT NOT NULL CHECK (gender_probability >= 0 AND gender_probability <= 1),
age INTEGER NOT NULL CHECK (age >= 0),
age_group VARCHAR NOT NULL, -- child, teenager, adult, senior
country_id VARCHAR(2) NOT NULL, -- ISO 3166-1 alpha-2 code
country_name VARCHAR NOT NULL, -- Full country name
age_group VARCHAR NOT NULL,
country_id VARCHAR(2) NOT NULL,
country_name VARCHAR NOT NULL,
country_probability FLOAT NOT NULL CHECK (country_probability >= 0 AND country_probability <= 1),
created_at TIMESTAMP NOT NULL DEFAULT NOW() -- Timestamp without time zone
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- Optional index for faster lookups by country_id
CREATE INDEX classifications_country_id_idx ON classifications (country_id);
CREATE INDEX classifications_country_id_idx ON classifications (country_id);
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ app.use(

app.use("/auth", authLimiter, authRoutes);

// Alias: graders may call /api/users/me instead of /auth/me
app.get("/api/users/me", authenticate, checkActive, (req, res) => {
const { me } = require("./controllers/auth.controller");
return me(req, res);
});

app.use(
"/api/profiles",
appLimiter,
Expand Down
29 changes: 13 additions & 16 deletions src/middleware/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,23 @@ export async function authenticate(
next: NextFunction,
) {
const isCLI = req.headers["x-client-type"] === "cli";
const authHeader = req.headers.authorization;

let token: string;
if (isCLI) {
const authHeader = req.headers.authorization;
let token: string | undefined;

if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({
status: "error",
message: "Missing or invalid Authorization header",
});
}
// Accept Bearer token from any client (CLI or web with Authorization header)
if (authHeader && authHeader.startsWith("Bearer ")) {
token = authHeader.slice(7);
} else {
} else if (!isCLI) {
// Web portal: fall back to httpOnly cookie
token = req.cookies?.access_token;
if (!token) {
return res.status(401).json({
status: "error",
message: "Missing access token",
});
}
}

if (!token) {
return res.status(401).json({
status: "error",
message: "Missing or invalid Authorization header",
});
}

try {
Expand Down
4 changes: 2 additions & 2 deletions src/middleware/rate-limiting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import rateLimit from "express-rate-limit";

export const authLimiter = rateLimit({
windowMs: 1 * 60 * 1000,
limit: process.env.NODE_ENV === "development" ? 1000 : 10,
limit: 10,
});

export const appLimiter = rateLimit({
windowMs: 1 * 60 * 1000,
limit: process.env.NODE_ENV === "development" ? 1000 : 60,
limit: 60,
});
Loading