mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
Various bug fixes (#102)
* feat: set lang in html head * fix: add # in front of git ref * fix: remove unused vars from example env * fix: package name and license field * fix: enable sourcemap for client and server * fix: emojis not showing in prod this is extremely cursed, but it works * chore: refactor auth manager * feat: disable invitations if simple auth disabled * feat: add drop version to footer * feat: translate auth endpoints * chore: move oidc module * feat: add weekly tasks enabled object cleanup as weekly task * feat: add timestamp to task log msgs * feat: add guard to prevent invalid progress % * fix: add missing global scope to i18n components * feat: set base url for i18n * feat: switch task log to json format * ci: run ci on develop branch only * fix: UserWidget text not updating #109 * fix: EXTERNAL_URL being computed at build * feat: add basic language outlines for translation * feat: add more english dialects
This commit is contained in:
@ -1,11 +1,13 @@
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const allowed = await aclManager.allowSystemACL(h3, ["auth:read"]);
|
||||
if (!allowed) throw createError({ statusCode: 403 });
|
||||
|
||||
const enabledAuthManagers = authManager.getAuthProviders();
|
||||
|
||||
const authData = {
|
||||
[AuthMec.Simple]: enabledAuthManagers.Simple,
|
||||
[AuthMec.OpenID]:
|
||||
|
||||
@ -30,6 +30,7 @@ export default defineEventHandler(async (h3) => {
|
||||
take: 10,
|
||||
});
|
||||
const dailyTasks = await taskHandler.dailyTasks();
|
||||
const weeklyTasks = await taskHandler.weeklyTasks();
|
||||
|
||||
return { runningTasks, historicalTasks, dailyTasks };
|
||||
return { runningTasks, historicalTasks, dailyTasks, weeklyTasks };
|
||||
});
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
const authManagers = Object.entries(enabledAuthManagers)
|
||||
.filter((e) => !!e[1])
|
||||
.map((e) => e[0]);
|
||||
|
||||
return authManagers;
|
||||
return authManager.getEnabledAuthProviders();
|
||||
});
|
||||
|
||||
@ -2,12 +2,11 @@ import { AuthMec } from "~/prisma/client";
|
||||
import type { JsonArray } from "@prisma/client/runtime/library";
|
||||
import { type } from "arktype";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import {
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
import authManager, {
|
||||
checkHashArgon2,
|
||||
checkHashBcrypt,
|
||||
} from "~/server/internal/security/simple";
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
} from "~/server/internal/auth";
|
||||
|
||||
const signinValidator = type({
|
||||
username: "string",
|
||||
@ -18,10 +17,12 @@ const signinValidator = type({
|
||||
export default defineEventHandler<{
|
||||
body: typeof signinValidator.infer;
|
||||
}>(async (h3) => {
|
||||
if (!enabledAuthManagers.Simple)
|
||||
const t = await useTranslation(h3);
|
||||
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Sign in method not enabled",
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const body = signinValidator(await readBody(h3));
|
||||
@ -55,14 +56,13 @@ export default defineEventHandler<{
|
||||
if (!authMek)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid username or password.",
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
if (!authMek.user.enabled)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage:
|
||||
"Invalid or disabled account. Please contact the server administrator.",
|
||||
statusMessage: t("errors.auth.disabled"),
|
||||
});
|
||||
|
||||
// LEGACY bcrypt
|
||||
@ -72,15 +72,14 @@ export default defineEventHandler<{
|
||||
|
||||
if (!hash)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage:
|
||||
"Invalid password state. Please contact the server administrator.",
|
||||
statusCode: 500,
|
||||
statusMessage: t("errors.auth.invalidPassState"),
|
||||
});
|
||||
|
||||
if (!(await checkHashBcrypt(body.password, hash)))
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid username or password.",
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
// TODO: send user to forgot password screen or something to force them to change their password to new system
|
||||
@ -93,14 +92,13 @@ export default defineEventHandler<{
|
||||
if (!hash || typeof hash !== "string")
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage:
|
||||
"Invalid password state. Please contact the server administrator.",
|
||||
statusMessage: t("errors.auth.invalidPassState"),
|
||||
});
|
||||
|
||||
if (!(await checkHashArgon2(body.password, hash)))
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid username or password.",
|
||||
statusMessage: t("errors.auth.invalidUserOrPass"),
|
||||
});
|
||||
|
||||
await sessionHandler.signin(h3, authMek.userId, body.rememberMe);
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import taskHandler from "~/server/internal/tasks";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const t = await useTranslation(h3);
|
||||
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const query = getQuery(h3);
|
||||
const id = query.id?.toString();
|
||||
if (!id)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "id required in fetching invitation",
|
||||
statusMessage: t("errors.auth.inviteIdRequired"),
|
||||
});
|
||||
taskHandler.runTaskGroupByName("cleanup:invitations");
|
||||
|
||||
@ -15,7 +24,7 @@ export default defineEventHandler(async (h3) => {
|
||||
if (!invitation)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Invalid or expired invitation",
|
||||
statusMessage: t("errors.auth.invalidInvite"),
|
||||
});
|
||||
|
||||
return invitation;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { createHashArgon2 } from "~/server/internal/security/simple";
|
||||
import authManager, { createHashArgon2 } from "~/server/internal/auth";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import objectHandler from "~/server/internal/objects";
|
||||
import { type } from "arktype";
|
||||
@ -18,13 +18,21 @@ export const CreateUserValidator = type({
|
||||
export default defineEventHandler<{
|
||||
body: typeof CreateUserValidator.infer;
|
||||
}>(async (h3) => {
|
||||
const t = await useTranslation(h3);
|
||||
|
||||
if (!authManager.getAuthProviders().Simple)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: t("errors.auth.method.signinDisabled"),
|
||||
});
|
||||
|
||||
const user = await readValidatedBody(h3, CreateUserValidator);
|
||||
|
||||
const invitationId = user.invitation;
|
||||
if (!invitationId)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid or expired invitation.",
|
||||
statusMessage: t("errors.auth.invalidInvite"),
|
||||
});
|
||||
|
||||
const invitation = await prisma.invitation.findUnique({
|
||||
@ -33,7 +41,7 @@ export default defineEventHandler<{
|
||||
if (!invitation)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid or expired invitation.",
|
||||
statusMessage: t("errors.auth.invalidInvite"),
|
||||
});
|
||||
|
||||
// reuse items from invite
|
||||
@ -46,7 +54,7 @@ export default defineEventHandler<{
|
||||
if (existing > 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Username already taken.",
|
||||
statusMessage: t("errors.auth.usernameTaken"),
|
||||
});
|
||||
|
||||
const userId = randomUUID();
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
import path from "path";
|
||||
import module from "module";
|
||||
import fs from "fs/promises";
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
import aclManager from "~/server/internal/acls";
|
||||
|
||||
const twemojiJson = module.findPackageJSON(
|
||||
"@discordapp/twemoji",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserIdACL(h3, ["object:read"]);
|
||||
if (!userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
});
|
||||
|
||||
if (!twemojiJson)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to resolve emoji package",
|
||||
});
|
||||
|
||||
const unsafeId = getRouterParam(h3, "id");
|
||||
if (!unsafeId)
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid ID" });
|
||||
|
||||
const svgPath = path.join(
|
||||
path.dirname(twemojiJson),
|
||||
"dist",
|
||||
"svg",
|
||||
sanitize(unsafeId),
|
||||
);
|
||||
|
||||
setHeader(
|
||||
h3,
|
||||
"Cache-Control",
|
||||
// 7 days
|
||||
"public, max-age=604800, s-maxage=604800",
|
||||
);
|
||||
setHeader(h3, "Content-Type", "image/svg+xml");
|
||||
return await fs.readFile(svgPath);
|
||||
});
|
||||
@ -4,6 +4,6 @@ export default defineEventHandler((_h3) => {
|
||||
return {
|
||||
appName: "Drop",
|
||||
version: systemConfig.getDropVersion(),
|
||||
ref: systemConfig.getGitRef(),
|
||||
gitRef: `#${systemConfig.getGitRef()}`,
|
||||
};
|
||||
});
|
||||
|
||||
62
server/internal/auth/index.ts
Normal file
62
server/internal/auth/index.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import { OIDCManager } from "./oidc";
|
||||
|
||||
class AuthManager {
|
||||
private authProviders: {
|
||||
[AuthMec.Simple]: boolean;
|
||||
[AuthMec.OpenID]: OIDCManager | undefined;
|
||||
} = {
|
||||
[AuthMec.Simple]: false,
|
||||
[AuthMec.OpenID]: undefined,
|
||||
};
|
||||
|
||||
private initFuncs: {
|
||||
[K in keyof typeof this.authProviders]: () => Promise<unknown>;
|
||||
} = {
|
||||
[AuthMec.OpenID]: OIDCManager.prototype.create,
|
||||
[AuthMec.Simple]: async () => {
|
||||
const disabled = process.env.DISABLE_SIMPLE_AUTH as string | undefined;
|
||||
return !disabled;
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
console.log("AuthManager initialized");
|
||||
}
|
||||
|
||||
async init() {
|
||||
for (const [key, init] of Object.entries(this.initFuncs)) {
|
||||
try {
|
||||
const object = await init();
|
||||
if (!object) break;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(this.authProviders as any)[key] = object;
|
||||
console.log(`enabled auth: ${key}`);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add every other auth mechanism here, and fall back to simple if none of them are enabled
|
||||
if (!this.authProviders[AuthMec.OpenID]) {
|
||||
this.authProviders[AuthMec.Simple] = true;
|
||||
}
|
||||
}
|
||||
|
||||
getAuthProviders() {
|
||||
return this.authProviders;
|
||||
}
|
||||
|
||||
getEnabledAuthProviders() {
|
||||
const authManagers = Object.entries(this.authProviders)
|
||||
.filter((e) => !!e[1])
|
||||
.map((e) => e[0]);
|
||||
|
||||
return authManagers;
|
||||
}
|
||||
}
|
||||
|
||||
const authManager = new AuthManager();
|
||||
export default authManager;
|
||||
|
||||
export * from "./passwordHash";
|
||||
@ -1,10 +1,11 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import prisma from "../db/database";
|
||||
import prisma from "../../db/database";
|
||||
import type { User } from "~/prisma/client";
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import objectHandler from "../objects";
|
||||
import objectHandler from "../../objects";
|
||||
import type { Readable } from "stream";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import { systemConfig } from "../../config/sys-conf";
|
||||
|
||||
interface OIDCWellKnown {
|
||||
authorization_endpoint: string;
|
||||
@ -118,7 +119,7 @@ export class OIDCManager {
|
||||
|
||||
const clientId = process.env.OIDC_CLIENT_ID as string | undefined;
|
||||
const clientSecret = process.env.OIDC_CLIENT_SECRET as string | undefined;
|
||||
const externalUrl = process.env.EXTERNAL_URL as string | undefined;
|
||||
const externalUrl = systemConfig.getExternalUrl();
|
||||
|
||||
if (!clientId || !clientSecret)
|
||||
throw new Error("Missing client ID or secret for OIDC");
|
||||
@ -2,6 +2,7 @@ class SystemConfig {
|
||||
private libraryFolder = process.env.LIBRARY ?? "./.data/library";
|
||||
private dataFolder = process.env.DATA ?? "./.data/data";
|
||||
|
||||
private externalUrl = process.env.EXTERNAL_URL ?? "http://localhost:3000";
|
||||
private dropVersion;
|
||||
private gitRef;
|
||||
|
||||
@ -33,6 +34,10 @@ class SystemConfig {
|
||||
shouldCheckForUpdates() {
|
||||
return this.checkForUpdates;
|
||||
}
|
||||
|
||||
getExternalUrl() {
|
||||
return this.externalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export const systemConfig = new SystemConfig();
|
||||
|
||||
@ -9,6 +9,7 @@ import checkUpdate from "./registry/update";
|
||||
import cleanupObjects from "./registry/objects";
|
||||
import { taskGroups, type TaskGroup } from "./group";
|
||||
import prisma from "../db/database";
|
||||
import { type } from "arktype";
|
||||
|
||||
// a task that has been run
|
||||
type FinishedTask = {
|
||||
@ -45,11 +46,12 @@ class TaskHandler {
|
||||
// list of all clients currently connected to tasks
|
||||
private clientRegistry = new Map<string, PeerImpl>();
|
||||
|
||||
private scheduledTasks: TaskGroup[] = [
|
||||
private dailyScheduledTasks: TaskGroup[] = [
|
||||
"cleanup:invitations",
|
||||
"cleanup:sessions",
|
||||
"check:update",
|
||||
];
|
||||
private weeklyScheduledTasks: TaskGroup[] = ["cleanup:objects"];
|
||||
|
||||
constructor() {
|
||||
// register the cleanup invitations task
|
||||
@ -124,18 +126,22 @@ class TaskHandler {
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const progress = (progress: number) => {
|
||||
const taskEntry = this.taskPool.get(task.id);
|
||||
if (!taskEntry) return;
|
||||
taskEntry.progress = progress;
|
||||
updateAllClients();
|
||||
};
|
||||
|
||||
const log = (entry: string) => {
|
||||
const taskEntry = this.taskPool.get(task.id);
|
||||
if (!taskEntry) return;
|
||||
taskEntry.log.push(entry);
|
||||
// console.log(`[Task ${task.taskGroup}]: ${entry}`);
|
||||
taskEntry.log.push(msgWithTimestamp(entry));
|
||||
updateAllClients();
|
||||
};
|
||||
|
||||
const progress = (progress: number) => {
|
||||
if (progress < 0 || progress > 100) {
|
||||
console.error("Progress must be between 0 and 100", { progress });
|
||||
return;
|
||||
}
|
||||
const taskEntry = this.taskPool.get(task.id);
|
||||
if (!taskEntry) return;
|
||||
taskEntry.progress = progress;
|
||||
// log(`Progress: ${progress}%`);
|
||||
updateAllClients();
|
||||
};
|
||||
|
||||
@ -288,7 +294,11 @@ class TaskHandler {
|
||||
}
|
||||
|
||||
dailyTasks() {
|
||||
return this.scheduledTasks;
|
||||
return this.dailyScheduledTasks;
|
||||
}
|
||||
|
||||
weeklyTasks() {
|
||||
return this.weeklyScheduledTasks;
|
||||
}
|
||||
|
||||
runTaskGroupByName(name: TaskGroup) {
|
||||
@ -304,7 +314,7 @@ class TaskHandler {
|
||||
* Runs all daily tasks that are scheduled to run once a day.
|
||||
*/
|
||||
async triggerDailyTasks() {
|
||||
for (const taskGroup of this.scheduledTasks) {
|
||||
for (const taskGroup of this.dailyScheduledTasks) {
|
||||
const mostRecent = await prisma.task.findFirst({
|
||||
where: {
|
||||
taskGroup,
|
||||
@ -324,6 +334,32 @@ class TaskHandler {
|
||||
}
|
||||
await this.runTaskGroupByName(taskGroup);
|
||||
}
|
||||
|
||||
// After running daily tasks, trigger weekly tasks as well
|
||||
await this.triggerWeeklyTasks();
|
||||
}
|
||||
|
||||
private async triggerWeeklyTasks() {
|
||||
for (const taskGroup of this.weeklyScheduledTasks) {
|
||||
const mostRecent = await prisma.task.findFirst({
|
||||
where: {
|
||||
taskGroup,
|
||||
},
|
||||
orderBy: {
|
||||
ended: "desc",
|
||||
},
|
||||
});
|
||||
if (mostRecent) {
|
||||
const currentTime = Date.now();
|
||||
const lastRun = mostRecent.ended.getTime();
|
||||
const difference = currentTime - lastRun;
|
||||
if (difference < 1000 * 60 * 60 * 24 * 7) {
|
||||
// If it's been less than one week
|
||||
continue; // skip
|
||||
}
|
||||
}
|
||||
await this.runTaskGroupByName(taskGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,6 +419,37 @@ interface DropTask {
|
||||
build: () => Task;
|
||||
}
|
||||
|
||||
export const TaskLog = type({
|
||||
timestamp: "string",
|
||||
message: "string",
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a log message with a timestamp in the format YYYY-MM-DD HH:mm:ss.SSS UTC
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
function msgWithTimestamp(message: string): string {
|
||||
const now = new Date();
|
||||
|
||||
const pad = (n: number, width = 2) => n.toString().padStart(width, "0");
|
||||
|
||||
const year = now.getUTCFullYear();
|
||||
const month = pad(now.getUTCMonth() + 1);
|
||||
const day = pad(now.getUTCDate());
|
||||
|
||||
const hours = pad(now.getUTCHours());
|
||||
const minutes = pad(now.getUTCMinutes());
|
||||
const seconds = pad(now.getUTCSeconds());
|
||||
const milliseconds = pad(now.getUTCMilliseconds(), 3);
|
||||
|
||||
const log: typeof TaskLog.infer = {
|
||||
timestamp: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds} UTC`,
|
||||
message,
|
||||
};
|
||||
return JSON.stringify(log);
|
||||
}
|
||||
|
||||
export function defineDropTask(buildTask: BuildTask): DropTask {
|
||||
// TODO: only let one task with the same taskGroup run at the same time if specified
|
||||
|
||||
|
||||
@ -1,39 +1,5 @@
|
||||
import { AuthMec } from "~/prisma/client";
|
||||
import { OIDCManager } from "../internal/oidc";
|
||||
|
||||
export const enabledAuthManagers: {
|
||||
[AuthMec.Simple]: boolean;
|
||||
[AuthMec.OpenID]: OIDCManager | undefined;
|
||||
} = {
|
||||
[AuthMec.Simple]: false,
|
||||
[AuthMec.OpenID]: undefined,
|
||||
};
|
||||
|
||||
const initFunctions: {
|
||||
[K in keyof typeof enabledAuthManagers]: () => Promise<unknown>;
|
||||
} = {
|
||||
[AuthMec.OpenID]: OIDCManager.prototype.create,
|
||||
[AuthMec.Simple]: async () => {
|
||||
const disabled = process.env.DISABLE_SIMPLE_AUTH as string | undefined;
|
||||
return !disabled;
|
||||
},
|
||||
};
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
for (const [key, init] of Object.entries(initFunctions)) {
|
||||
try {
|
||||
const object = await init();
|
||||
if (!object) break;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(enabledAuthManagers as any)[key] = object;
|
||||
console.log(`enabled auth: ${key}`);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add every other auth mechanism here, and fall back to simple if none of them are enabled
|
||||
if (!enabledAuthManagers[AuthMec.OpenID]) {
|
||||
enabledAuthManagers[AuthMec.Simple] = true;
|
||||
}
|
||||
await authManager.init();
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
defineRouteMeta({
|
||||
openAPI: {
|
||||
@ -10,6 +10,7 @@ defineRouteMeta({
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const enabledAuthManagers = authManager.getAuthProviders();
|
||||
if (!enabledAuthManagers.OpenID) return sendRedirect(h3, "/auth/signin");
|
||||
|
||||
const manager = enabledAuthManagers.OpenID;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||
import authManager from "~/server/internal/auth";
|
||||
|
||||
defineRouteMeta({
|
||||
openAPI: {
|
||||
@ -11,6 +11,7 @@ defineRouteMeta({
|
||||
export default defineEventHandler((h3) => {
|
||||
const redirect = getQuery(h3).redirect?.toString();
|
||||
|
||||
const enabledAuthManagers = authManager.getAuthProviders();
|
||||
if (!enabledAuthManagers.OpenID)
|
||||
return sendRedirect(
|
||||
h3,
|
||||
|
||||
Reference in New Issue
Block a user