1
0

add: login endpoint in backend

This commit is contained in:
alikia2x (寒寒) 2025-11-07 00:11:21 +08:00
parent 5be288bfac
commit 19851ec10c
7 changed files with 260 additions and 32 deletions

View File

@ -84,12 +84,14 @@
"name": "elysia-api",
"version": "0.7.0",
"dependencies": {
"@alikia/random-key": "^1.1.1",
"@elysiajs/cors": "^1.4.0",
"@elysiajs/openapi": "^1.4.0",
"@elysiajs/server-timing": "^1.4.0",
"@rabbit-company/argon2id": "^2.1.0",
"chalk": "^5.6.2",
"elysia": "^1.4.0",
"elysia-ip": "^1.0.10",
"zod": "^4.1.11",
},
"devDependencies": {
@ -266,6 +268,8 @@
"@alikia/dark-theme-hook": ["@alikia/dark-theme-hook@1.0.2", "", { "dependencies": { "react": "^18.3.1" } }, "sha512-OloatZRefHB7Ey3zjhfsKFZoHbfzewPfOeEwA7q9zDXViNKGyTA4CiCF3US5vzqfhpR16wpYcPSmpVabKI3MYg=="],
"@alikia/random-key": ["@alikia/random-key@1.1.1", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-B1u15FRJoLBfx+n32YvnvMzxjK5QARJAzqkNbyI+Ve16jxL+SX8Q+hU4J/uEhCYxqdzs/eBKCTf6L5ThGFjxWw=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
@ -1516,6 +1520,8 @@
"elysia-api": ["elysia-api@workspace:packages/elysia"],
"elysia-ip": ["elysia-ip@1.0.10", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],

View File

@ -1 +1,132 @@
import Argon2id from "@rabbit-company/argon2id";
import Argon2id from "@rabbit-company/argon2id";
import { dbMain } from "@core/drizzle";
import { usersInCredentials, loginSessionsInCredentials } from "@core/drizzle/main/schema";
import { eq, and, isNull } from "drizzle-orm";
import { generate as generateId } from "@alikia/random-key";
import logger from "@core/log";
export interface User {
id: number;
username: string;
nickname: string | null;
role: string;
}
export async function verifyUser(username: string, password: string): Promise<User | null> {
const user = await dbMain
.select()
.from(usersInCredentials)
.where(eq(usersInCredentials.username, username))
.limit(1);
if (user.length === 0) {
return null;
}
const foundUser = user[0];
const isPasswordValid = await Argon2id.verify(foundUser.password, password);
if (!isPasswordValid) {
return null;
}
return {
id: foundUser.id,
username: foundUser.username,
nickname: foundUser.nickname,
role: foundUser.role
};
}
export async function createSession(
userId: number,
ipAddress: string | null,
userAgent: string,
expiresInDays: number = 30
): Promise<string> {
const sessionId = await generateId(24);
const expireAt = new Date();
expireAt.setDate(expireAt.getDate() + expiresInDays);
try {
await dbMain.insert(loginSessionsInCredentials).values({
id: sessionId,
uid: userId,
ipAddress,
userAgent,
lastUsedAt: new Date().toISOString(),
expireAt: expireAt.toISOString()
});
} catch (error) {
logger.error(error as Error);
throw error;
}
return sessionId;
}
export async function validateSession(
sessionId: string
): Promise<{ user: User; session: any } | null> {
const session = await dbMain
.select()
.from(loginSessionsInCredentials)
.where(
and(
eq(loginSessionsInCredentials.id, sessionId),
isNull(loginSessionsInCredentials.deactivatedAt)
)
)
.limit(1);
if (session.length === 0) {
return null;
}
const foundSession = session[0];
if (foundSession.expireAt && new Date(foundSession.expireAt) < new Date()) {
return null;
}
const user = await dbMain
.select()
.from(usersInCredentials)
.where(eq(usersInCredentials.id, foundSession.uid))
.limit(1);
if (user.length === 0) {
return null;
}
await dbMain
.update(loginSessionsInCredentials)
.set({ lastUsedAt: new Date().toISOString() })
.where(eq(loginSessionsInCredentials.id, sessionId));
return {
user: {
id: user[0].id,
username: user[0].username,
nickname: user[0].nickname,
role: user[0].role
},
session: foundSession
};
}
export async function deactivateSession(sessionId: string): Promise<boolean> {
const result = await dbMain
.update(loginSessionsInCredentials)
.set({
deactivatedAt: new Date().toISOString()
})
.where(eq(loginSessionsInCredentials.id, sessionId));
return result.length ? result.length > 0 : false;
}
export function getSessionExpirationDate(days: number = 30): Date {
const expireAt = new Date();
expireAt.setDate(expireAt.getDate() + days);
return expireAt;
}

View File

@ -7,12 +7,14 @@
"format": "prettier --write ."
},
"dependencies": {
"@alikia/random-key": "^1.1.1",
"@elysiajs/cors": "^1.4.0",
"@elysiajs/openapi": "^1.4.0",
"@elysiajs/server-timing": "^1.4.0",
"@rabbit-company/argon2id": "^2.1.0",
"chalk": "^5.6.2",
"elysia": "^1.4.0",
"elysia-ip": "^1.0.10",
"zod": "^4.1.11"
},
"devDependencies": {

View File

@ -0,0 +1,104 @@
import { Elysia, t } from "elysia";
import { ip } from "elysia-ip";
import { verifyUser, createSession, deactivateSession, getSessionExpirationDate } from "@elysia/lib/auth";
export const authHandler = new Elysia({ prefix: "/auth" })
.use(ip())
.post(
"/session",
async ({ body, set, cookie, ip, request }) => {
const { username, password } = body;
const user = await verifyUser(username, password);
if (!user) {
set.status = 401;
return { message: "Invalid credentials." };
}
const userAgent = request.headers.get("user-agent") || "Unknown";
const sessionId = await createSession(user.id, ip || null, userAgent);
const expiresAt = getSessionExpirationDate();
cookie.sessionId.value = sessionId;
cookie.sessionId.httpOnly = true;
cookie.sessionId.secure = process.env.NODE_ENV === 'production';
cookie.sessionId.sameSite = 'strict';
cookie.sessionId.expires = expiresAt;
return {
message: "You are logged in.",
user: {
id: user.id,
username: user.username,
nickname: user.nickname,
role: user.role
}
};
},
{
response: {
200: t.Object({
message: t.String(),
user: t.Object({
id: t.Integer(),
username: t.String(),
nickname: t.Optional(t.String()),
role: t.String()
})
}),
401: t.Object({
message: t.String()
})
},
body: t.Object({
username: t.String(),
password: t.String()
})
}
)
.delete(
"/session",
async ({ set, cookie }) => {
const sessionId = cookie.sessionId?.value;
if (!sessionId) {
set.status = 401;
return { message: "Not authenticated." };
}
await deactivateSession(sessionId as string);
cookie.sessionId.remove();
return { message: "Successfully logged out." };
},
{
response: {
200: t.Object({
message: t.String()
}),
401: t.Object({
message: t.String()
})
}
}
)
.delete(
"/session/:id",
async ({ params }) => {
const sessionId = params.id;
await deactivateSession(sessionId as string);
return { message: "Successfully logged out." };
},
{
response: {
200: t.Object({
message: t.String()
}),
401: t.Object({
message: t.String()
})
}
}
);

View File

@ -66,13 +66,13 @@ export const songInfoHandler = new Elysia({ prefix: "/song" })
const songID = await getSongID(id);
if (!songID) {
return status(404, {
message: "song not found"
message: "Given song cannot be found."
});
}
const info = await getSongInfo(songID);
if (!info) {
return status(404, {
message: "song not found"
message: "Given song cannot be found."
});
}
const singers = await getSingers(info.id);
@ -117,13 +117,13 @@ export const songInfoHandler = new Elysia({ prefix: "/song" })
const songID = await getSongID(id);
if (!songID) {
return status(404, {
message: "song not found"
message: "Given song cannot be found."
});
}
const info = await getSongInfo(songID);
if (!info) {
return status(404, {
message: "song not found"
message: "Given song cannot be found."
});
}
if (body.name) {
@ -133,16 +133,23 @@ export const songInfoHandler = new Elysia({ prefix: "/song" })
await dbMain
.update(songs)
.set({ producer: body.producer })
.where(eq(songs.id, songID));
.where(eq(songs.id, songID))
.returning();
}
const updatedData = await dbMain
.select()
.from(songs)
.where(eq(songs.id, songID));
return {
message: "success"
message: "Successfully updated song info.",
updated: updatedData.length > 0 ? updatedData[0] : null
};
},
{
response: {
200: t.Object({
message: t.String()
message: t.String(),
updated: t.Any()
}),
404: t.Object({
message: t.String()

View File

@ -1,24 +0,0 @@
import { dbCred } from "@core/drizzle";
import { users } from "@core/drizzle/cred/schema";
import { Elysia, t } from "elysia";
export const loginHandler = new Elysia({ prefix: "/login" }).post(
"/session",
async ({ params, status, body }) => {
const { username, password } = body;
return {};
},
{
response: {
200: t.Object({}),
404: t.Object({
message: t.String()
})
},
body: t.Object({
username: t.String(),
password: t.String()
})
}
);

View File

@ -7,6 +7,7 @@ import { songInfoHandler } from "@elysia/routes/song/info";
import { rootHandler } from "@elysia/routes/root";
import { getVideoMetadataHandler } from "@elysia/routes/video/metadata";
import { closeMileStoneHandler } from "@elysia/routes/song/milestone";
import { authHandler } from "@elysia/routes/auth/session";
const [host, port] = getBindingInfo();
logStartup(host, port);
@ -63,6 +64,7 @@ const app = new Elysia({
.use(openapi())
.use(rootHandler)
.use(pingHandler)
.use(authHandler)
.use(getVideoMetadataHandler)
.use(songInfoHandler)
.use(closeMileStoneHandler)