diff --git a/.gitignore b/.gitignore index 9a5aced..aade9cb 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,4 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* +**/.helix diff --git a/concord-server/src/routes/authRoutes.ts b/concord-server/src/routes/authRoutes.ts new file mode 100644 index 0000000..6e21fcf --- /dev/null +++ b/concord-server/src/routes/authRoutes.ts @@ -0,0 +1,229 @@ +import { Hono } from "hono"; +import { zValidator } from "@hono/zod-validator"; +import { describeRoute, resolver } from "hono-openapi"; +import { + getUserCredentials, + getUserInformation, +} from "../services/userService"; +import shaHash from "../helper/hashing"; +import { PrismaClient } from "@prisma/client"; +import { + loginSchema, + validateTokenSchema, + refreshTokenSchema, + logoutSchema, + authResponseSchema, + validationResponseSchema, + errorResponseSchema, + successResponseSchema, +} from "../validators/authValidator"; + +const prisma = new PrismaClient(); +const authRoutes = new Hono(); + +// Login endpoint +authRoutes.post( + "/login", + describeRoute({ + description: "User login", + responses: { + 200: { + description: "Login successful", + content: { + "application/json": { schema: resolver(authResponseSchema) }, + }, + }, + 401: { + description: "Invalid credentials", + content: { + "application/json": { schema: resolver(errorResponseSchema) }, + }, + }, + }, + }), + zValidator("json", loginSchema), + async (c) => { + try { + const { username, password } = await c.req.json(); + + // Find user by username + const user = await prisma.user.findFirst({ + where: { username: username }, + }); + + if (!user) { + return c.json({ error: "Invalid username or password" }, 401); + } + + // Get user credentials + const userCredentials = await getUserCredentials(user.id); + if (!userCredentials) { + return c.json({ error: "Invalid username or password" }, 401); + } + + // Hash + // const hashedPassword = shaHash(password, user.id); + + // Verify password + if (password !== userCredentials.password) { + return c.json({ error: "Invalid username or password" }, 401); + } + + // Generate new token + const token = crypto.randomUUID(); + + // Update user's token in database + await prisma.userAuth.update({ + where: { userId: user.id }, + data: { token: token }, + }); + + // Get full user information + const userInfo = await getUserInformation(user.id); + if (!userInfo) { + return c.json({ error: "Failed to get user information" }, 500); + } + + return c.json({ + user: userInfo, + token: token, + }); + } catch (error) { + console.error("Login error:", error); + return c.json({ error: "Internal server error" }, 500); + } + }, +); + +// Token validation endpoint +authRoutes.post( + "/validate", + describeRoute({ + description: "Validate user token", + responses: { + 200: { + description: "Token validation result", + content: { + "application/json": { schema: resolver(validationResponseSchema) }, + }, + }, + }, + }), + zValidator("json", validateTokenSchema), + async (c) => { + try { + const { token, userId } = await c.req.json(); + + // Get user credentials + const userCredentials = await getUserCredentials(userId); + if (!userCredentials || userCredentials.token !== token) { + return c.json({ valid: false }); + } + + // Get user information + const userInfo = await getUserInformation(userId); + if (!userInfo) { + return c.json({ valid: false }); + } + + return c.json({ + valid: true, + user: userInfo, + }); + } catch (error) { + return c.json({ valid: false }); + } + }, +); + +// Token refresh endpoint +authRoutes.post( + "/refresh", + describeRoute({ + description: "Refresh user token", + responses: { + 200: { + description: "Token refreshed successfully", + content: { + "application/json": { schema: resolver(authResponseSchema) }, + }, + }, + 401: { + description: "Invalid token", + content: { + "application/json": { schema: resolver(errorResponseSchema) }, + }, + }, + }, + }), + zValidator("json", refreshTokenSchema), + async (c) => { + try { + const { userId, oldToken } = await c.req.json(); + + // Verify old token + const userCredentials = await getUserCredentials(userId); + if (!userCredentials || userCredentials.token !== oldToken) { + return c.json({ error: "Invalid token" }, 401); + } + + // Generate new token + const newToken = crypto.randomUUID(); + + // Update token in database + await prisma.userAuth.update({ + where: { userId: userId }, + data: { token: newToken }, + }); + + // Get user information + const userInfo = await getUserInformation(userId); + if (!userInfo) { + return c.json({ error: "Failed to get user information" }, 500); + } + + return c.json({ + user: userInfo, + token: newToken, + }); + } catch (error) { + console.error("Token refresh error:", error); + return c.json({ error: "Internal server error" }, 500); + } + }, +); + +// Logout endpoint (invalidate token) +authRoutes.post( + "/logout", + describeRoute({ + description: "User logout", + responses: { + 200: { + description: "Logout successful", + content: { + "application/json": { schema: resolver(successResponseSchema) }, + }, + }, + }, + }), + zValidator("json", logoutSchema), + async (c) => { + try { + const { userId } = await c.req.json(); + + // Clear token in database + await prisma.userAuth.update({ + where: { userId: userId }, + data: { token: null }, + }); + + return c.json({ success: true }); + } catch (error) { + console.error("Logout error:", error); + return c.json({ error: "Internal server error" }, 500); + } + }, +); + +export default authRoutes; diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts index 84ef367..53b4ddd 100644 --- a/concord-server/src/routes/index.ts +++ b/concord-server/src/routes/index.ts @@ -5,6 +5,7 @@ import messageRoutes from "./messageRoutes"; import { channelRoutes } from "./channelRoutes"; import instanceRoutes from "./instanceRoutes"; import { categoryRoutes } from "./categoryRoutes"; +import authRoutes from "./authRoutes"; const routes = new Hono(); @@ -13,5 +14,6 @@ routes.route("/message", messageRoutes); routes.route("/channel", channelRoutes); routes.route("/instance", instanceRoutes); routes.route("/category", categoryRoutes); +routes.route("/auth", authRoutes); export default routes; diff --git a/concord-server/src/validators/authValidator.ts b/concord-server/src/validators/authValidator.ts new file mode 100644 index 0000000..cb618e1 --- /dev/null +++ b/concord-server/src/validators/authValidator.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; + +export const loginSchema = z.object({ + username: z.string().min(3).max(30), + password: z.string().min(1), +}); + +export const validateTokenSchema = z.object({ + token: z.string(), + userId: z.uuidv7(), +}); + +export const refreshTokenSchema = z.object({ + userId: z.uuidv7(), + oldToken: z.string(), +}); + +export const logoutSchema = z.object({ + userId: z.uuidv7(), +}); + +// Response schemas for OpenAPI documentation +export const authResponseSchema = z.object({ + user: z.object({ + id: z.string(), + userName: z.string(), + nickName: z.string().nullable(), + bio: z.string().nullable(), + picture: z.string().nullable(), + banner: z.string().nullable(), + admin: z.boolean(), + status: z.enum(["online", "offline", "dnd", "idle", "invis"]), + role: z.array(z.any()), + }), + token: z.string(), +}); + +export const validationResponseSchema = z.object({ + valid: z.boolean(), + user: z.any().optional(), +}); + +export const errorResponseSchema = z.object({ + error: z.string(), +}); + +export const successResponseSchema = z.object({ + success: z.boolean(), +}); + +// Type exports +export type LoginInput = z.infer; +export type ValidateTokenInput = z.infer; +export type RefreshTokenInput = z.infer; +export type LogoutInput = z.infer; +export type AuthResponse = z.infer; +export type ValidationResponse = z.infer; diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts index e4f1632..9b9bfb6 100644 --- a/concord-server/src/validators/userValidator.ts +++ b/concord-server/src/validators/userValidator.ts @@ -7,7 +7,7 @@ export const queryUserByIdSchema = z.object({ export const queryAllUsersByInstanceId = z.object({ instanceId: z.uuidv7(), }); -import { is } from "zod/v4/locales"; + export const createUserSchema = z.object({ username: z.string().min(3).max(30), nickname: z.string().min(1).max(30).optional(),