mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
/api/auth/logout, /api/auth/refreshToken Endpoint 구현
This commit is contained in:
@@ -1,44 +0,0 @@
|
|||||||
import argon2 from "argon2";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import { getClientByPubKey } from "$lib/server/db/client";
|
|
||||||
import { getUserByEmail } from "$lib/server/db/user";
|
|
||||||
import env from "$lib/server/loadenv";
|
|
||||||
|
|
||||||
interface TokenData {
|
|
||||||
type: "access" | "refresh";
|
|
||||||
userId: number;
|
|
||||||
clientId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyPassword = async (hash: string, password: string) => {
|
|
||||||
return await argon2.verify(hash, password);
|
|
||||||
};
|
|
||||||
|
|
||||||
const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => {
|
|
||||||
return jwt.sign(
|
|
||||||
{
|
|
||||||
type,
|
|
||||||
userId,
|
|
||||||
clientId,
|
|
||||||
} satisfies TokenData,
|
|
||||||
env.jwt.secret,
|
|
||||||
{
|
|
||||||
expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const login = async (email: string, password: string, pubKey?: string) => {
|
|
||||||
const user = await getUserByEmail(email);
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
const isValid = await verifyPassword(user.password, password);
|
|
||||||
if (!isValid) return null;
|
|
||||||
|
|
||||||
const client = pubKey ? await getClientByPubKey(pubKey) : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessToken: issueToken("access", user.id, client?.id),
|
|
||||||
refreshToken: issueToken("refresh", user.id, client?.id),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -5,3 +5,9 @@ export const user = sqliteTable("user", {
|
|||||||
email: text("email").notNull().unique(),
|
email: text("email").notNull().unique(),
|
||||||
password: text("password").notNull(),
|
password: text("password").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const revokedToken = sqliteTable("revoked_token", {
|
||||||
|
id: integer("id").primaryKey(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
revokedAt: integer("revoked_at").notNull(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import db from "./drizzle";
|
import db from "./drizzle";
|
||||||
import { user } from "./schema";
|
import { user, revokedToken } from "./schema";
|
||||||
|
|
||||||
export const getUserByEmail = async (email: string) => {
|
export const getUserByEmail = async (email: string) => {
|
||||||
const users = await db.select().from(user).where(eq(user.email, email)).execute();
|
const users = await db.select().from(user).where(eq(user.email, email)).execute();
|
||||||
return users[0] ?? null;
|
return users[0] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const revokeToken = async (token: string) => {
|
||||||
|
await db
|
||||||
|
.insert(revokedToken)
|
||||||
|
.values({
|
||||||
|
token,
|
||||||
|
revokedAt: Date.now(),
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isTokenRevoked = async (token: string) => {
|
||||||
|
const tokens = await db
|
||||||
|
.select()
|
||||||
|
.from(revokedToken)
|
||||||
|
.where(eq(revokedToken.token, token))
|
||||||
|
.execute();
|
||||||
|
return tokens.length > 0;
|
||||||
|
};
|
||||||
|
|||||||
38
src/lib/server/modules/auth.ts
Normal file
38
src/lib/server/modules/auth.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import env from "$lib/server/loadenv";
|
||||||
|
|
||||||
|
interface TokenData {
|
||||||
|
type: "access" | "refresh";
|
||||||
|
userId: number;
|
||||||
|
clientId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TokenError {
|
||||||
|
EXPIRED,
|
||||||
|
INVALID,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => {
|
||||||
|
return jwt.sign(
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
userId,
|
||||||
|
clientId,
|
||||||
|
} satisfies TokenData,
|
||||||
|
env.jwt.secret,
|
||||||
|
{
|
||||||
|
expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyToken = (token: string) => {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, env.jwt.secret) as TokenData;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
return TokenError.EXPIRED;
|
||||||
|
}
|
||||||
|
return TokenError.INVALID;
|
||||||
|
}
|
||||||
|
};
|
||||||
60
src/lib/server/services/auth.ts
Normal file
60
src/lib/server/services/auth.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
import argon2 from "argon2";
|
||||||
|
import { getClientByPubKey } from "$lib/server/db/client";
|
||||||
|
import { getUserByEmail, revokeToken, isTokenRevoked } from "$lib/server/db/user";
|
||||||
|
import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth";
|
||||||
|
|
||||||
|
const verifyPassword = async (hash: string, password: string) => {
|
||||||
|
return await argon2.verify(hash, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const login = async (email: string, password: string, pubKey?: string) => {
|
||||||
|
const user = await getUserByEmail(email);
|
||||||
|
if (!user) {
|
||||||
|
error(401, "Invalid email or password");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await verifyPassword(user.password, password);
|
||||||
|
if (!isValid) {
|
||||||
|
error(401, "Invalid email or password");
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = pubKey ? await getClientByPubKey(pubKey) : undefined;
|
||||||
|
if (client === null) {
|
||||||
|
error(401, "Invalid public key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: issueToken("access", user.id, client?.id),
|
||||||
|
refreshToken: issueToken("refresh", user.id, client?.id),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyRefreshToken = async (refreshToken: string) => {
|
||||||
|
const tokenData = verifyToken(refreshToken);
|
||||||
|
if (tokenData === TokenError.EXPIRED) {
|
||||||
|
error(401, "Token expired");
|
||||||
|
} else if (
|
||||||
|
tokenData === TokenError.INVALID ||
|
||||||
|
tokenData.type !== "refresh" ||
|
||||||
|
(await isTokenRevoked(refreshToken))
|
||||||
|
) {
|
||||||
|
error(401, "Invalid token");
|
||||||
|
}
|
||||||
|
return tokenData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logout = async (refreshToken: string) => {
|
||||||
|
await verifyRefreshToken(refreshToken);
|
||||||
|
await revokeToken(refreshToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshToken = async (refreshToken: string) => {
|
||||||
|
const tokenData = await verifyRefreshToken(refreshToken);
|
||||||
|
|
||||||
|
await revokeToken(refreshToken);
|
||||||
|
return {
|
||||||
|
accessToken: issueToken("access", tokenData.userId, tokenData.clientId),
|
||||||
|
refreshToken: issueToken("refresh", tokenData.userId, tokenData.clientId),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { error, json } from "@sveltejs/kit";
|
import { error, json } from "@sveltejs/kit";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { login } from "$lib/server/auth";
|
import { login } from "$lib/server/services/auth";
|
||||||
import type { RequestHandler } from "./$types";
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
@@ -14,9 +14,5 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||||||
if (!zodRes.success) error(400, zodRes.error.message);
|
if (!zodRes.success) error(400, zodRes.error.message);
|
||||||
|
|
||||||
const { email, password, pubKey } = zodRes.data;
|
const { email, password, pubKey } = zodRes.data;
|
||||||
const loginRes = await login(email.trim(), password.trim(), pubKey?.trim());
|
return json(await login(email.trim(), password.trim(), pubKey?.trim()));
|
||||||
if (!loginRes) error(401, "Invalid email, password, or public key");
|
|
||||||
|
|
||||||
const { accessToken, refreshToken } = loginRes;
|
|
||||||
return json({ accessToken, refreshToken });
|
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/routes/api/auth/logout/+server.ts
Normal file
18
src/routes/api/auth/logout/+server.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { error, text } from "@sveltejs/kit";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { logout } from "$lib/server/services/auth";
|
||||||
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
const zodRes = z
|
||||||
|
.object({
|
||||||
|
refreshToken: z.string().nonempty(),
|
||||||
|
})
|
||||||
|
.safeParse(await request.json());
|
||||||
|
if (!zodRes.success) error(400, zodRes.error.message);
|
||||||
|
|
||||||
|
const { refreshToken } = zodRes.data;
|
||||||
|
await logout(refreshToken.trim());
|
||||||
|
|
||||||
|
return text("Logged out");
|
||||||
|
};
|
||||||
16
src/routes/api/auth/refreshToken/+server.ts
Normal file
16
src/routes/api/auth/refreshToken/+server.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { error, json } from "@sveltejs/kit";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { refreshToken } from "$lib/server/services/auth";
|
||||||
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
const zodRes = z
|
||||||
|
.object({
|
||||||
|
refreshToken: z.string().nonempty(),
|
||||||
|
})
|
||||||
|
.safeParse(await request.json());
|
||||||
|
if (!zodRes.success) error(400, zodRes.error.message);
|
||||||
|
|
||||||
|
const { refreshToken: token } = zodRes.data;
|
||||||
|
return json(await refreshToken(token.trim()));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user