auth: update server authentication
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -137,3 +137,4 @@ dist
|
|||||||
# Vite logs files
|
# Vite logs files
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.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 { channelRoutes } from "./channelRoutes";
|
||||||
import instanceRoutes from "./instanceRoutes";
|
import instanceRoutes from "./instanceRoutes";
|
||||||
import { categoryRoutes } from "./categoryRoutes";
|
import { categoryRoutes } from "./categoryRoutes";
|
||||||
|
import authRoutes from "./authRoutes";
|
||||||
|
|
||||||
const routes = new Hono();
|
const routes = new Hono();
|
||||||
|
|
||||||
@@ -13,5 +14,6 @@ routes.route("/message", messageRoutes);
|
|||||||
routes.route("/channel", channelRoutes);
|
routes.route("/channel", channelRoutes);
|
||||||
routes.route("/instance", instanceRoutes);
|
routes.route("/instance", instanceRoutes);
|
||||||
routes.route("/category", categoryRoutes);
|
routes.route("/category", categoryRoutes);
|
||||||
|
routes.route("/auth", authRoutes);
|
||||||
|
|
||||||
export default routes;
|
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({
|
export const queryAllUsersByInstanceId = z.object({
|
||||||
instanceId: z.uuidv7(),
|
instanceId: z.uuidv7(),
|
||||||
});
|
});
|
||||||
import { is } from "zod/v4/locales";
|
|
||||||
export const createUserSchema = z.object({
|
export const createUserSchema = z.object({
|
||||||
username: z.string().min(3).max(30),
|
username: z.string().min(3).max(30),
|
||||||
nickname: z.string().min(1).max(30).optional(),
|
nickname: z.string().min(1).max(30).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user