mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 23:27:55 -05:00
Alternative login flow (magic link via bot) (#1488)
* Log in link creation initial * Add global name to update all command * Remove left over log * Login command * Update command * Add todos * TODOs * Migration file fix order
This commit is contained in:
@@ -4,6 +4,8 @@ export {
|
||||
stopImpersonatingAction,
|
||||
logInAction,
|
||||
logOutAction,
|
||||
createLogInLinkAction,
|
||||
logInViaLinkLoader,
|
||||
} from "./routes.server";
|
||||
|
||||
export { getUser, requireUser } from "./user.server";
|
||||
|
||||
30
app/modules/auth/queries/createLogInLink.server.ts
Normal file
30
app/modules/auth/queries/createLogInLink.server.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { sql } from "~/db/sql";
|
||||
import type { LogInLink } from "~/db/types";
|
||||
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 LogInLink;
|
||||
}
|
||||
10
app/modules/auth/queries/deleteLogInLinkByCode.server.ts
Normal file
10
app/modules/auth/queries/deleteLogInLinkByCode.server.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
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 });
|
||||
}
|
||||
18
app/modules/auth/queries/userIdByLogInLinkCode.server.ts
Normal file
18
app/modules/auth/queries/userIdByLogInLinkCode.server.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { canPerformAdminActions } from "~/permissions";
|
||||
import { canAccessLohiEndpoint, canPerformAdminActions } from "~/permissions";
|
||||
import { ADMIN_PAGE, authErrorUrl } from "~/utils/urls";
|
||||
import {
|
||||
authenticator,
|
||||
DISCORD_AUTH_KEY,
|
||||
IMPERSONATED_SESSION_KEY,
|
||||
SESSION_KEY,
|
||||
} from "./authenticator.server";
|
||||
import { authSessionStorage } from "./session.server";
|
||||
import { getUserId } from "./user.server";
|
||||
import { validate } from "~/utils/remix";
|
||||
import { parseSearchParams, validate } from "~/utils/remix";
|
||||
import { z } from "zod";
|
||||
import { createLogInLink } from "./queries/createLogInLink.server";
|
||||
import { userIdByLogInLinkCode } from "./queries/userIdByLogInLinkCode.server";
|
||||
import { deleteLogInLinkByCode } from "./queries/deleteLogInLinkByCode.server";
|
||||
import { db } from "~/db";
|
||||
|
||||
const throwOnAuthErrors = process.env["THROW_ON_AUTH_ERROR"] === "true";
|
||||
|
||||
@@ -80,3 +86,76 @@ export const stopImpersonatingAction: ActionFunction = async ({ request }) => {
|
||||
headers: { "Set-Cookie": await authSessionStorage.commitSession(session) },
|
||||
});
|
||||
};
|
||||
|
||||
// below is alternative log-in flow that is operated via the Lohi Discord bot
|
||||
// this is intended primarily as a workaround when website is having problems communicating
|
||||
// with the Discord due to rate limits or other reasons
|
||||
|
||||
// only light validation here as we generally trust Lohi
|
||||
const createLogInLinkActionSchema = z.object({
|
||||
discordId: z.string(),
|
||||
discordAvatar: z.string(),
|
||||
discordName: z.string(),
|
||||
discordUniqueName: z.string(),
|
||||
updateOnly: z.enum(["true", "false"]),
|
||||
});
|
||||
|
||||
export const createLogInLinkAction: ActionFunction = ({ request }) => {
|
||||
const data = parseSearchParams({
|
||||
request,
|
||||
schema: createLogInLinkActionSchema,
|
||||
});
|
||||
|
||||
if (!canAccessLohiEndpoint(request)) {
|
||||
throw new Response(null, { status: 403 });
|
||||
}
|
||||
|
||||
const user = db.users.upsertLite({
|
||||
discordAvatar: data.discordAvatar,
|
||||
discordDiscriminator: "0",
|
||||
discordId: data.discordId,
|
||||
discordName: data.discordName,
|
||||
discordUniqueName: data.discordUniqueName,
|
||||
});
|
||||
|
||||
if (data.updateOnly === "true") return null;
|
||||
|
||||
const createdLink = createLogInLink(user.id);
|
||||
|
||||
return {
|
||||
code: createdLink.code,
|
||||
};
|
||||
};
|
||||
|
||||
const logInViaLinkActionSchema = z.object({
|
||||
code: z.string(),
|
||||
});
|
||||
|
||||
export const logInViaLinkLoader: LoaderFunction = async ({ request }) => {
|
||||
const data = parseSearchParams({
|
||||
request,
|
||||
schema: logInViaLinkActionSchema,
|
||||
});
|
||||
const user = await getUserId(request);
|
||||
|
||||
if (user) {
|
||||
throw redirect("/");
|
||||
}
|
||||
|
||||
const userId = userIdByLogInLinkCode(data.code);
|
||||
if (!userId) {
|
||||
throw new Response("Invalid log in link", { status: 400 });
|
||||
}
|
||||
|
||||
const session = await authSessionStorage.getSession(
|
||||
request.headers.get("Cookie"),
|
||||
);
|
||||
|
||||
session.set(SESSION_KEY, userId);
|
||||
|
||||
deleteLogInLinkByCode(data.code);
|
||||
|
||||
throw redirect("/", {
|
||||
headers: { "Set-Cookie": await authSessionStorage.commitSession(session) },
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user