add: login endpoint in backend
This commit is contained in:
parent
5be288bfac
commit
19851ec10c
6
bun.lock
6
bun.lock
@ -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=="],
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
104
packages/elysia/routes/auth/session.ts
Normal file
104
packages/elysia/routes/auth/session.ts
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
})
|
||||
}
|
||||
);
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user