rename auth and start work separating config

This commit is contained in:
Milo Schwartz 2025-01-01 16:40:01 -05:00
parent d447de9e8a
commit b199595100
No known key found for this signature in database
15 changed files with 153 additions and 120 deletions

View file

@ -12,12 +12,12 @@ import { eq } from "drizzle-orm";
import config from "@server/config"; import config from "@server/config";
import type { RandomReader } from "@oslojs/crypto/random"; import type { RandomReader } from "@oslojs/crypto/random";
import { generateRandomString } from "@oslojs/crypto/random"; import { generateRandomString } from "@oslojs/crypto/random";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export const SESSION_COOKIE_NAME = config.server.session_cookie_name; export const SESSION_COOKIE_NAME = config.server.session_cookie_name;
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30; export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
export const SECURE_COOKIES = config.server.secure_cookies; export const SECURE_COOKIES = config.server.secure_cookies;
export const COOKIE_DOMAIN = export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url);
"." + new URL(config.app.base_url).hostname.split(".").slice(-2).join(".");
export function generateSessionToken(): string { export function generateSessionToken(): string {
const bytes = new Uint8Array(20); const bytes = new Uint8Array(20);

View file

@ -9,12 +9,12 @@ import { Newt, newts, newtSessions, NewtSession } from "@server/db/schema";
import db from "@server/db"; import db from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import config from "@server/config"; import config from "@server/config";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export const SESSION_COOKIE_NAME = "session"; export const SESSION_COOKIE_NAME = "session";
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30; export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
export const SECURE_COOKIES = config.server.secure_cookies; export const SECURE_COOKIES = config.server.secure_cookies;
export const COOKIE_DOMAIN = export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url);
"." + new URL(config.app.base_url).hostname.split(".").slice(-2).join(".");
export async function createNewtSession( export async function createNewtSession(
token: string, token: string,

View file

@ -8,12 +8,12 @@ import {
import db from "@server/db"; import db from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import config from "@server/config"; import config from "@server/config";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export const SESSION_COOKIE_NAME = "resource_session"; export const SESSION_COOKIE_NAME = "resource_session";
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30; export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
export const SECURE_COOKIES = config.server.secure_cookies; export const SECURE_COOKIES = config.server.secure_cookies;
export const COOKIE_DOMAIN = export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url);
"." + new URL(config.app.base_url).hostname.split(".").slice(-2).join(".");
export async function createResourceSession(opts: { export async function createResourceSession(opts: {
token: string; token: string;

View file

@ -1,9 +1,9 @@
import { z } from "zod";
import { fromError } from "zod-validation-error";
import path from "path";
import fs from "fs"; import fs from "fs";
import yaml from "js-yaml"; import yaml from "js-yaml";
import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { z } from "zod";
import { fromError } from "zod-validation-error";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);
@ -79,102 +79,125 @@ const environmentSchema = z.object({
.optional() .optional()
}); });
export function getConfig() { export class Config {
const loadConfig = (configPath: string) => { private rawConfig!: z.infer<typeof environmentSchema>;
try {
const yamlContent = fs.readFileSync(configPath, "utf8");
const config = yaml.load(yamlContent);
return config;
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Error loading configuration file: ${error.message}`
);
}
throw error;
}
};
const configFilePath1 = path.join(APP_PATH, "config.yml"); constructor() {
const configFilePath2 = path.join(APP_PATH, "config.yaml"); this.loadConfig();
let environment: any;
if (fs.existsSync(configFilePath1)) {
environment = loadConfig(configFilePath1);
} else if (fs.existsSync(configFilePath2)) {
environment = loadConfig(configFilePath2);
} }
if (!environment) {
const exampleConfigPath = path.join(__DIRNAME, "config.example.yml"); public getRawConfig() {
if (fs.existsSync(exampleConfigPath)) { return this.rawConfig;
}
public loadConfig() {
const loadConfig = (configPath: string) => {
try { try {
const exampleConfigContent = fs.readFileSync( const yamlContent = fs.readFileSync(configPath, "utf8");
exampleConfigPath, const config = yaml.load(yamlContent);
"utf8" return config;
);
fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8");
environment = loadConfig(configFilePath1);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
throw new Error( throw new Error(
`Error creating configuration file from example: ${error.message}` `Error loading configuration file: ${error.message}`
); );
} }
throw error; throw error;
} }
} else { };
throw new Error(
"No configuration file found and no example configuration available" const configFilePath1 = path.join(APP_PATH, "config.yml");
const configFilePath2 = path.join(APP_PATH, "config.yaml");
let environment: any;
if (fs.existsSync(configFilePath1)) {
environment = loadConfig(configFilePath1);
} else if (fs.existsSync(configFilePath2)) {
environment = loadConfig(configFilePath2);
}
if (!environment) {
const exampleConfigPath = path.join(
__DIRNAME,
"config.example.yml"
); );
if (fs.existsSync(exampleConfigPath)) {
try {
const exampleConfigContent = fs.readFileSync(
exampleConfigPath,
"utf8"
);
fs.writeFileSync(
configFilePath1,
exampleConfigContent,
"utf8"
);
environment = loadConfig(configFilePath1);
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Error creating configuration file from example: ${
error.message
}`
);
}
throw error;
}
} else {
throw new Error(
"No configuration file found and no example configuration available"
);
}
} }
}
if (!environment) { if (!environment) {
throw new Error("No configuration file found"); throw new Error("No configuration file found");
}
const parsedConfig = environmentSchema.safeParse(environment);
if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error);
throw new Error(`Invalid configuration file: ${errors}`);
}
const packageJsonPath = path.join(__DIRNAME, "..", "package.json");
let packageJson: any;
if (fs.existsSync && fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
packageJson = JSON.parse(packageJsonContent);
if (packageJson.version) {
process.env.APP_VERSION = packageJson.version;
} }
const parsedConfig = environmentSchema.safeParse(environment);
if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error);
throw new Error(`Invalid configuration file: ${errors}`);
}
const packageJsonPath = path.join(__DIRNAME, "..", "package.json");
let packageJson: any;
if (fs.existsSync && fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
packageJson = JSON.parse(packageJsonContent);
if (packageJson.version) {
process.env.APP_VERSION = packageJson.version;
}
}
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
process.env.SERVER_EXTERNAL_PORT =
parsedConfig.data.server.external_port.toString();
process.env.SERVER_INTERNAL_PORT =
parsedConfig.data.server.internal_port.toString();
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
?.require_email_verification
? "true"
: "false";
process.env.SESSION_COOKIE_NAME =
parsedConfig.data.server.session_cookie_name;
process.env.RESOURCE_SESSION_COOKIE_NAME =
parsedConfig.data.server.resource_session_cookie_name;
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
?.disable_signup_without_invite
? "true"
: "false";
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
?.disable_user_create_org
? "true"
: "false";
this.rawConfig = parsedConfig.data;
} }
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
process.env.SERVER_EXTERNAL_PORT =
parsedConfig.data.server.external_port.toString();
process.env.SERVER_INTERNAL_PORT =
parsedConfig.data.server.internal_port.toString();
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
?.require_email_verification
? "true"
: "false";
process.env.SESSION_COOKIE_NAME =
parsedConfig.data.server.session_cookie_name;
process.env.RESOURCE_SESSION_COOKIE_NAME =
parsedConfig.data.server.resource_session_cookie_name;
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
?.disable_signup_without_invite
? "true"
: "false";
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
?.disable_user_create_org
? "true"
: "false";
return parsedConfig.data;
} }
export default getConfig(); export const config = new Config();
export default config;

