mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
/api/auth 아래의 Endpoint들을 tRPC로 마이그레이션
This commit is contained in:
@@ -3,11 +3,6 @@ import env from "$lib/server/loadenv";
|
|||||||
import { authenticate, AuthenticationError } from "$lib/server/modules/auth";
|
import { authenticate, AuthenticationError } from "$lib/server/modules/auth";
|
||||||
|
|
||||||
export const authenticateMiddleware: Handle = async ({ event, resolve }) => {
|
export const authenticateMiddleware: Handle = async ({ event, resolve }) => {
|
||||||
const { pathname, search } = event.url;
|
|
||||||
if (pathname === "/api/auth/login") {
|
|
||||||
return await resolve(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionIdSigned = event.cookies.get("sessionId");
|
const sessionIdSigned = event.cookies.get("sessionId");
|
||||||
if (!sessionIdSigned) {
|
if (!sessionIdSigned) {
|
||||||
@@ -24,6 +19,7 @@ export const authenticateMiddleware: Handle = async ({ event, resolve }) => {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof AuthenticationError) {
|
if (e instanceof AuthenticationError) {
|
||||||
|
const { pathname, search } = event.url;
|
||||||
if (pathname === "/auth/login" || pathname.startsWith("/api/trpc")) {
|
if (pathname === "/auth/login" || pathname.startsWith("/api/trpc")) {
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
} else if (pathname.startsWith("/api")) {
|
} else if (pathname.startsWith("/api")) {
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const passwordChangeRequest = z.object({
|
|
||||||
oldPassword: z.string().trim().nonempty(),
|
|
||||||
newPassword: z.string().trim().nonempty(),
|
|
||||||
});
|
|
||||||
export type PasswordChangeRequest = z.input<typeof passwordChangeRequest>;
|
|
||||||
|
|
||||||
export const loginRequest = z.object({
|
|
||||||
email: z.email(),
|
|
||||||
password: z.string().trim().nonempty(),
|
|
||||||
});
|
|
||||||
export type LoginRequest = z.input<typeof loginRequest>;
|
|
||||||
|
|
||||||
export const sessionUpgradeRequest = z.object({
|
|
||||||
encPubKey: z.base64().nonempty(),
|
|
||||||
sigPubKey: z.base64().nonempty(),
|
|
||||||
});
|
|
||||||
export type SessionUpgradeRequest = z.input<typeof sessionUpgradeRequest>;
|
|
||||||
|
|
||||||
export const sessionUpgradeResponse = z.object({
|
|
||||||
id: z.int().positive(),
|
|
||||||
challenge: z.base64().nonempty(),
|
|
||||||
});
|
|
||||||
export type SessionUpgradeResponse = z.output<typeof sessionUpgradeResponse>;
|
|
||||||
|
|
||||||
export const sessionUpgradeVerifyRequest = z.object({
|
|
||||||
id: z.int().positive(),
|
|
||||||
answerSig: z.base64().nonempty(),
|
|
||||||
force: z.boolean().default(false),
|
|
||||||
});
|
|
||||||
export type SessionUpgradeVerifyRequest = z.input<typeof sessionUpgradeVerifyRequest>;
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from "./auth";
|
|
||||||
export * from "./category";
|
export * from "./category";
|
||||||
export * from "./directory";
|
export * from "./directory";
|
||||||
export * from "./file";
|
export * from "./file";
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
|
||||||
import argon2 from "argon2";
|
|
||||||
import { getClient, getClientByPubKeys, getUserClient } from "$lib/server/db/client";
|
|
||||||
import { IntegrityError } from "$lib/server/db/error";
|
|
||||||
import {
|
|
||||||
upgradeSession,
|
|
||||||
deleteSession,
|
|
||||||
deleteAllOtherSessions,
|
|
||||||
registerSessionUpgradeChallenge,
|
|
||||||
consumeSessionUpgradeChallenge,
|
|
||||||
} from "$lib/server/db/session";
|
|
||||||
import { getUser, getUserByEmail, setUserPassword } from "$lib/server/db/user";
|
|
||||||
import env from "$lib/server/loadenv";
|
|
||||||
import { startSession } from "$lib/server/modules/auth";
|
|
||||||
import { verifySignature, generateChallenge } from "$lib/server/modules/crypto";
|
|
||||||
|
|
||||||
const hashPassword = async (password: string) => {
|
|
||||||
return await argon2.hash(password);
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyPassword = async (hash: string, password: string) => {
|
|
||||||
return await argon2.verify(hash, password);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const changePassword = async (
|
|
||||||
userId: number,
|
|
||||||
sessionId: string,
|
|
||||||
oldPassword: string,
|
|
||||||
newPassword: string,
|
|
||||||
) => {
|
|
||||||
if (oldPassword === newPassword) {
|
|
||||||
error(400, "Same passwords");
|
|
||||||
} else if (newPassword.length < 8) {
|
|
||||||
error(400, "Too short password");
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await getUser(userId);
|
|
||||||
if (!user) {
|
|
||||||
error(500, "Invalid session id");
|
|
||||||
} else if (!(await verifyPassword(user.password, oldPassword))) {
|
|
||||||
error(403, "Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
await setUserPassword(userId, await hashPassword(newPassword));
|
|
||||||
await deleteAllOtherSessions(userId, sessionId);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const login = async (email: string, password: string, ip: string, userAgent: string) => {
|
|
||||||
const user = await getUserByEmail(email);
|
|
||||||
if (!user || !(await verifyPassword(user.password, password))) {
|
|
||||||
error(401, "Invalid email or password");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { sessionIdSigned: await startSession(user.id, ip, userAgent) };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const logout = async (sessionId: string) => {
|
|
||||||
await deleteSession(sessionId);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createSessionUpgradeChallenge = async (
|
|
||||||
sessionId: string,
|
|
||||||
userId: number,
|
|
||||||
ip: string,
|
|
||||||
encPubKey: string,
|
|
||||||
sigPubKey: string,
|
|
||||||
) => {
|
|
||||||
const client = await getClientByPubKeys(encPubKey, sigPubKey);
|
|
||||||
const userClient = client ? await getUserClient(userId, client.id) : undefined;
|
|
||||||
if (!client) {
|
|
||||||
error(401, "Invalid public key(s)");
|
|
||||||
} else if (!userClient || userClient.state === "challenging") {
|
|
||||||
error(403, "Unregistered client");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { answer, challenge } = await generateChallenge(32, encPubKey);
|
|
||||||
const { id } = await registerSessionUpgradeChallenge(
|
|
||||||
sessionId,
|
|
||||||
client.id,
|
|
||||||
answer.toString("base64"),
|
|
||||||
ip,
|
|
||||||
new Date(Date.now() + env.challenge.sessionUpgradeExp),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { id, challenge: challenge.toString("base64") };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const verifySessionUpgradeChallenge = async (
|
|
||||||
sessionId: string,
|
|
||||||
userId: number,
|
|
||||||
ip: string,
|
|
||||||
challengeId: number,
|
|
||||||
answerSig: string,
|
|
||||||
force: boolean,
|
|
||||||
) => {
|
|
||||||
const challenge = await consumeSessionUpgradeChallenge(challengeId, sessionId, ip);
|
|
||||||
if (!challenge) {
|
|
||||||
error(403, "Invalid challenge answer");
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await getClient(challenge.clientId);
|
|
||||||
if (!client) {
|
|
||||||
error(500, "Invalid challenge answer");
|
|
||||||
} else if (
|
|
||||||
!verifySignature(Buffer.from(challenge.answer, "base64"), answerSig, client.sigPubKey)
|
|
||||||
) {
|
|
||||||
error(403, "Invalid challenge answer signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await upgradeSession(userId, sessionId, client.id, force);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof IntegrityError) {
|
|
||||||
if (e.message === "Session not found") {
|
|
||||||
error(500, "Invalid challenge answer");
|
|
||||||
} else if (!force && e.message === "Session already exists") {
|
|
||||||
error(409, "Already logged in");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import { callPostApi } from "$lib/hooks";
|
import { TRPCClientError } from "@trpc/client";
|
||||||
import { encodeToBase64, decryptChallenge, signMessageRSA } from "$lib/modules/crypto";
|
import { encodeToBase64, decryptChallenge, signMessageRSA } from "$lib/modules/crypto";
|
||||||
import type {
|
import { useTRPC } from "$trpc/client";
|
||||||
SessionUpgradeRequest,
|
|
||||||
SessionUpgradeResponse,
|
|
||||||
SessionUpgradeVerifyRequest,
|
|
||||||
} from "$lib/server/schemas";
|
|
||||||
|
|
||||||
export const requestSessionUpgrade = async (
|
export const requestSessionUpgrade = async (
|
||||||
encryptKeyBase64: string,
|
encryptKeyBase64: string,
|
||||||
@@ -13,27 +9,45 @@ export const requestSessionUpgrade = async (
|
|||||||
signKey: CryptoKey,
|
signKey: CryptoKey,
|
||||||
force = false,
|
force = false,
|
||||||
) => {
|
) => {
|
||||||
let res = await callPostApi<SessionUpgradeRequest>("/api/auth/upgradeSession", {
|
const trpc = useTRPC();
|
||||||
encPubKey: encryptKeyBase64,
|
let id, challenge;
|
||||||
sigPubKey: verifyKeyBase64,
|
try {
|
||||||
});
|
({ id, challenge } = await trpc.auth.upgradeSession.mutate({
|
||||||
if (res.status === 403) return [false, "Unregistered client"] as const;
|
encPubKey: encryptKeyBase64,
|
||||||
else if (!res.ok) return [false] as const;
|
sigPubKey: verifyKeyBase64,
|
||||||
|
}));
|
||||||
const { id, challenge }: SessionUpgradeResponse = await res.json();
|
} catch (e) {
|
||||||
|
if (e instanceof TRPCClientError && e.data?.code === "FORBIDDEN") {
|
||||||
|
return [false, "Unregistered client"] as const;
|
||||||
|
}
|
||||||
|
return [false] as const;
|
||||||
|
}
|
||||||
const answer = await decryptChallenge(challenge, decryptKey);
|
const answer = await decryptChallenge(challenge, decryptKey);
|
||||||
const answerSig = await signMessageRSA(answer, signKey);
|
const answerSig = await signMessageRSA(answer, signKey);
|
||||||
|
|
||||||
res = await callPostApi<SessionUpgradeVerifyRequest>("/api/auth/upgradeSession/verify", {
|
try {
|
||||||
id,
|
await trpc.auth.verifySessionUpgrade.mutate({
|
||||||
answerSig: encodeToBase64(answerSig),
|
id,
|
||||||
force,
|
answerSig: encodeToBase64(answerSig),
|
||||||
});
|
force,
|
||||||
if (res.status === 409) return [false, "Already logged in"] as const;
|
});
|
||||||
else return [res.ok] as const;
|
} catch (e) {
|
||||||
|
if (e instanceof TRPCClientError && e.data?.code === "CONFLICT") {
|
||||||
|
return [false, "Already logged in"] as const;
|
||||||
|
}
|
||||||
|
return [false] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [true] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestLogout = async () => {
|
export const requestLogout = async () => {
|
||||||
const res = await callPostApi("/api/auth/logout");
|
const trpc = useTRPC();
|
||||||
return res.ok;
|
try {
|
||||||
|
await trpc.auth.logout.mutate();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// TODO: Error Handling
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { callPostApi } from "$lib/hooks";
|
import { useTRPC } from "$trpc/client";
|
||||||
import type { PasswordChangeRequest } from "$lib/server/schemas";
|
|
||||||
|
|
||||||
export const requestPasswordChange = async (oldPassword: string, newPassword: string) => {
|
export const requestPasswordChange = async (oldPassword: string, newPassword: string) => {
|
||||||
const res = await callPostApi<PasswordChangeRequest>("/api/auth/changePassword", {
|
const trpc = useTRPC();
|
||||||
oldPassword,
|
|
||||||
newPassword,
|
try {
|
||||||
});
|
await trpc.auth.changePassword.mutate({ oldPassword, newPassword });
|
||||||
return res.ok;
|
return true;
|
||||||
|
} catch {
|
||||||
|
// TODO: Error Handling
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { callPostApi } from "$lib/hooks";
|
import { useTRPC } from "$trpc/client";
|
||||||
import type { LoginRequest } from "$lib/server/schemas";
|
|
||||||
|
|
||||||
export { requestLogout } from "$lib/services/auth";
|
export { requestLogout } from "$lib/services/auth";
|
||||||
export { requestDeletedFilesCleanup } from "$lib/services/file";
|
export { requestDeletedFilesCleanup } from "$lib/services/file";
|
||||||
@@ -9,6 +8,13 @@ export {
|
|||||||
} from "$lib/services/key";
|
} from "$lib/services/key";
|
||||||
|
|
||||||
export const requestLogin = async (email: string, password: string) => {
|
export const requestLogin = async (email: string, password: string) => {
|
||||||
const res = await callPostApi<LoginRequest>("/api/auth/login", { email, password });
|
const trpc = useTRPC();
|
||||||
return res.ok;
|
|
||||||
|
try {
|
||||||
|
await trpc.auth.login.mutate({ email, password });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// TODO: Error Handling
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { error, text } from "@sveltejs/kit";
|
|
||||||
import { authorize } from "$lib/server/modules/auth";
|
|
||||||
import { passwordChangeRequest } from "$lib/server/schemas";
|
|
||||||
import { changePassword } from "$lib/server/services/auth";
|
|
||||||
import type { RequestHandler } from "./$types";
|
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ locals, request }) => {
|
|
||||||
const { sessionId, userId } = await authorize(locals, "any");
|
|
||||||
|
|
||||||
const zodRes = passwordChangeRequest.safeParse(await request.json());
|
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
|
||||||
const { oldPassword, newPassword } = zodRes.data;
|
|
||||||
|
|
||||||
await changePassword(userId, sessionId, oldPassword, newPassword);
|
|
||||||
return text("Password changed", { headers: { "Content-Type": "text/plain" } });
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { error, text } from "@sveltejs/kit";
|
|
||||||
import env from "$lib/server/loadenv";
|
|
||||||
import { loginRequest } from "$lib/server/schemas";
|
|
||||||
import { login } from "$lib/server/services/auth";
|
|
||||||
import type { RequestHandler } from "./$types";
|
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ locals, request, cookies }) => {
|
|
||||||
const zodRes = loginRequest.safeParse(await request.json());
|
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
|
||||||
const { email, password } = zodRes.data;
|
|
||||||
|
|
||||||
const { sessionIdSigned } = await login(email, password, locals.ip, locals.userAgent);
|
|
||||||
cookies.set("sessionId", sessionIdSigned, {
|
|
||||||
path: "/",
|
|
||||||
maxAge: env.session.exp / 1000,
|
|
||||||
secure: true,
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
|
|
||||||
return text("Logged in", { headers: { "Content-Type": "text/plain" } });
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { text } from "@sveltejs/kit";
|
|
||||||
import { authorize } from "$lib/server/modules/auth";
|
|
||||||
import { logout } from "$lib/server/services/auth";
|
|
||||||
import type { RequestHandler } from "./$types";
|
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ locals, cookies }) => {
|
|
||||||
const { sessionId } = await authorize(locals, "any");
|
|
||||||
|
|
||||||
await logout(sessionId);
|
|
||||||
cookies.delete("sessionId", { path: "/" });
|
|
||||||
|
|
||||||
return text("Logged out", { headers: { "Content-Type": "text/plain" } });
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { error, json } from "@sveltejs/kit";
|
|
||||||
import { authorize } from "$lib/server/modules/auth";
|
|
||||||
import {
|
|
||||||
sessionUpgradeRequest,
|
|
||||||
sessionUpgradeResponse,
|
|
||||||
type SessionUpgradeResponse,
|
|
||||||
} from "$lib/server/schemas";
|
|
||||||
import { createSessionUpgradeChallenge } from "$lib/server/services/auth";
|
|
||||||
import type { RequestHandler } from "./$types";
|
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ locals, request }) => {
|
|
||||||
const { sessionId, userId } = await authorize(locals, "notClient");
|
|
||||||
|
|
||||||
const zodRes = sessionUpgradeRequest.safeParse(await request.json());
|
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
|
||||||
const { encPubKey, sigPubKey } = zodRes.data;
|
|
||||||
|
|
||||||
const { id, challenge } = await createSessionUpgradeChallenge(
|
|
||||||
sessionId,
|
|
||||||
userId,
|
|
||||||
locals.ip,
|
|
||||||
encPubKey,
|
|
||||||
sigPubKey,
|
|
||||||
);
|
|
||||||
return json(sessionUpgradeResponse.parse({ id, challenge } satisfies SessionUpgradeResponse));
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { error, text } from "@sveltejs/kit";
|
|
||||||
import { authorize } from "$lib/server/modules/auth";
|
|
||||||
import { sessionUpgradeVerifyRequest } from "$lib/server/schemas";
|
|
||||||
import { verifySessionUpgradeChallenge } from "$lib/server/services/auth";
|
|
||||||
import type { RequestHandler } from "./$types";
|
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ locals, request }) => {
|
|
||||||
const { sessionId, userId } = await authorize(locals, "notClient");
|
|
||||||
|
|
||||||
const zodRes = sessionUpgradeVerifyRequest.safeParse(await request.json());
|
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
|
||||||
const { id, answerSig, force } = zodRes.data;
|
|
||||||
|
|
||||||
await verifySessionUpgradeChallenge(sessionId, userId, locals.ip, id, answerSig, force);
|
|
||||||
return text("Session upgraded", { headers: { "Content-Type": "text/plain" } });
|
|
||||||
};
|
|
||||||
@@ -2,6 +2,7 @@ import type { RequestEvent } from "@sveltejs/kit";
|
|||||||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||||
import { createContext, router } from "./init.server";
|
import { createContext, router } from "./init.server";
|
||||||
import {
|
import {
|
||||||
|
authRouter,
|
||||||
categoryRouter,
|
categoryRouter,
|
||||||
clientRouter,
|
clientRouter,
|
||||||
directoryRouter,
|
directoryRouter,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
} from "./routers";
|
} from "./routers";
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
|
auth: authRouter,
|
||||||
category: categoryRouter,
|
category: categoryRouter,
|
||||||
client: clientRouter,
|
client: clientRouter,
|
||||||
directory: directoryRouter,
|
directory: directoryRouter,
|
||||||
|
|||||||
153
src/trpc/routers/auth.ts
Normal file
153
src/trpc/routers/auth.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import argon2 from "argon2";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { ClientRepo, SessionRepo, UserRepo, IntegrityError } from "$lib/server/db";
|
||||||
|
import env from "$lib/server/loadenv";
|
||||||
|
import { startSession } from "$lib/server/modules/auth";
|
||||||
|
import { generateChallenge, verifySignature } from "$lib/server/modules/crypto";
|
||||||
|
import { publicProcedure, roleProcedure, router } from "../init.server";
|
||||||
|
|
||||||
|
const hashPassword = async (password: string) => {
|
||||||
|
return await argon2.hash(password);
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyPassword = async (hash: string, password: string) => {
|
||||||
|
return await argon2.verify(hash, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
const authRouter = router({
|
||||||
|
login: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
email: z.email(),
|
||||||
|
password: z.string().trim().nonempty(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const user = await UserRepo.getUserByEmail(input.email);
|
||||||
|
if (!user || !(await verifyPassword(user.password, input.password))) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid email or password" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionIdSigned = await startSession(user.id, ctx.locals.ip, ctx.locals.userAgent);
|
||||||
|
ctx.cookies.set("sessionId", sessionIdSigned, {
|
||||||
|
path: "/",
|
||||||
|
maxAge: env.session.exp / 1000,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "strict",
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
logout: roleProcedure["any"].mutation(async ({ ctx }) => {
|
||||||
|
await SessionRepo.deleteSession(ctx.session.sessionId);
|
||||||
|
ctx.cookies.delete("sessionId", { path: "/" });
|
||||||
|
}),
|
||||||
|
|
||||||
|
changePassword: roleProcedure["any"]
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
oldPassword: z.string().trim().nonempty(),
|
||||||
|
newPassword: z.string().trim().nonempty(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (input.oldPassword === input.newPassword) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "Same passwords" });
|
||||||
|
} else if (input.newPassword.length < 8) {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: "Too short password" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await UserRepo.getUser(ctx.session.userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" });
|
||||||
|
} else if (!(await verifyPassword(user.password, input.oldPassword))) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid password" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserRepo.setUserPassword(ctx.session.userId, await hashPassword(input.newPassword));
|
||||||
|
await SessionRepo.deleteAllOtherSessions(ctx.session.userId, ctx.session.sessionId);
|
||||||
|
}),
|
||||||
|
|
||||||
|
upgradeSession: roleProcedure["notClient"]
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
encPubKey: z.base64().nonempty(),
|
||||||
|
sigPubKey: z.base64().nonempty(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const client = await ClientRepo.getClientByPubKeys(input.encPubKey, input.sigPubKey);
|
||||||
|
const userClient = client
|
||||||
|
? await ClientRepo.getUserClient(ctx.session.userId, client.id)
|
||||||
|
: undefined;
|
||||||
|
if (!client) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid public key(s)" });
|
||||||
|
} else if (!userClient || userClient.state === "challenging") {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN", message: "Unregistered client" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { answer, challenge } = await generateChallenge(32, input.encPubKey);
|
||||||
|
const { id } = await SessionRepo.registerSessionUpgradeChallenge(
|
||||||
|
ctx.session.sessionId,
|
||||||
|
client.id,
|
||||||
|
answer.toString("base64"),
|
||||||
|
ctx.locals.ip,
|
||||||
|
new Date(Date.now() + env.challenge.sessionUpgradeExp),
|
||||||
|
);
|
||||||
|
return { id, challenge: challenge.toString("base64") };
|
||||||
|
}),
|
||||||
|
|
||||||
|
verifySessionUpgrade: roleProcedure["notClient"]
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
id: z.int().positive(),
|
||||||
|
answerSig: z.base64().nonempty(),
|
||||||
|
force: z.boolean().default(false),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const challenge = await SessionRepo.consumeSessionUpgradeChallenge(
|
||||||
|
input.id,
|
||||||
|
ctx.session.sessionId,
|
||||||
|
ctx.locals.ip,
|
||||||
|
);
|
||||||
|
if (!challenge) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await ClientRepo.getClient(challenge.clientId);
|
||||||
|
if (!client) {
|
||||||
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid challenge answer" });
|
||||||
|
} else if (
|
||||||
|
!verifySignature(Buffer.from(challenge.answer, "base64"), input.answerSig, client.sigPubKey)
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Invalid challenge answer signature",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await SessionRepo.upgradeSession(
|
||||||
|
ctx.session.userId,
|
||||||
|
ctx.session.sessionId,
|
||||||
|
client.id,
|
||||||
|
input.force,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof IntegrityError) {
|
||||||
|
if (e.message === "Session not found") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Invalid challenge answer",
|
||||||
|
});
|
||||||
|
} else if (!input.force && e.message === "Session already exists") {
|
||||||
|
throw new TRPCError({ code: "CONFLICT", message: "Already logged in" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default authRouter;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { default as authRouter } from "./auth";
|
||||||
export { default as categoryRouter } from "./category";
|
export { default as categoryRouter } from "./category";
|
||||||
export { default as clientRouter } from "./client";
|
export { default as clientRouter } from "./client";
|
||||||
export { default as directoryRouter } from "./directory";
|
export { default as directoryRouter } from "./directory";
|
||||||
|
|||||||
Reference in New Issue
Block a user