mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 22:09:16 +01:00
Replace eslint and prettier with Biome
This commit is contained in:
parent
4a5a2ea590
commit
af0d627f19
199 changed files with 16493 additions and 16361 deletions
|
|
@ -1,95 +1,91 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { oauthRedirectUri } from "@constants";
|
||||
import {
|
||||
calculatePKCECodeChallenge,
|
||||
discoveryRequest,
|
||||
generateRandomCodeVerifier,
|
||||
processDiscoveryResponse,
|
||||
calculatePKCECodeChallenge,
|
||||
discoveryRequest,
|
||||
generateRandomCodeVerifier,
|
||||
processDiscoveryResponse,
|
||||
} from "oauth4webapi";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 20,
|
||||
},
|
||||
route: "/oauth/authorize-external",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 20,
|
||||
},
|
||||
route: "/oauth/authorize-external",
|
||||
});
|
||||
|
||||
/**
|
||||
* Redirects the user to the external OAuth provider
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
const issuerId = matchedRoute.query.issuer;
|
||||
const issuerId = matchedRoute.query.issuer;
|
||||
|
||||
// This is the Lysand client's client_id, not the external OAuth provider's client_id
|
||||
const clientId = matchedRoute.query.clientId;
|
||||
// This is the Lysand client's client_id, not the external OAuth provider's client_id
|
||||
const clientId = matchedRoute.query.clientId;
|
||||
|
||||
if (!clientId || clientId === "undefined") {
|
||||
return redirectToLogin("Missing client_id");
|
||||
}
|
||||
if (!clientId || clientId === "undefined") {
|
||||
return redirectToLogin("Missing client_id");
|
||||
}
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
const issuer = config.oidc.providers.find(
|
||||
provider => provider.id === issuerId
|
||||
);
|
||||
const issuer = config.oidc.providers.find(
|
||||
(provider) => provider.id === issuerId,
|
||||
);
|
||||
|
||||
if (!issuer) {
|
||||
return redirectToLogin("Invalid issuer");
|
||||
}
|
||||
if (!issuer) {
|
||||
return redirectToLogin("Invalid issuer");
|
||||
}
|
||||
|
||||
const issuerUrl = new URL(issuer.url);
|
||||
const issuerUrl = new URL(issuer.url);
|
||||
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then(res => processDiscoveryResponse(issuerUrl, res));
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
||||
|
||||
const codeVerifier = generateRandomCodeVerifier();
|
||||
const codeVerifier = generateRandomCodeVerifier();
|
||||
|
||||
// Store into database
|
||||
// Store into database
|
||||
|
||||
const newFlow = await client.openIdLoginFlow.create({
|
||||
data: {
|
||||
codeVerifier,
|
||||
application: {
|
||||
connect: {
|
||||
client_id: clientId,
|
||||
},
|
||||
},
|
||||
issuerId,
|
||||
},
|
||||
});
|
||||
const newFlow = await client.openIdLoginFlow.create({
|
||||
data: {
|
||||
codeVerifier,
|
||||
application: {
|
||||
connect: {
|
||||
client_id: clientId,
|
||||
},
|
||||
},
|
||||
issuerId,
|
||||
},
|
||||
});
|
||||
|
||||
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||
|
||||
return Response.redirect(
|
||||
authServer.authorization_endpoint +
|
||||
"?" +
|
||||
new URLSearchParams({
|
||||
client_id: issuer.client_id,
|
||||
redirect_uri:
|
||||
oauthRedirectUri(issuerId) + `?flow=${newFlow.id}`,
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
// PKCE
|
||||
code_challenge_method: "S256",
|
||||
code_challenge: codeChallenge,
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
return Response.redirect(
|
||||
`${authServer.authorization_endpoint}?${new URLSearchParams({
|
||||
client_id: issuer.client_id,
|
||||
redirect_uri: `${oauthRedirectUri(issuerId)}?flow=${newFlow.id}`,
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
// PKCE
|
||||
code_challenge_method: "S256",
|
||||
code_challenge: codeChallenge,
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,198 +1,196 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { oauthRedirectUri } from "@constants";
|
||||
import { randomBytes } from "crypto";
|
||||
import {
|
||||
authorizationCodeGrantRequest,
|
||||
discoveryRequest,
|
||||
expectNoState,
|
||||
isOAuth2Error,
|
||||
processDiscoveryResponse,
|
||||
validateAuthResponse,
|
||||
userInfoRequest,
|
||||
processAuthorizationCodeOpenIDResponse,
|
||||
processUserInfoResponse,
|
||||
getValidatedIdTokenClaims,
|
||||
authorizationCodeGrantRequest,
|
||||
discoveryRequest,
|
||||
expectNoState,
|
||||
getValidatedIdTokenClaims,
|
||||
isOAuth2Error,
|
||||
processAuthorizationCodeOpenIDResponse,
|
||||
processDiscoveryResponse,
|
||||
processUserInfoResponse,
|
||||
userInfoRequest,
|
||||
validateAuthResponse,
|
||||
} from "oauth4webapi";
|
||||
import { client } from "~database/datasource";
|
||||
import { TokenType } from "~database/entities/Token";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 20,
|
||||
},
|
||||
route: "/oauth/callback/:issuer",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 20,
|
||||
},
|
||||
route: "/oauth/callback/:issuer",
|
||||
});
|
||||
|
||||
/**
|
||||
* Redirects the user to the external OAuth provider
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
client_id: matchedRoute.query.clientId,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
client_id: matchedRoute.query.clientId,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
const currentUrl = new URL(req.url);
|
||||
const currentUrl = new URL(req.url);
|
||||
|
||||
// Remove state query parameter from URL
|
||||
currentUrl.searchParams.delete("state");
|
||||
const issuerParam = matchedRoute.params.issuer;
|
||||
const flow = await client.openIdLoginFlow.findFirst({
|
||||
where: {
|
||||
id: matchedRoute.query.flow,
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
// Remove state query parameter from URL
|
||||
currentUrl.searchParams.delete("state");
|
||||
const issuerParam = matchedRoute.params.issuer;
|
||||
const flow = await client.openIdLoginFlow.findFirst({
|
||||
where: {
|
||||
id: matchedRoute.query.flow,
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!flow) {
|
||||
return redirectToLogin("Invalid flow");
|
||||
}
|
||||
if (!flow) {
|
||||
return redirectToLogin("Invalid flow");
|
||||
}
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
const issuer = config.oidc.providers.find(
|
||||
provider => provider.id === issuerParam
|
||||
);
|
||||
const issuer = config.oidc.providers.find(
|
||||
(provider) => provider.id === issuerParam,
|
||||
);
|
||||
|
||||
if (!issuer) {
|
||||
return redirectToLogin("Invalid issuer");
|
||||
}
|
||||
if (!issuer) {
|
||||
return redirectToLogin("Invalid issuer");
|
||||
}
|
||||
|
||||
const issuerUrl = new URL(issuer.url);
|
||||
const issuerUrl = new URL(issuer.url);
|
||||
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then(res => processDiscoveryResponse(issuerUrl, res));
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
||||
|
||||
const parameters = validateAuthResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
currentUrl,
|
||||
// Whether to expect state or not
|
||||
expectNoState
|
||||
);
|
||||
const parameters = validateAuthResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
currentUrl,
|
||||
// Whether to expect state or not
|
||||
expectNoState,
|
||||
);
|
||||
|
||||
if (isOAuth2Error(parameters)) {
|
||||
return redirectToLogin(
|
||||
parameters.error_description || parameters.error
|
||||
);
|
||||
}
|
||||
if (isOAuth2Error(parameters)) {
|
||||
return redirectToLogin(
|
||||
parameters.error_description || parameters.error,
|
||||
);
|
||||
}
|
||||
|
||||
const response = await authorizationCodeGrantRequest(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
parameters,
|
||||
oauthRedirectUri(issuerParam) + `?flow=${flow.id}`,
|
||||
flow.codeVerifier
|
||||
);
|
||||
const response = await authorizationCodeGrantRequest(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
parameters,
|
||||
`${oauthRedirectUri(issuerParam)}?flow=${flow.id}`,
|
||||
flow.codeVerifier,
|
||||
);
|
||||
|
||||
const result = await processAuthorizationCodeOpenIDResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
response
|
||||
);
|
||||
const result = await processAuthorizationCodeOpenIDResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
response,
|
||||
);
|
||||
|
||||
if (isOAuth2Error(result)) {
|
||||
return redirectToLogin(result.error_description || result.error);
|
||||
}
|
||||
if (isOAuth2Error(result)) {
|
||||
return redirectToLogin(result.error_description || result.error);
|
||||
}
|
||||
|
||||
const { access_token } = result;
|
||||
const { access_token } = result;
|
||||
|
||||
const claims = getValidatedIdTokenClaims(result);
|
||||
const { sub } = claims;
|
||||
const claims = getValidatedIdTokenClaims(result);
|
||||
const { sub } = claims;
|
||||
|
||||
// Validate `sub`
|
||||
// Later, we'll use this to automatically set the user's data
|
||||
await userInfoRequest(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
access_token
|
||||
).then(res =>
|
||||
processUserInfoResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
sub,
|
||||
res
|
||||
)
|
||||
);
|
||||
// Validate `sub`
|
||||
// Later, we'll use this to automatically set the user's data
|
||||
await userInfoRequest(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
access_token,
|
||||
).then((res) =>
|
||||
processUserInfoResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
sub,
|
||||
res,
|
||||
),
|
||||
);
|
||||
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
linkedOpenIdAccounts: {
|
||||
some: {
|
||||
serverId: sub,
|
||||
issuerId: issuer.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
linkedOpenIdAccounts: {
|
||||
some: {
|
||||
serverId: sub,
|
||||
issuerId: issuer.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return redirectToLogin("No user found with that account");
|
||||
}
|
||||
if (!user) {
|
||||
return redirectToLogin("No user found with that account");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!flow.application) return redirectToLogin("Invalid client_id");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!flow.application) return redirectToLogin("Invalid client_id");
|
||||
|
||||
const code = randomBytes(32).toString("hex");
|
||||
const code = randomBytes(32).toString("hex");
|
||||
|
||||
await client.application.update({
|
||||
where: { id: flow.application.id },
|
||||
data: {
|
||||
tokens: {
|
||||
create: {
|
||||
access_token: randomBytes(64).toString("base64url"),
|
||||
code: code,
|
||||
scope: flow.application.scopes,
|
||||
token_type: TokenType.BEARER,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.application.update({
|
||||
where: { id: flow.application.id },
|
||||
data: {
|
||||
tokens: {
|
||||
create: {
|
||||
access_token: randomBytes(64).toString("base64url"),
|
||||
code: code,
|
||||
scope: flow.application.scopes,
|
||||
token_type: TokenType.BEARER,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Redirect back to application
|
||||
return Response.redirect(
|
||||
`/oauth/redirect?` +
|
||||
new URLSearchParams({
|
||||
redirect_uri: flow.application.redirect_uris,
|
||||
code,
|
||||
client_id: flow.application.client_id,
|
||||
application: flow.application.name,
|
||||
website: flow.application.website ?? "",
|
||||
scope: flow.application.scopes,
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
// Redirect back to application
|
||||
return Response.redirect(
|
||||
`/oauth/redirect?${new URLSearchParams({
|
||||
redirect_uri: flow.application.redirect_uris,
|
||||
code,
|
||||
client_id: flow.application.client_id,
|
||||
application: flow.application.name,
|
||||
website: flow.application.website ?? "",
|
||||
scope: flow.application.scopes,
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,28 +2,28 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { jsonResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 10,
|
||||
},
|
||||
route: "/oauth/providers",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 10,
|
||||
},
|
||||
route: "/oauth/providers",
|
||||
});
|
||||
|
||||
/**
|
||||
* Lists available OAuth providers
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
return jsonResponse(
|
||||
config.oidc.providers.map(p => ({
|
||||
name: p.name,
|
||||
icon: p.icon,
|
||||
id: p.id,
|
||||
}))
|
||||
);
|
||||
return jsonResponse(
|
||||
config.oidc.providers.map((p) => ({
|
||||
name: p.name,
|
||||
icon: p.icon,
|
||||
id: p.id,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,61 +3,61 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 10,
|
||||
},
|
||||
route: "/oauth/token",
|
||||
allowedMethods: ["POST"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 10,
|
||||
},
|
||||
route: "/oauth/token",
|
||||
});
|
||||
|
||||
/**
|
||||
* Allows getting token from OAuth code
|
||||
*/
|
||||
export default apiRoute<{
|
||||
grant_type: string;
|
||||
code: string;
|
||||
redirect_uri: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
scope: string;
|
||||
grant_type: string;
|
||||
code: string;
|
||||
redirect_uri: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
scope: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { grant_type, code, redirect_uri, client_id, client_secret, scope } =
|
||||
extraData.parsedRequest;
|
||||
const { grant_type, code, redirect_uri, client_id, client_secret, scope } =
|
||||
extraData.parsedRequest;
|
||||
|
||||
if (grant_type !== "authorization_code")
|
||||
return errorResponse(
|
||||
"Invalid grant type (try 'authorization_code')",
|
||||
400
|
||||
);
|
||||
if (grant_type !== "authorization_code")
|
||||
return errorResponse(
|
||||
"Invalid grant type (try 'authorization_code')",
|
||||
400,
|
||||
);
|
||||
|
||||
// Get associated token
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
code,
|
||||
application: {
|
||||
client_id,
|
||||
secret: client_secret,
|
||||
redirect_uris: redirect_uri,
|
||||
scopes: scope?.replaceAll("+", " "),
|
||||
},
|
||||
scope: scope?.replaceAll("+", " "),
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
// Get associated token
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
code,
|
||||
application: {
|
||||
client_id,
|
||||
secret: client_secret,
|
||||
redirect_uris: redirect_uri,
|
||||
scopes: scope?.replaceAll("+", " "),
|
||||
},
|
||||
scope: scope?.replaceAll("+", " "),
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!token)
|
||||
return errorResponse("Invalid access token or client credentials", 401);
|
||||
if (!token)
|
||||
return errorResponse("Invalid access token or client credentials", 401);
|
||||
|
||||
return jsonResponse({
|
||||
access_token: token.access_token,
|
||||
token_type: token.token_type,
|
||||
scope: token.scope,
|
||||
created_at: token.created_at,
|
||||
});
|
||||
return jsonResponse({
|
||||
access_token: token.access_token,
|
||||
token_type: token.token_type,
|
||||
scope: token.scope,
|
||||
created_at: token.created_at,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue