Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps/web
diff options
context:
space:
mode:
authorLeitwolf112026-08-30 21:45:01 +0300
committerLeitwolf112026-08-30 21:45:01 +0300
commit76c4c1876a86dbca0adf86750362b3581d6fc69d (patch)
tree52bc17d6a977d6c1b13c26c1e016a0e00db99ca5 /apps/web
parentc89a3f20a0f477573c41b831d993850e0adb30e7 (diff)
downloadprogmaweb-1.0.0.tar.gz
Add secure Google sign-in to Progmasoft Account. Use the server-side authorization-code flow with an exact account-domain callback, temporary hardened external cookies, verified provider email matching, canonical Account-name creation for new users, and no separate ViGet publisher identity. Add frontend entry points, provider failure guidance, locked Google authentication dependencies, service secret injection, and backend regression coverage without committing OAuth credentials.1.0.0
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/globals.css28
-rw-r--r--apps/web/app/login/page.tsx14
-rw-r--r--apps/web/app/register/page.tsx22
-rw-r--r--apps/web/components/AuthForm.tsx40
4 files changed, 97 insertions, 7 deletions
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index a481a52..8ec857b 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -863,6 +863,34 @@ a {
margin-top: 3px;
}
+.auth-divider {
+ align-items: center;
+ color: var(--muted);
+ display: flex;
+ font-size: 0.78rem;
+ gap: 0.75rem;
+ text-transform: uppercase;
+}
+
+.auth-divider::before,
+.auth-divider::after {
+ background: var(--line);
+ content: "";
+ flex: 1;
+ height: 1px;
+}
+
+.google-auth-button {
+ background: transparent;
+ border-color: var(--line);
+ color: var(--ink);
+ width: 100%;
+}
+
+.google-auth-button:hover {
+ border-color: var(--accent);
+}
+
.form-switch {
margin: 0;
color: var(--ink-650);
diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx
index 2162ffe..90c2910 100644
--- a/apps/web/app/login/page.tsx
+++ b/apps/web/app/login/page.tsx
@@ -10,14 +10,24 @@ export const metadata: Metadata = {
robots: { index: false, follow: false },
};
-export default function LoginPage() {
+interface LoginPageProps {
+ searchParams: Promise<{ google?: string }>;
+}
+
+export default async function LoginPage({ searchParams }: LoginPageProps) {
+ const { google } = await searchParams;
+ const initialMessage =
+ google === "failed"
+ ? "Google sign-in could not be completed. Try again."
+ : undefined;
+
return (
<AccountShell
eyebrow="Progmasoft account"
title="Sign in securely."
description="Continue to your account dashboard and connected services."
>
- <AuthForm mode="login" />
+ <AuthForm initialMessage={initialMessage} mode="login" />
</AccountShell>
);
}
diff --git a/apps/web/app/register/page.tsx b/apps/web/app/register/page.tsx
index 97c63e5..d120d45 100644
--- a/apps/web/app/register/page.tsx
+++ b/apps/web/app/register/page.tsx
@@ -10,14 +10,32 @@ export const metadata: Metadata = {
robots: { index: false, follow: false },
};
-export default function RegisterPage() {
+interface RegisterPageProps {
+ searchParams: Promise<{ google?: string }>;
+}
+
+const googleMessages: Record<string, string> = {
+ "account-name-required":
+ "Choose an Account name before continuing with Google.",
+ "account-name-unavailable":
+ "That Account name is unavailable. Choose another name.",
+ "invalid-account-name":
+ "Use a valid Account name before continuing with Google.",
+};
+
+export default async function RegisterPage({
+ searchParams,
+}: RegisterPageProps) {
+ const { google } = await searchParams;
+ const initialMessage = google ? googleMessages[google] : undefined;
+
return (
<AccountShell
eyebrow="Create an identity"
title="Start with a durable account."
description="Choose the name used in your Progmasoft account URL."
>
- <AuthForm mode="register" />
+ <AuthForm initialMessage={initialMessage} mode="register" />
</AccountShell>
);
}
diff --git a/apps/web/components/AuthForm.tsx b/apps/web/components/AuthForm.tsx
index 56fb78d..f32c49d 100644
--- a/apps/web/components/AuthForm.tsx
+++ b/apps/web/components/AuthForm.tsx
@@ -5,11 +5,12 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
-import { type FormEvent, useId, useState } from "react";
+import { type FormEvent, type MouseEvent, useId, useState } from "react";
type AuthMode = "login" | "register";
interface AuthFormProps {
+ initialMessage?: string;
mode: AuthMode;
}
@@ -30,13 +31,34 @@ function firstProblemMessage(problem: ProblemDetails): string {
return firstError ?? problem.title ?? "The request could not be completed.";
}
-export function AuthForm({ mode }: AuthFormProps) {
+export function AuthForm({ initialMessage, mode }: AuthFormProps) {
const router = useRouter();
const formId = useId();
const [pending, setPending] = useState(false);
- const [message, setMessage] = useState<string | null>(null);
+ const [message, setMessage] = useState<string | null>(initialMessage ?? null);
const [showPassword, setShowPassword] = useState(false);
+ function continueWithGoogle(event: MouseEvent<HTMLButtonElement>) {
+ const form = event.currentTarget.form;
+ const parameters = new URLSearchParams();
+
+ if (mode === "register") {
+ const accountName = form?.elements.namedItem("accountName");
+ if (!(accountName instanceof HTMLInputElement)) {
+ setMessage("The Account name field could not be read.");
+ return;
+ }
+ if (!accountName.checkValidity()) {
+ accountName.reportValidity();
+ return;
+ }
+ parameters.set("accountName", accountName.value);
+ }
+
+ const query = parameters.size === 0 ? "" : `?${parameters.toString()}`;
+ window.open(`/api/v1/accounts/oauth/google/start${query}`, "_self");
+ }
+
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setMessage(null);
@@ -217,6 +239,18 @@ export function AuthForm({ mode }: AuthFormProps) {
: "Sign in"}
</button>
+ <div className="auth-divider" aria-hidden="true">
+ <span>or</span>
+ </div>
+ <button
+ className="button google-auth-button"
+ type="button"
+ disabled={pending}
+ onClick={continueWithGoogle}
+ >
+ Continue with Google
+ </button>
+
<p className="form-switch">
{mode === "register"
? "Already have an account?"