Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions app/auth-portal/src/store/auth.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export const useAuthStore = defineStore("auth", {
// split from LOAD_USER since it will likely change
// and because this request loading blocks the whole page/app
async CHECK_AUTH() {
return new ApiRequest<{ user: User }>({
const req = new ApiRequest<{ user: User }>({
url: "/whoami",
onSuccess: (response) => {
this.user = response.user;
Expand All @@ -173,10 +173,46 @@ export const useAuthStore = defineStore("auth", {
posthog.alias(this.user.id, this.user.email);
}
},
onFail(e) {
onFail: async (e) => {
/* eslint-disable-next-line no-console */
console.log("RESTORE AUTH FAILED!", e);
// trigger logout?

// Try local login - backend will reject if LOCAL_AUTH_MODE is not enabled
// This auto-detects local mode without requiring frontend config
/* eslint-disable-next-line no-console */
console.log("Attempting local auth auto-detection...");
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.LOCAL_LOGIN();
},
});
return req;
},

/**
* LOCAL AUTH MODE ONLY
* Authenticates with local development auth (no Auth0)
* Backend will reject this if LOCAL_AUTH_MODE is not enabled
*/
async LOCAL_LOGIN(email?: string) {
return new ApiRequest<{ user: User; token: string }>({
method: "post",
url: "/auth/local-login",
params: { email },
onSuccess: (response) => {
this.user = response.user;
posthog.identify(this.user.id);
if (this.user.email) {
posthog.alias(this.user.id, this.user.email);
}
/* eslint-disable-next-line no-console */
console.log("🔧 LOCAL AUTH MODE: Local login successful", {
userId: response.user.id,
email: response.user.email,
});
},
onFail: (e) => {
// Silently fail - this is expected when not in local mode
// Backend returns 403 "LocalAuthDisabled" when LOCAL_AUTH_MODE is not enabled
},
});
},
Expand Down
13 changes: 7 additions & 6 deletions app/auth-portal/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ export default defineConfig({
process.env.NODE_ENV !== "production" &&
checkerPlugin({
vueTsc: true,
eslint: {
lintCommand: packageJson.scripts.lint,
// I _think_ we only want to pop up an error on the screen for proper errors
// otherwise we can get a lot of unused var errors when you comment something out temporarily
dev: { logLevel: ["error"] },
},
// NOTE: ESLint checker disabled due to incompatibility between
// vite-plugin-checker@0.12.0 and ESLint 9
// ESLint can still be run manually via: pnpm lint
// eslint: {
// lintCommand: packageJson.scripts.lint,
// dev: { logLevel: ["error"] },
// },
}),

// https://github.com/btd/rollup-plugin-visualizer/issues/176
Expand Down
2 changes: 0 additions & 2 deletions app/web/.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
VITE_SI_ENV=local
VITE_API_PROXY_PATH=/api

VITE_AUTH_API_URL=https://auth-api.systeminit.com
VITE_AUTH_PORTAL_URL=https://auth.systeminit.com
VITE_AUTH0_DOMAIN=systeminit.auth0.com
VITE_BACKEND_HOSTS=["/localhost/g","/si.keeb.dev/g","/app.systeminit.com/g","/tools.systeminit.com/g"]

Expand Down
5 changes: 5 additions & 0 deletions bin/auth-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { httpRequestLoggingMiddleware } from "./lib/request-logger";
import { loadAuthMiddleware, requireWebTokenMiddleware } from "./services/auth.service";
import { detectClientIp } from "./lib/client-ip";
import { CustomAppContext, CustomAppState } from "./custom-state";
import { logLocalAuthWarning } from "./services/auth0-local.service";

import './lib/posthog';

Expand Down Expand Up @@ -47,6 +48,10 @@ if (process.env.NODE_ENV !== 'test') {
await routesLoaded;
app.listen(process.env.PORT);
console.log(chalk.green.bold(`Auth API listening on port ${process.env.PORT}`));

// Log warning if local auth mode is enabled
logLocalAuthWarning();

// await prisma.$disconnect();
} catch (err) {
console.log('ERROR!', err);
Expand Down
82 changes: 82 additions & 0 deletions bin/auth-api/src/routes/auth.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
getAuth0LogoutUrl,
getAuth0UserCredential,
} from "../services/auth0.service";
import {
completeLocalAuth0TokenExchange,
isLocalAuth,
} from "../services/auth0-local.service";
import {
createAuthToken,
createSdfAuthToken,
Expand Down Expand Up @@ -49,6 +53,23 @@ const parseCliRedirectPort = (port: string[] | string): number => {
};

router.get("/auth/login", async (ctx) => {
// LOCAL AUTH MODE: bypass Auth0 entirely for local development
if (isLocalAuth()) {
// eslint-disable-next-line no-console
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: "info",
type: "local-auth",
action: "login_redirect",
message: "🔧 LOCAL AUTH MODE: Redirecting to local login - NO Auth0 calls",
}));

// Redirect directly to local login success (auto-authenticate)
ctx.redirect(`${process.env.AUTH_PORTAL_URL}/login-success?local=true`);
return;
}

// PRODUCTION MODE: Normal Auth0 OAuth flow
// passing in cli_redir=PORT_NO will begin the auth flow for the si cli
const cliRedirParam = ctx.request.query.cli_redir;
const cliRedirect = cliRedirParam
Expand Down Expand Up @@ -178,6 +199,67 @@ router.get("/auth/login-callback", async (ctx) => {
}
});

/**
* LOCAL AUTH MODE ONLY
* Simple login endpoint that bypasses Auth0 entirely
* Creates/updates local user and returns session token
*/
router.post("/auth/local-login", async (ctx) => {
if (!isLocalAuth()) {
throw new ApiError("Forbidden", "LocalAuthDisabled", "Local auth mode is not enabled");
}

// eslint-disable-next-line no-console
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: "info",
type: "local-auth",
action: "local_login",
message: "🔧 LOCAL AUTH MODE: Processing local login - NO Auth0 interaction",
}));

const reqBody = validate(
ctx.request.body,
z.object({
email: z.string().email().optional(),
}),
);

// Use local mock Auth0 profile
const { profile } = await completeLocalAuth0TokenExchange(reqBody.email);
const user = await createOrUpdateUserFromAuth0Details(profile);

// eslint-disable-next-line no-console
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: "info",
type: "local-auth",
action: "user_authenticated",
userId: user.id,
email: user.email,
message: "🔧 LOCAL AUTH MODE: User authenticated locally",
}));

// Create session token for auth-api communication
const siToken = createAuthToken(user.id);

ctx.cookies.set(SI_COOKIE_NAME, siToken, {
httpOnly: true,
secure: false, // Local development doesn't use HTTPS
});

ctx.body = {
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
nickname: user.nickname,
},
token: siToken,
};
});

