Files
gerbeur/api/routes/users.ts
2026-03-16 16:52:53 +00:00

179 lines
4.3 KiB
TypeScript

import { Router } from "@oak/oak";
import {
APIErrorCode,
APIException,
isLoginUserRequest,
isRegisterUserRequest,
} from "../model/interfaces.ts";
import { createJWT, verifyJWT, verifyPassword } from "../lib/jwt.ts";
import { type AuthContext, authMiddleware } from "../middleware/auth.ts";
import {
createUser,
getUserById,
getUserByUsername,
} from "../services/user-service.ts";
import {
getDumpsByUser,
getVotedDumpsByUser,
} from "../services/dump-service.ts";
import { listPlaylistsByUser } from "../services/playlist-service.ts";
// Users router
const router = new Router({ prefix: "/api/users" });
// Register a new user
router.post("/register", async (ctx) => {
try {
const body = await ctx.request.body.json();
if (!isRegisterUserRequest(body)) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
400,
"Invalid request",
);
}
const user = await createUser(body);
const token = await createJWT({
userId: user.id,
username: user.username,
isAdmin: user.isAdmin,
});
ctx.response.status = 201;
ctx.response.body = {
success: true,
data: {
token,
user,
},
};
} catch (err) {
console.error(err);
throw new APIException(
APIErrorCode.SERVER_ERROR,
500,
"Failed to register user",
);
}
});
// Login
router.post("/login", async (ctx) => {
try {
const body = await ctx.request.body.json();
if (!isLoginUserRequest(body)) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
400,
"Invalid request",
);
}
const user = getUserByUsername(body.username);
const valid = await verifyPassword(body.password, user.passwordHash);
if (!valid) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
401,
"Invalid username or password",
);
}
const token = await createJWT({
userId: user.id,
username: user.username,
isAdmin: user.isAdmin,
});
ctx.response.body = {
success: true,
data: {
token,
user,
},
};
} catch (err) {
console.error(err);
throw new APIException(APIErrorCode.SERVER_ERROR, 500, "Failed to login");
}
});
// Get current user profile
router.get("/me", authMiddleware, (ctx: AuthContext) => {
try {
if (!ctx.state.user) {
throw new APIException(
APIErrorCode.UNAUTHORIZED,
401,
"Not authenticated",
);
}
const user = getUserById(ctx.state.user.userId);
ctx.response.body = {
success: true,
data: user,
};
} catch (err) {
console.error(err);
throw new APIException(
APIErrorCode.SERVER_ERROR,
500,
"Failed to fetch user profile",
);
}
});
// Public user profile by internal ID (used when only userId is available, e.g. dump.userId)
router.get("/by-id/:userId", (ctx) => {
const user = getUserById(ctx.params.userId);
const { passwordHash: _, ...publicUser } = user;
ctx.response.body = { success: true, data: publicUser };
});
// Playlists by user (optional auth: include private only if requester === owner)
router.get("/:username/playlists", async (ctx) => {
const user = getUserByUsername(ctx.params.username);
let requestingUserId: string | null = null;
const authHeader = ctx.request.headers.get("Authorization");
if (authHeader?.startsWith("Bearer ")) {
const payload = await verifyJWT(authHeader.substring(7));
if (payload) requestingUserId = payload.userId;
}
const playlists = listPlaylistsByUser(user.id, requestingUserId);
ctx.response.body = { success: true, data: playlists };
});
// Public user profile by username (no passwordHash)
router.get("/:username", (ctx) => {
const user = getUserByUsername(ctx.params.username);
const { passwordHash: _, ...publicUser } = user;
ctx.response.body = { success: true, data: publicUser };
});
// Dumps posted by user
router.get("/:username/dumps", (ctx) => {
const user = getUserByUsername(ctx.params.username);
const dumps = getDumpsByUser(user.id);
ctx.response.body = { success: true, data: dumps };
});
// Dumps upvoted by user
router.get("/:username/votes", (ctx) => {
const user = getUserByUsername(ctx.params.username);
const dumps = getVotedDumpsByUser(user.id);
ctx.response.body = { success: true, data: dumps };
});
export default router;