mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
143 lines
5.0 KiB
TypeScript
143 lines
5.0 KiB
TypeScript
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 { cookieOptions } from "$lib/server/modules/auth";
|
|
import { generateChallenge, verifySignature, issueSessionId } from "$lib/server/modules/crypto";
|
|
import { router, publicProcedure, roleProcedure } from "../init.server";
|
|
|
|
const authRouter = router({
|
|
login: publicProcedure
|
|
.input(
|
|
z.object({
|
|
email: z.email(),
|
|
password: z.string().nonempty(),
|
|
}),
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const user = await UserRepo.getUserByEmail(input.email);
|
|
if (!user || !(await argon2.verify(user.password, input.password))) {
|
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid email or password" });
|
|
}
|
|
|
|
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
|
|
await SessionRepo.createSession(user.id, sessionId, ctx.locals.ip, ctx.locals.userAgent);
|
|
ctx.cookies.set("sessionId", sessionIdSigned, cookieOptions);
|
|
}),
|
|
|
|
logout: roleProcedure["any"].mutation(async ({ ctx }) => {
|
|
await SessionRepo.deleteSession(ctx.session.sessionId);
|
|
ctx.cookies.delete("sessionId", cookieOptions);
|
|
}),
|
|
|
|
changePassword: roleProcedure["any"]
|
|
.input(
|
|
z.object({
|
|
oldPassword: z.string().nonempty(),
|
|
newPassword: z.string().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 argon2.verify(user.password, input.oldPassword))) {
|
|
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid password" });
|
|
}
|
|
|
|
await UserRepo.setUserPassword(ctx.session.userId, await argon2.hash(input.newPassword));
|
|
await SessionRepo.deleteAllOtherSessions(ctx.session.userId, ctx.session.sessionId);
|
|
}),
|
|
|
|
upgrade: 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") };
|
|
}),
|
|
|
|
verifyUpgrade: 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;
|