router.get("/auth/cli-auth-api-token", async (ctx) => {
const nonce = ctx.request.query.nonce;
if (!nonce) {
Expand Down
20 changes: 20 additions & 0 deletions bin/auth-api/src/routes/user.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
refreshUserAuth0Profile,
saveUser,
} from "../services/users.service";
import { isLocalAuth } from "../services/auth0-local.service";
import { resendAuth0EmailVerification } from "../services/auth0.service";
import { tracker } from "../lib/tracker";
import { createProductionWorkspaceForUser } from "../services/workspaces.service";
Expand Down Expand Up @@ -484,6 +485,25 @@ router.post("/users/:userId/dismissFirstTimeModal", async (ctx) => {
router.get("/users/:userId/firstTimeModal", async (ctx) => {
const user = await extractOwnUserIdParam(ctx);

// LOCAL AUTH MODE: Always skip onboarding for local users
if (isLocalAuth()) {
// eslint-disable-next-line no-console
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: "info",
type: "local-auth",
action: "check_onboarding",
userId: user.id,
message: "🔧 LOCAL AUTH MODE: Returning firstTimeModal=false to skip onboarding",
}));

ctx.body = {
firstTimeModal: false,
};
return;
}

// PRODUCTION MODE: Return actual value from database
ctx.body = {
firstTimeModal: (user?.onboardingDetails)?.firstTimeModal,
};
Expand Down
48 changes: 32 additions & 16 deletions bin/auth-api/src/routes/workspace.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { tracker } from "../lib/tracker";
import { posthog } from "../lib/posthog";
import { findLatestTosForUser } from "../services/tos.service";
import { automationApiRouter, extractAuthUser, router } from ".";
import { isLocalAuth } from "../services/auth0-local.service";

// When we send a hubspot email via the posthog event
// if the workspace name is a domain name like string e.g. bing.com
Expand Down Expand Up @@ -560,29 +561,44 @@ router.patch("/workspaces/:workspaceId/setHidden", async (ctx) => {
router.get("/workspaces/:workspaceId/go", async (ctx) => {
const { authUser, workspace } = await authorizeWorkspaceRoute(ctx);

// we require the user to have verified their email before they can log into a workspace
if (!authUser.emailVerified) {
// we'll first refresh from auth0 to make sure its actually not verified
await refreshUserAuth0Profile(authUser);
// then throw an error
// LOCAL AUTH MODE: Skip email verification and ToS checks
if (isLocalAuth()) {
// eslint-disable-next-line no-console
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: "info",
type: "local-auth",
action: "skip_verification",
userId: authUser.id,
workspaceId: workspace.id,
message: "🔧 LOCAL AUTH MODE: Skipping email verification and ToS checks",
}));
} else {
// PRODUCTION MODE: Enforce email verification and ToS acceptance
// we require the user to have verified their email before they can log into a workspace
if (!authUser.emailVerified) {
// we'll first refresh from auth0 to make sure its actually not verified
await refreshUserAuth0Profile(authUser);
// then throw an error
if (!authUser.emailVerified) {
throw new ApiError(
"Unauthorized",
"EmailNotVerified",
"System Initiative Requires Verified Emails to access Workspaces. Check your registered email for Verification email from SI Auth Portal.",
);
}
}

const latestTos = await findLatestTosForUser(authUser);
if (latestTos > authUser.agreedTosVersion) {
throw new ApiError(
"Unauthorized",
"EmailNotVerified",
"System Initiative Requires Verified Emails to access Workspaces. Check your registered email for Verification email from SI Auth Portal.",
"MissingTosAcceptance",
"Terms of Service have been updated, return to the SI auth portal to accept them.",
);
}
}

const latestTos = await findLatestTosForUser(authUser);
if (latestTos > authUser.agreedTosVersion) {
throw new ApiError(
"Unauthorized",
"MissingTosAcceptance",
"Terms of Service have been updated, return to the SI auth portal to accept them.",
);
}

const { redirect } = validate(
ctx.request.query,
z.object({
Expand Down
Loading