feat(frontend): Allow glitch-soc users to login to their account

This commit is contained in:
Jesse Wierzbinski 2024-04-15 13:20:39 -10:00
parent de60f37393
commit 866cd4345d
No known key found for this signature in database
4 changed files with 206 additions and 65 deletions

View file

@ -2,6 +2,8 @@ import { join } from "node:path";
import { config } from "config-manager"; import { config } from "config-manager";
import type { LogManager, MultiLogManager } from "~packages/log-manager"; import type { LogManager, MultiLogManager } from "~packages/log-manager";
import { languages } from "./glitch-languages"; import { languages } from "./glitch-languages";
import { redirect } from "@response";
import { retrieveUserFromToken, userToAPI } from "~database/entities/User";
export const handleGlitchRequest = async ( export const handleGlitchRequest = async (
req: Request, req: Request,
@ -9,6 +11,10 @@ export const handleGlitchRequest = async (
): Promise<Response | null> => { ): Promise<Response | null> => {
const url = new URL(req.url); const url = new URL(req.url);
let path = url.pathname; let path = url.pathname;
const accessToken = req.headers
.get("Cookie")
?.match(/_mastodon_session=(.*?)(;|$)/)?.[1];
const user = await retrieveUserFromToken(accessToken ?? "");
// Strip leading /web from path // Strip leading /web from path
if (path.startsWith("/web")) path = path.slice(4); if (path.startsWith("/web")) path = path.slice(4);
@ -103,6 +109,13 @@ export const handleGlitchRequest = async (
}); });
} }
if (path === "/auth/sign_in") {
if (req.method === "POST") {
return redirect("/api/auth/mastodon-login", 307);
}
path = "/auth/sign_in.html";
}
// Redirect / to /index.html // Redirect / to /index.html
if (path === "/" || path === "") path = "/index.html"; if (path === "/" || path === "") path = "/index.html";
// If path doesn't have an extension (e.g. /about), serve index.html // If path doesn't have an extension (e.g. /about), serve index.html
@ -115,11 +128,36 @@ export const handleGlitchRequest = async (
if (await file.exists()) { if (await file.exists()) {
let fileContents = await file.text(); let fileContents = await file.text();
if (path === "/auth/sign_in.html" && url.searchParams.get("error")) {
// Insert error message as first child of form.form_container
const rewriter = new HTMLRewriter()
.on("div.form-container", {
element(element) {
element.prepend(
` <div class='flash-message alert'>
<strong>${decodeURIComponent(
url.searchParams.get("error") ?? "",
)}</strong>
</div>`,
{
html: true,
},
);
},
})
.transform(new Response(fileContents));
fileContents = await rewriter.text();
}
for (const server of config.frontend.glitch.server) { for (const server of config.frontend.glitch.server) {
fileContents = fileContents.replace( fileContents = fileContents.replaceAll(
`${new URL(server).origin}/`, `${new URL(server).origin}/`,
"/", "/",
); );
fileContents = fileContents.replaceAll(
new URL(server).host,
new URL(config.http.base_url).host,
);
} }
fileContents = fileContents.replaceAll( fileContents = fileContents.replaceAll(
@ -161,7 +199,7 @@ export const handleGlitchRequest = async (
element.setInnerContent( element.setInnerContent(
JSON.stringify({ JSON.stringify({
meta: { meta: {
access_token: null, access_token: accessToken || null,
activity_api_enabled: true, activity_api_enabled: true,
admin: null, admin: null,
domain: new URL(config.http.base_url).host, domain: new URL(config.http.base_url).host,
@ -177,7 +215,9 @@ export const handleGlitchRequest = async (
"https://github.com/lysand-org/lysand", "https://github.com/lysand-org/lysand",
sso_redirect: null, sso_redirect: null,
status_page_url: null, status_page_url: null,
streaming_api_base_url: null, streaming_api_base_url: `wss://${
new URL(config.http.base_url).host
}`,
timeline_preview: true, timeline_preview: true,
title: config.instance.name, title: config.instance.name,
trends_as_landing_page: false, trends_as_landing_page: false,
@ -187,9 +227,24 @@ export const handleGlitchRequest = async (
display_media: null, display_media: null,
reduce_motion: null, reduce_motion: null,
use_blurhash: null, use_blurhash: null,
me: user ? user.id : undefined,
}, },
compose: { text: "" }, compose: user
accounts: {}, ? {
text: "",
me: user.id,
default_privacy: "public",
default_sensitive: false,
default_language: "en",
}
: {
text: "",
},
accounts: user
? {
[user.id]: userToAPI(user, true),
}
: {},
media_attachments: { media_attachments: {
accept_content_types: accept_content_types:
config.validation.allowed_mime_types, config.validation.allowed_mime_types,

View file

@ -35,6 +35,7 @@ export const rawRoutes = {
"/api/v2/media": "./server/api/api/v2/media/index", "/api/v2/media": "./server/api/api/v2/media/index",
"/api/v2/search": "./server/api/api/v2/search/index", "/api/v2/search": "./server/api/api/v2/search/index",
"/api/auth/login": "./server/api/api/auth/login/index", "/api/auth/login": "./server/api/api/auth/login/index",
"/api/auth/mastodon-login": "./server/api/api/auth/mastodon-login/index",
"/api/auth/redirect": "./server/api/api/auth/redirect/index", "/api/auth/redirect": "./server/api/api/auth/redirect/index",
"/nodeinfo/2.0": "./server/api/nodeinfo/2.0/index", "/nodeinfo/2.0": "./server/api/nodeinfo/2.0/index",
"/oauth/authorize-external": "./server/api/oauth/authorize-external/index", "/oauth/authorize-external": "./server/api/oauth/authorize-external/index",

View file

@ -4,6 +4,7 @@ import { TokenType } from "~database/entities/Token";
import { findFirstUser } from "~database/entities/User"; import { findFirstUser } from "~database/entities/User";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { token } from "~drizzle/schema"; import { token } from "~drizzle/schema";
import { z } from "zod";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -17,13 +18,16 @@ export const meta = applyConfig({
}, },
}); });
export const schema = z.object({
email: z.string().email(),
password: z.string().max(100).min(3),
});
/** /**
* OAuth Code flow * OAuth Code flow
*/ */
export default apiRoute<{ export default apiRoute<typeof meta, typeof schema>(
email: string; async (req, matchedRoute, extraData) => {
password: string;
}>(async (req, matchedRoute, extraData) => {
const scopes = (matchedRoute.query.scope || "") const scopes = (matchedRoute.query.scope || "")
.replaceAll("+", " ") .replaceAll("+", " ")
.split(" "); .split(" ");
@ -52,7 +56,10 @@ export default apiRoute<{
where: (user, { eq }) => eq(user.email, email), where: (user, { eq }) => eq(user.email, email),
}); });
if (!user || !(await Bun.password.verify(password, user.password || ""))) if (
!user ||
!(await Bun.password.verify(password, user.password || ""))
)
return redirectToLogin("Invalid username or password"); return redirectToLogin("Invalid username or password");
const application = await db.query.application.findFirst({ const application = await db.query.application.findFirst({
@ -84,4 +91,5 @@ export default apiRoute<{
}).toString()}`, }).toString()}`,
302, 302,
); );
}); },
);

View file

@ -0,0 +1,77 @@
import { randomBytes } from "node:crypto";
import { apiRoute, applyConfig } from "@api";
import { TokenType } from "~database/entities/Token";
import { findFirstUser } from "~database/entities/User";
import { db } from "~drizzle/db";
import { token } from "~drizzle/schema";
import { z } from "zod";
import { config } from "~packages/config-manager";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 4,
duration: 60,
},
route: "/api/auth/login",
auth: {
required: false,
},
});
export const schema = z.object({
"user[email]": z.string().email(),
"user[password]": z.string().max(100).min(3),
});
/**
* Mastodon-FE login route
*/
export default apiRoute<typeof meta, typeof schema>(
async (req, matchedRoute, extraData) => {
const { "user[email]": email, "user[password]": password } =
extraData.parsedRequest;
const redirectToLogin = (error: string) =>
Response.redirect(
`/auth/sign_in?${new URLSearchParams({
...matchedRoute.query,
error: encodeURIComponent(error),
}).toString()}`,
302,
);
const user = await findFirstUser({
where: (user, { eq }) => eq(user.email, email),
});
if (
!user ||
!(await Bun.password.verify(password, user.password || ""))
)
return redirectToLogin("Invalid email or password");
const code = randomBytes(32).toString("hex");
const accessToken = randomBytes(64).toString("base64url");
await db.insert(token).values({
accessToken,
code: code,
scope: "read write follow push",
tokenType: TokenType.BEARER,
applicationId: null,
userId: user.id,
});
// Redirect to home
return new Response(null, {
headers: {
Location: "/",
"Set-Cookie": `_mastodon_session=${accessToken}; Domain=${
new URL(config.http.base_url).hostname
}; SameSite=Lax; Path=/; HttpOnly`,
},
status: 303,
});
},
);