auth: update server authentication
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -137,3 +137,4 @@ dist
|
||||
# Vite logs files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
**/.helix
|
||||
|
||||
229
concord-server/src/routes/authRoutes.ts
Normal file
229
concord-server/src/routes/authRoutes.ts
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
57
concord-server/src/validators/authValidator.ts
Normal file
57
concord-server/src/validators/authValidator.ts
Normal file
@@ -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<typeof loginSchema>;
|
||||
export type ValidateTokenInput = z.infer<typeof validateTokenSchema>;
|
||||
export type RefreshTokenInput = z.infer<typeof refreshTokenSchema>;
|
||||
export type LogoutInput = z.infer<typeof logoutSchema>;
|
||||
export type AuthResponse = z.infer<typeof authResponseSchema>;
|
||||
export type ValidationResponse = z.infer<typeof validationResponseSchema>;
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user