Migrate log in link DB functions to Kysely

This commit is contained in:
Kalle 2026-01-26 19:59:41 +02:00
parent a1257b7c00
commit c0692f1081
6 changed files with 112 additions and 65 deletions

View File

@ -0,0 +1,69 @@
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { dbInsertUsers, dbReset } from "~/utils/Test";
import * as LogInLinkRepository from "./LogInLinkRepository.server";
describe("create", () => {
beforeEach(async () => {
await dbInsertUsers(1);
});
afterEach(() => {
dbReset();
});
test("creates a login link with correct userId", async () => {
const link = await LogInLinkRepository.create(1);
expect(link.userId).toBe(1);
});
test("creates a login link with future expiration", async () => {
const beforeCreation = Math.floor(Date.now() / 1000);
const link = await LogInLinkRepository.create(1);
expect(link.expiresAt).toBeGreaterThan(beforeCreation);
});
});
describe("del", () => {
beforeEach(async () => {
await dbInsertUsers(1);
});
afterEach(() => {
dbReset();
});
test("deletes a login link by code", async () => {
const link = await LogInLinkRepository.create(1);
await LogInLinkRepository.del(link.code);
const result = await LogInLinkRepository.findValidByCode(link.code);
expect(result).toBeUndefined();
});
});
describe("findValidByCode", () => {
beforeEach(async () => {
await dbInsertUsers(1);
});
afterEach(() => {
dbReset();
});
test("returns userId for valid code", async () => {
const link = await LogInLinkRepository.create(1);
const result = await LogInLinkRepository.findValidByCode(link.code);
expect(result?.userId).toBe(1);
});
test("returns undefined for non-existent code", async () => {
const result = await LogInLinkRepository.findValidByCode("nonexistent1");
expect(result).toBeUndefined();
});
});

View File

@ -0,0 +1,37 @@
import { add } from "date-fns";
import { nanoid } from "nanoid";
import { db } from "~/db/sql";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
const LOG_IN_LINK_LENGTH = 12;
const LOG_IN_LINK_VALID_FOR_MINUTES = 10;
/** Creates a new login link for a user with 10-minute expiration */
export function create(userId: number) {
return db
.insertInto("LogInLink")
.values({
code: nanoid(LOG_IN_LINK_LENGTH),
expiresAt: dateToDatabaseTimestamp(
add(new Date(), { minutes: LOG_IN_LINK_VALID_FOR_MINUTES }),
),
userId,
})
.returningAll()
.executeTakeFirstOrThrow();
}
/** Deletes a login link by its code */
export function del(code: string) {
return db.deleteFrom("LogInLink").where("code", "=", code).execute();
}
/** Finds a valid (non-expired) login link by code, returns userId if valid */
export function findValidByCode(code: string) {
return db
.selectFrom("LogInLink")
.select("userId")
.where("code", "=", code)
.where("expiresAt", ">", databaseTimestampNow())
.executeTakeFirst();
}

View File

@ -12,9 +12,7 @@ import {
parseSearchParams,
} from "~/utils/remix.server";
import { ADMIN_PAGE, authErrorUrl } from "~/utils/urls";
import { createLogInLink } from "../queries/createLogInLink.server";
import { deleteLogInLinkByCode } from "../queries/deleteLogInLinkByCode.server";
import { userIdByLogInLinkCode } from "../queries/userIdByLogInLinkCode.server";
import * as LogInLinkRepository from "../LogInLinkRepository.server";
import {
authenticator,
IMPERSONATED_SESSION_KEY,
@ -141,7 +139,7 @@ export const createLogInLinkAction: ActionFunction = async ({ request }) => {
if (data.updateOnly === "true") return null;
const createdLink = createLogInLink(user.id);
const createdLink = await LogInLinkRepository.create(user.id);
return {
code: createdLink.code,
@ -169,10 +167,11 @@ export const logInViaLinkLoader: LoaderFunction = async ({ request }) => {
throw redirect("/");
}
const userId = userIdByLogInLinkCode(data.code);
if (!userId) {
const result = await LogInLinkRepository.findValidByCode(data.code);
if (!result) {
throw new Response("Invalid log in link", { status: 400 });
}
const userId = result.userId;
const session = await authSessionStorage.getSession(
request.headers.get("Cookie"),
@ -180,7 +179,7 @@ export const logInViaLinkLoader: LoaderFunction = async ({ request }) => {
session.set(SESSION_KEY, userId);
deleteLogInLinkByCode(data.code);
await LogInLinkRepository.del(data.code);
throw redirect("/", {
headers: { "Set-Cookie": await authSessionStorage.commitSession(session) },

View File

@ -1,30 +0,0 @@
import { nanoid } from "nanoid";
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
import { dateToDatabaseTimestamp } from "~/utils/dates";
const stm = sql.prepare(/* sql */ `
insert into "LogInLink" (
"userId",
"expiresAt",
"code"
) values (
@userId,
@expiresAt,
@code
) returning *
`);
// 10 minutes
const LOG_IN_LINK_VALID_FOR = 10 * 60 * 1000;
const LOG_IN_LINK_LENGTH = 12;
export function createLogInLink(userId: number) {
return stm.get({
userId,
expiresAt: dateToDatabaseTimestamp(
new Date(Date.now() + LOG_IN_LINK_VALID_FOR),
),
code: nanoid(LOG_IN_LINK_LENGTH),
}) as Tables["LogInLink"];
}

View File

@ -1,10 +0,0 @@
import { sql } from "~/db/sql";
const stm = sql.prepare(/* sql */ `
delete from "LogInLink"
where "code" = @code
`);
export function deleteLogInLinkByCode(code: string) {
return stm.run({ code });
}

View File

@ -1,18 +0,0 @@
import { sql } from "~/db/sql";
import { dateToDatabaseTimestamp } from "~/utils/dates";
const stm = sql.prepare(/* sql */ `
select "userId"
from "LogInLink"
where "code" = @code
and "expiresAt" > @now
`);
export function userIdByLogInLinkCode(code: string) {
return (
stm.get({
code,
now: dateToDatabaseTimestamp(new Date()),
}) as any
)?.userId as number | undefined;
}