Skip to content
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
9 changes: 4 additions & 5 deletions e2e/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test.describe('Authentication', () => {
await expect(page.getByLabel('Username')).toBeVisible();
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByLabel('Password')).toBeVisible();
await expect(page.getByRole('button', { name: 'Register' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Create account' })).toBeVisible();
});

test('register new user shows success message', async ({ page }) => {
Expand All @@ -17,11 +17,10 @@ test.describe('Authentication', () => {
await page.getByLabel('Username').fill(`user_${tag}`);
await page.getByLabel('Email').fill(`${tag}@commonly.test`);
await page.getByLabel('Password').fill('TestPass123!');
await page.getByRole('button', { name: 'Register' }).click();
await page.getByRole('button', { name: 'Create account' }).click();

// Success message from res.data.message — exact text depends on backend
// but it must be non-error text visible on the page
await expect(page.locator('.MuiTypography-root').filter({ hasNotText: /Register|Create|Start/ }).last()).toBeVisible({ timeout: 8000 });
// v2 register swaps to a success state with a "Continue to sign in" CTA
await expect(page.getByRole('button', { name: 'Continue to sign in' })).toBeVisible({ timeout: 8000 });
});

test('login with wrong password shows error', async ({ page }) => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/v2/V2App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import V2FeaturePage from './components/V2FeaturePage';
import V2YourTeamPage from './components/V2YourTeamPage';
import V2InviteRedeem from './components/V2InviteRedeem';
import { useAuth } from '../context/AuthContext';
import Register from '../components/Register';
import V2Register from './components/V2Register';
import RegistrationInviteRequired from '../components/RegistrationInviteRequired';
import VerifyEmail from '../components/VerifyEmail';
import DiscordCallback from '../components/DiscordCallback';
Expand Down Expand Up @@ -125,7 +125,7 @@ const V2App: React.FC = () => {
<Route path="landing" element={<V2LandingPage />} />
<Route path="use-cases/:useCaseId" element={<UseCasePage />} />
<Route path="login" element={<V2Login />} />
<Route path="register" element={<Register />} />
<Route path="register" element={<V2Register />} />
<Route path="register/invite-required" element={<RegistrationInviteRequired />} />
<Route path="verify-email" element={<VerifyEmail />} />
<Route path="discord/callback" element={<DiscordCallback />} />
Expand Down
157 changes: 157 additions & 0 deletions frontend/src/v2/components/V2Register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams, Navigate, Link } from 'react-router-dom';
import axios from '../../utils/axiosConfig';

// v2-native sign-up. Pairs with V2Login (reuses the .v2-login styles) so the
// auth surfaces match after v2 became the default. Mirrors the legacy
// Register flow: honor the invite-only policy, POST /api/auth/register, then
// surface the backend's message and hand off to sign-in (the backend may send
// a verification email; it does not always return a usable session).
//
// The .v2-login__card class goes on the <form>/<div> directly (like V2Login) —
// a bare <form> picks up a dark global background, so it must carry the card.

interface RegistrationPolicy {
loaded: boolean;
inviteOnly: boolean;
}

const Brand: React.FC = () => (
<div className="v2-login__brand">
<span className="v2-rail__brand-icon">c</span>
commonly
</div>
);

const V2Register: React.FC = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [invitationCode] = useState(searchParams.get('invite') || '');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState<string | null>(null);
const [policy, setPolicy] = useState<RegistrationPolicy>({ loaded: false, inviteOnly: false });

useEffect(() => {
let active = true;
axios.get('/api/auth/registration-policy')
.then((res) => { if (active) setPolicy({ loaded: true, inviteOnly: Boolean(res.data?.inviteOnly) }); })
.catch(() => { if (active) setPolicy({ loaded: true, inviteOnly: false }); });
return () => { active = false; };
}, []);

const hasInviteFromUrl = useMemo(() => Boolean(searchParams.get('invite')), [searchParams]);

if (policy.loaded && policy.inviteOnly && !hasInviteFromUrl) {
return <Navigate to="/v2/register/invite-required" replace />;
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSubmitting(true);
try {
const res = await axios.post('/api/auth/register', {
username: username.trim(),
email: email.trim(),
password,
invitationCode: invitationCode.trim(),
});
const data = res.data as { message?: string };
setDone(data?.message || 'Your account is ready. Sign in to continue.');
} catch (err) {
const e1 = err as { response?: { data?: { error?: string; msg?: string } } };
setError(e1.response?.data?.error || e1.response?.data?.msg || 'Registration failed.');
} finally {
setSubmitting(false);
}
};

if (done) {
return (
<div className="v2-login">
<div className="v2-login__card">
<Brand />
<h1 className="v2-login__title">Account created</h1>
<p className="v2-login__subtitle">{done}</p>
<button
type="button"
className="v2-login__submit"
onClick={() => navigate('/v2/login')}
>
Continue to sign in
</button>
</div>
</div>
);
}

return (
<div className="v2-login">
<form className="v2-login__card" onSubmit={handleSubmit}>
<Brand />
<h1 className="v2-login__title">Create your account</h1>
<p className="v2-login__subtitle">
Join the shared space where agents and humans collaborate.
</p>

<label className="v2-login__field">
<span className="v2-login__label">Username</span>
<input
className="v2-login__input"
type="text"
autoComplete="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</label>

<label className="v2-login__field">
<span className="v2-login__label">Email</span>
<input
className="v2-login__input"
type="email"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</label>

<label className="v2-login__field">
<span className="v2-login__label">Password</span>
<input
className="v2-login__input"
type="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>

<button
type="submit"
className="v2-login__submit"
disabled={submitting}
>
{submitting ? 'Creating account...' : 'Create account'}
</button>

{error && <div className="v2-login__error">{error}</div>}

<div className="v2-login__hint">
Already have an account?
{' '}
<Link to="/v2/login" className="v2-login__link">Sign in</Link>
</div>
</form>
</div>
);
};

export default V2Register;
9 changes: 6 additions & 3 deletions frontend/src/v2/v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -3381,7 +3381,10 @@
border-color: var(--v2-accent);
}

.v2-login__submit {
/* Specificity-matched against the global .v2-root button:not(.MuiButtonBase-root)
reset (0,0,2,1) — prefix with .v2-root button.X so the accent fill wins.
Without this the submit button renders transparent (login + register). */
.v2-root button.v2-login__submit {
width: 100%;
margin-top: 8px;
padding: 10px 14px;
Expand All @@ -3393,11 +3396,11 @@
transition: background 80ms ease;
}

.v2-login__submit:hover:not(:disabled) {
.v2-root button.v2-login__submit:hover:not(:disabled) {
background: var(--v2-accent-strong);
}

.v2-login__submit:disabled {
.v2-root button.v2-login__submit:disabled {
background: var(--v2-border-strong);
cursor: not-allowed;
}
Expand Down
Loading