From a1c7ad71202a0e521ebca8412820ca5fd99dfeda Mon Sep 17 00:00:00 2001
From: space
Date: Sun, 26 Jul 2026 17:06:05 +0200
Subject: [PATCH] feat(setup): add ability to skip setup wizard and update user
model
---
backend/src/index.ts | 3 +-
backend/src/routes/api/user.ts | 12 ++++++
backend/src/routes/auth.ts | 14 ++++++-
frontend/src/hooks/useAuth.ts | 1 +
frontend/src/pages/SetupWizard.tsx | 41 +++++++++++++++----
frontend/src/services/api.ts | 1 +
.../migration.sql | 1 +
prisma/schema.prisma | 1 +
8 files changed, 64 insertions(+), 10 deletions(-)
create mode 100644 prisma/migrations/20260726170000_setup_skipped/migration.sql
diff --git a/backend/src/index.ts b/backend/src/index.ts
index dd26f24d..6346aa88 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -18,7 +18,7 @@ import { runWebhookReconciliation } from "./services/webhookReconcile";
import { loginHandler, logoutHandler, meHandler, setupStatusHandler, firstUserHandler } from "./routes/auth";
import { webhookHandler } from "./routes/webhook";
-import { getUserSettings, updateUsername, updatePassword, updateGitea, updateAws, getWebhookSecret, regenerateWebhookSecret } from "./routes/api/user";
+import { getUserSettings, updateUsername, updatePassword, updateGitea, updateAws, skipSetupWizard, getWebhookSecret, regenerateWebhookSecret } from "./routes/api/user";
import { listRepos, saveRepoConfig, toggleRepoEnabled, getRepoConfig } from "./routes/api/repos";
import { listPreviews, getPreview, stopPreviewRoute, rebuildPreviewRoute, previewLogsWs, previewAppLogsWs } from "./routes/api/previews";
import { getStats } from "./routes/api/stats";
@@ -66,6 +66,7 @@ server.path("/", (path) => path
.http("PATCH", "/api/user/password", (http) => http.onRequest(updatePassword))
.http("PUT", "/api/user/gitea", (http) => http.onRequest(updateGitea))
.http("PUT", "/api/user/aws", (http) => http.onRequest(updateAws))
+ .http("POST", "/api/user/setup/skip", (http) => http.onRequest(skipSetupWizard))
.http("GET", "/api/user/webhook-secret", (http) => http.onRequest(getWebhookSecret))
.http("POST", "/api/user/webhook-secret/regenerate", (http) => http.onRequest(regenerateWebhookSecret))
);
diff --git a/backend/src/routes/api/user.ts b/backend/src/routes/api/user.ts
index 469bbc57..117f6f75 100644
--- a/backend/src/routes/api/user.ts
+++ b/backend/src/routes/api/user.ts
@@ -181,6 +181,18 @@ export async function updateAws(ctr: any) {
return makeResponse({ ctr, content: { code: 200, message: `Connected as ${validation.arn}`, data: { arn: validation.arn } } });
}
+export async function skipSetupWizard(ctr: any) {
+ const user = requireAuth(ctr);
+ if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });
+
+ await prisma.user.update({
+ where: { id: user.id },
+ data: { setupSkipped: true },
+ });
+
+ return makeResponse({ ctr, content: { code: 200, message: "Setup wizard skipped" } });
+}
+
export async function getWebhookSecret(ctr: any) {
const user = requireAuth(ctr);
if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });
diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts
index 7ad9a7c3..410ce96a 100644
--- a/backend/src/routes/auth.ts
+++ b/backend/src/routes/auth.ts
@@ -10,6 +10,17 @@ const log = createLogger("AUTH_ROUTE");
const COOKIE_NAME = "pp_session";
const COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
+function isSetupComplete(user: {
+ setupSkipped: boolean;
+ giteaInstanceUrl: string | null;
+ giteaPAT: string | null;
+ awsAccessKeyId: string | null;
+ awsSecretAccessKey: string | null;
+ awsRegion: string | null;
+}) {
+ return user.setupSkipped || !!(user.giteaInstanceUrl && user.giteaPAT && user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion);
+}
+
export async function loginHandler(ctr: any) {
let body: any;
try {
@@ -82,7 +93,8 @@ export async function meHandler(ctr: any) {
awsAccessKeyId: user.awsAccessKeyId ? "****" : null,
awsRegion: user.awsRegion,
awsConfigured: !!(user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion),
- setupComplete: !!(user.giteaInstanceUrl && user.giteaPAT && user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion),
+ setupSkipped: user.setupSkipped,
+ setupComplete: isSetupComplete(user),
},
},
});
diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts
index 2db1583b..eac4af1a 100644
--- a/frontend/src/hooks/useAuth.ts
+++ b/frontend/src/hooks/useAuth.ts
@@ -6,6 +6,7 @@ export interface AuthUser {
username: string;
isAdmin: boolean;
isFounder: boolean;
+ setupSkipped: boolean;
setupComplete: boolean;
giteaUsername: string | null;
giteaInstanceUrl: string | null;
diff --git a/frontend/src/pages/SetupWizard.tsx b/frontend/src/pages/SetupWizard.tsx
index af7a144e..97b4619e 100644
--- a/frontend/src/pages/SetupWizard.tsx
+++ b/frontend/src/pages/SetupWizard.tsx
@@ -44,7 +44,32 @@ export function SetupWizard() {
}, [step, user?.id]);
const next = () => setStep(s => Math.min(s + 1, STEPS.length - 1));
- const skip = () => navigate("/");
+ const skip = async () => {
+ setSaving(true);
+ const res = await api.user.skipSetup();
+ setSaving(false);
+ if (res.ok) {
+ await refresh();
+ navigate("/");
+ } else {
+ toast.error(res.message || "Failed to skip setup");
+ }
+ };
+ const leaveWizard = async (path: string) => {
+ if (user?.setupComplete) {
+ navigate(path);
+ return;
+ }
+ setSaving(true);
+ const res = await api.user.skipSetup();
+ setSaving(false);
+ if (res.ok) {
+ await refresh();
+ navigate(path);
+ } else {
+ toast.error(res.message || "Failed to finish setup");
+ }
+ };
const handleGiteaNext = async () => {
setSaving(true);
@@ -112,8 +137,8 @@ export function SetupWizard() {
-
@@ -227,8 +252,8 @@ export function SetupWizard() {
Open a pull request on an enabled repo and PP will provision a preview automatically.
- navigate("/")} className="inline-flex items-center gap-1 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors">
- Go to Dashboard
+ leaveWizard("/")} disabled={saving} className="inline-flex items-center gap-1 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50">
+ {saving ? "Finishing..." : Go to Dashboard }
)}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index a20fb55d..b947a2cc 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -37,6 +37,7 @@ export const api = {
request("PUT", "/user/gitea", data),
updateAws: (data: { awsAccessKeyId: string; awsSecretAccessKey: string; awsRegion: string }) =>
request("PUT", "/user/aws", data),
+ skipSetup: () => request("POST", "/user/setup/skip"),
getWebhookSecret: () => request("GET", "/user/webhook-secret"),
regenerateWebhookSecret: () => request("POST", "/user/webhook-secret/regenerate"),
},
diff --git a/prisma/migrations/20260726170000_setup_skipped/migration.sql b/prisma/migrations/20260726170000_setup_skipped/migration.sql
new file mode 100644
index 00000000..55bf12d8
--- /dev/null
+++ b/prisma/migrations/20260726170000_setup_skipped/migration.sql
@@ -0,0 +1 @@
+ALTER TABLE "User" ADD COLUMN "setupSkipped" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 25bec2d3..888e80e9 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -36,6 +36,7 @@ model User {
passwordHash String @db.VarChar(256)
isAdmin Boolean @default(false)
isFounder Boolean @default(false)
+ setupSkipped Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt