fix(ui): use app icon in header #3
@@ -18,7 +18,7 @@ import { runWebhookReconciliation } from "./services/webhookReconcile";
|
|||||||
|
|
||||||
import { loginHandler, logoutHandler, meHandler, setupStatusHandler, firstUserHandler } from "./routes/auth";
|
import { loginHandler, logoutHandler, meHandler, setupStatusHandler, firstUserHandler } from "./routes/auth";
|
||||||
import { webhookHandler } from "./routes/webhook";
|
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 { listRepos, saveRepoConfig, toggleRepoEnabled, getRepoConfig } from "./routes/api/repos";
|
||||||
import { listPreviews, getPreview, stopPreviewRoute, rebuildPreviewRoute, previewLogsWs, previewAppLogsWs } from "./routes/api/previews";
|
import { listPreviews, getPreview, stopPreviewRoute, rebuildPreviewRoute, previewLogsWs, previewAppLogsWs } from "./routes/api/previews";
|
||||||
import { getStats } from "./routes/api/stats";
|
import { getStats } from "./routes/api/stats";
|
||||||
@@ -66,6 +66,7 @@ server.path("/", (path) => path
|
|||||||
.http("PATCH", "/api/user/password", (http) => http.onRequest(updatePassword))
|
.http("PATCH", "/api/user/password", (http) => http.onRequest(updatePassword))
|
||||||
.http("PUT", "/api/user/gitea", (http) => http.onRequest(updateGitea))
|
.http("PUT", "/api/user/gitea", (http) => http.onRequest(updateGitea))
|
||||||
.http("PUT", "/api/user/aws", (http) => http.onRequest(updateAws))
|
.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("GET", "/api/user/webhook-secret", (http) => http.onRequest(getWebhookSecret))
|
||||||
.http("POST", "/api/user/webhook-secret/regenerate", (http) => http.onRequest(regenerateWebhookSecret))
|
.http("POST", "/api/user/webhook-secret/regenerate", (http) => http.onRequest(regenerateWebhookSecret))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 } } });
|
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) {
|
export async function getWebhookSecret(ctr: any) {
|
||||||
const user = requireAuth(ctr);
|
const user = requireAuth(ctr);
|
||||||
if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });
|
if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ const log = createLogger("AUTH_ROUTE");
|
|||||||
const COOKIE_NAME = "pp_session";
|
const COOKIE_NAME = "pp_session";
|
||||||
const COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
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) {
|
export async function loginHandler(ctr: any) {
|
||||||
let body: any;
|
let body: any;
|
||||||
try {
|
try {
|
||||||
@@ -82,7 +93,8 @@ export async function meHandler(ctr: any) {
|
|||||||
awsAccessKeyId: user.awsAccessKeyId ? "****" : null,
|
awsAccessKeyId: user.awsAccessKeyId ? "****" : null,
|
||||||
awsRegion: user.awsRegion,
|
awsRegion: user.awsRegion,
|
||||||
awsConfigured: !!(user.awsAccessKeyId && user.awsSecretAccessKey && 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),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface AuthUser {
|
|||||||
username: string;
|
username: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isFounder: boolean;
|
isFounder: boolean;
|
||||||
|
setupSkipped: boolean;
|
||||||
setupComplete: boolean;
|
setupComplete: boolean;
|
||||||
giteaUsername: string | null;
|
giteaUsername: string | null;
|
||||||
giteaInstanceUrl: string | null;
|
giteaInstanceUrl: string | null;
|
||||||
|
|||||||
@@ -44,7 +44,32 @@ export function SetupWizard() {
|
|||||||
}, [step, user?.id]);
|
}, [step, user?.id]);
|
||||||
|
|
||||||
const next = () => setStep(s => Math.min(s + 1, STEPS.length - 1));
|
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 () => {
|
const handleGiteaNext = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -112,8 +137,8 @@ export function SetupWizard() {
|
|||||||
<button onClick={next} 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">
|
<button onClick={next} 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">
|
||||||
Get Started <ArrowRight size={16} />
|
Get Started <ArrowRight size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={skip} className="px-4 py-2.5 text-sm text-gray-500 dark:text-slate-400 hover:underline">
|
<button onClick={skip} disabled={saving} className="px-4 py-2.5 text-sm text-gray-500 dark:text-slate-400 hover:underline disabled:opacity-50">
|
||||||
Skip wizard
|
{saving ? "Skipping..." : "Skip wizard"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,10 +236,10 @@ export function SetupWizard() {
|
|||||||
Go to the Repos page to enable previews for a repository. PP will automatically register the webhook.
|
Go to the Repos page to enable previews for a repository. PP will automatically register the webhook.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<a href="/repos"
|
<button onClick={() => leaveWizard("/repos")} disabled={saving}
|
||||||
className="flex-1 inline-flex items-center justify-center gap-1 py-2.5 text-center bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors">
|
className="flex-1 inline-flex items-center justify-center gap-1 py-2.5 text-center bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50">
|
||||||
Go to Repos <ArrowRight size={16} />
|
Go to Repos <ArrowRight size={16} />
|
||||||
</a>
|
</button>
|
||||||
<button onClick={next} className="text-sm text-gray-500 dark:text-slate-400 hover:underline px-2">Skip</button>
|
<button onClick={next} className="text-sm text-gray-500 dark:text-slate-400 hover:underline px-2">Skip</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,8 +252,8 @@ export function SetupWizard() {
|
|||||||
<p className="text-gray-600 dark:text-slate-300 text-sm">
|
<p className="text-gray-600 dark:text-slate-300 text-sm">
|
||||||
Open a pull request on an enabled repo and PP will provision a preview automatically.
|
Open a pull request on an enabled repo and PP will provision a preview automatically.
|
||||||
</p>
|
</p>
|
||||||
<button onClick={() => 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">
|
<button onClick={() => 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">
|
||||||
Go to Dashboard <ArrowRight size={16} />
|
{saving ? "Finishing..." : <span className="inline-flex items-center gap-1">Go to Dashboard <ArrowRight size={16} /></span>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const api = {
|
|||||||
request("PUT", "/user/gitea", data),
|
request("PUT", "/user/gitea", data),
|
||||||
updateAws: (data: { awsAccessKeyId: string; awsSecretAccessKey: string; awsRegion: string }) =>
|
updateAws: (data: { awsAccessKeyId: string; awsSecretAccessKey: string; awsRegion: string }) =>
|
||||||
request("PUT", "/user/aws", data),
|
request("PUT", "/user/aws", data),
|
||||||
|
skipSetup: () => request("POST", "/user/setup/skip"),
|
||||||
getWebhookSecret: () => request("GET", "/user/webhook-secret"),
|
getWebhookSecret: () => request("GET", "/user/webhook-secret"),
|
||||||
regenerateWebhookSecret: () => request("POST", "/user/webhook-secret/regenerate"),
|
regenerateWebhookSecret: () => request("POST", "/user/webhook-secret/regenerate"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "User" ADD COLUMN "setupSkipped" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -36,6 +36,7 @@ model User {
|
|||||||
passwordHash String @db.VarChar(256)
|
passwordHash String @db.VarChar(256)
|
||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
isFounder Boolean @default(false)
|
isFounder Boolean @default(false)
|
||||||
|
setupSkipped Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user