View file

@ -6,7 +6,7 @@ import logger from "@server/logger";
export async function sendEmail( export async function sendEmail(
template: ReactElement, template: ReactElement,
opts: { opts: {
name: string | undefined; name?: string;
from: string | undefined; from: string | undefined;
to: string | undefined; to: string | undefined;
subject: string; subject: string;

View file

@ -11,6 +11,7 @@ import { createAdminRole } from "@server/setup/ensureActions";
import config from "@server/config"; import config from "@server/config";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { defaultRoleAllowedActions } from "../role"; import { defaultRoleAllowedActions } from "../role";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
const createOrgSchema = z const createOrgSchema = z
.object({ .object({
@ -83,7 +84,7 @@ export async function createOrg(
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// create a url from config.app.base_url and get the hostname // create a url from config.app.base_url and get the hostname
const domain = new URL(config.app.base_url).hostname; const domain = extractBaseDomain(config.app.base_url);
const newOrg = await trx const newOrg = await trx
.insert(orgs) .insert(orgs)

View file

@ -53,8 +53,6 @@ export async function createResource(
let { name, subdomain } = parsedBody.data; let { name, subdomain } = parsedBody.data;
subdomain = subdomain.toLowerCase(); // always to lower case
// Validate request params // Validate request params
const parsedParams = createResourceParamsSchema.safeParse(req.params); const parsedParams = createResourceParamsSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {

View file

@ -41,8 +41,6 @@ export async function traefikConfigProvider(
const badgerMiddlewareName = "badger"; const badgerMiddlewareName = "badger";
const redirectMiddlewareName = "redirect-to-https"; const redirectMiddlewareName = "redirect-to-https";
// const baseDomain = new URL(config.app.base_url).hostname;
const http: any = { const http: any = {
routers: {}, routers: {},
services: {}, services: {},

View file

@ -3,10 +3,11 @@ import { orgs } from "../db/schema";
import config from "@server/config"; import config from "@server/config";
import { ne } from "drizzle-orm"; import { ne } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export async function copyInConfig() { export async function copyInConfig() {
// create a url from config.app.base_url and get the hostname // create a url from config.app.base_url and get the hostname
const domain = new URL(config.app.base_url).hostname; const domain = extractBaseDomain(config.app.base_url);
// update the domain on all of the orgs where the domain is not equal to the new domain // update the domain on all of the orgs where the domain is not equal to the new domain
// TODO: eventually each org could have a unique domain that we do not want to overwrite, so this will be unnecessary // TODO: eventually each org could have a unique domain that we do not want to overwrite, so this will be unnecessary

View file

@ -1,18 +1,23 @@
import { ensureActions } from "./ensureActions"; import { ensureActions } from "./ensureActions";
import { copyInConfig } from "./copyInConfig"; import { copyInConfig } from "./copyInConfig";
import logger from "@server/logger";
import { runMigrations } from "./migrations"; import { runMigrations } from "./migrations";
import { setupServerAdmin } from "./setupServerAdmin"; import { setupServerAdmin } from "./setupServerAdmin";
import { loadConfig } from "@server/config";
export async function runSetupFunctions() { export async function runSetupFunctions() {
try { try {
logger.info(`Setup for version ${process.env.APP_VERSION}`);
await runMigrations(); // run the migrations await runMigrations(); // run the migrations
console.log("Migrations completed successfully.")
// ANYTHING BEFORE THIS LINE CANNOT USE THE CONFIG
loadConfig();
await copyInConfig(); // copy in the config to the db as needed await copyInConfig(); // copy in the config to the db as needed
await setupServerAdmin(); await setupServerAdmin();
await ensureActions(); // make sure all of the actions are in the db and the roles await ensureActions(); // make sure all of the actions are in the db and the roles
} catch (error) { } catch (error) {
logger.error("Error running setup functions", error); console.error("Error running setup functions:", error);
process.exit(1); process.exit(1);
} }
} }

View file

@ -1,4 +1,3 @@
import logger from "@server/logger";
import { __DIRNAME } from "@server/config"; import { __DIRNAME } from "@server/config";
import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import db, { exists } from "@server/db"; import db, { exists } from "@server/db";
@ -8,12 +7,12 @@ import { versionMigrations } from "@server/db/schema";
import { desc } from "drizzle-orm"; import { desc } from "drizzle-orm";
// Import all migrations explicitly // Import all migrations explicitly
import migration100beta1 from "./scripts/1.0.0-beta1"; import m1 from "./scripts/1.0.0-beta1";
// Add new migration imports here as they are created // Add new migration imports here as they are created
// Define the migration list with versions and their corresponding functions // Define the migration list with versions and their corresponding functions
const migrations = [ const migrations = [
{ version: "1.0.0-beta.1", run: migration100beta1 } { version: "1.0.0-beta.1", run: m1 }
// Add new migrations here as they are created // Add new migrations here as they are created
] as const; ] as const;
@ -23,21 +22,21 @@ export async function runMigrations() {
} }
if (process.env.ENVIRONMENT !== "prod") { if (process.env.ENVIRONMENT !== "prod") {
logger.info("Skipping migrations in non-prod environment"); console.info("Skipping migrations in non-prod environment");
return; return;
} }
if (exists) { if (exists) {
await executeScripts(); await executeScripts();
} else { } else {
logger.info("Running migrations..."); console.info("Running migrations...");
try { try {
migrate(db, { migrate(db, {
migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build
}); });
logger.info("Migrations completed successfully."); console.info("Migrations completed successfully.");
} catch (error) { } catch (error) {
logger.error("Error running migrations:", error); console.error("Error running migrations:", error);
} }
// insert process.env.APP_VERSION into the versionMigrations table // insert process.env.APP_VERSION into the versionMigrations table
@ -61,7 +60,7 @@ async function executeScripts() {
.limit(1); .limit(1);
const startVersion = lastExecuted[0]?.version ?? "0.0.0"; const startVersion = lastExecuted[0]?.version ?? "0.0.0";
logger.info(`Starting migrations from version ${startVersion}`); console.info(`Starting migrations from version ${startVersion}`);
// Filter and sort migrations // Filter and sort migrations
const pendingMigrations = migrations const pendingMigrations = migrations
@ -70,7 +69,7 @@ async function executeScripts() {
// Run migrations in order // Run migrations in order
for (const migration of pendingMigrations) { for (const migration of pendingMigrations) {
logger.info(`Running migration ${migration.version}`); console.info(`Running migration ${migration.version}`);
try { try {
await migration.run(); await migration.run();
@ -84,11 +83,11 @@ async function executeScripts() {
}) })
.execute(); .execute();
logger.info( console.info(
`Successfully completed migration ${migration.version}` `Successfully completed migration ${migration.version}`
); );
} catch (error) { } catch (error) {
logger.error( console.error(
`Failed to run migration ${migration.version}:`, `Failed to run migration ${migration.version}:`,
error error
); );
@ -96,9 +95,9 @@ async function executeScripts() {
} }
} }
logger.info("All migrations completed successfully"); console.info("All migrations completed successfully");
} catch (error) { } catch (error) {
logger.error("Migration process failed:", error); console.error("Migration process failed:", error);
throw error; throw error;
} }
} }

View file

@ -1,6 +1,6 @@
import logger from "@server/logger"; import logger from "@server/logger";
export default async function migration100beta1() { export default async function migration() {
logger.info("Running setup script 1.0.0-beta.1"); logger.info("Running setup script 1.0.0-beta.1");
// SQL operations would go here in ts format // SQL operations would go here in ts format
logger.info("Done..."); logger.info("Done...");

View file

@ -0,0 +1,11 @@
export function extractBaseDomain(url: string): string {
const newUrl = new URL(url);
const hostname = newUrl.hostname;
const parts = hostname.split(".");
if (parts.length <= 2) {
return parts.join(".");
}
return parts.slice(1).join(".");
}

View file

@ -398,7 +398,7 @@ export default function ResourceAuthenticationPage() {
onCheckedChange={(val) => setSsoEnabled(val)} onCheckedChange={(val) => setSsoEnabled(val)}
/> />
<Label htmlFor="sso-toggle"> <Label htmlFor="sso-toggle">
Allow Unified Login Use Platform SSO
</Label> </Label>
</div> </div>
<span className="text-muted-foreground text-sm"> <span className="text-muted-foreground text-sm">

View file

@ -44,7 +44,6 @@ export default async function InvitePage(props: {
await authCookieHeader() await authCookieHeader()
) )
.catch((e) => { .catch((e) => {
console.error(e);
error = formatAxiosError(e); error = formatAxiosError(e);
}); });
@ -68,8 +67,6 @@ export default async function InvitePage(props: {
const type = cardType(); const type = cardType();
console.log("card type is", type, error)
if (!user && type === "user_does_not_exist") { if (!user && type === "user_does_not_exist") {
redirect(`/auth/signup?redirect=/invite?token=${params.token}`); redirect(`/auth/signup?redirect=/invite?token=${params.token}`);
} }