import { useMemo, useState } from 'react';
import type { FormEvent } from 'react';
import { Link, Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { AlertCircle, ArrowRight, Github, Loader2, Mail, User2 } from 'lucide-react';
import {
AuthError,
getAuthBootstrap,
startGoogleSignIn,
signInWithEmail,
signUpWithEmail,
startGitHubSignIn,
} from '@/lib/auth-client';
import { useAuthSession } from '@/lib/use-auth-session';
function sanitizeRedirect(raw: string | null): string {
if (!raw) {
return '/projects';
}
const value = raw.trim();
if (!value.startsWith('/') || value.startsWith('//')) {
return '/projects';
}
return value;
}
function buildOAuthCallbackURL(path: string): string {
const cleanPath = sanitizeRedirect(path);
return `${window.location.origin}${cleanPath}`;
}
function GoogleMark() {
return (
);
}
function AuthCard({
title,
subtitle,
children,
}: {
title: string;
subtitle: string;
children: React.ReactNode;
}) {
return (
{title}
{subtitle}
{children}
);
}
function AuthLayout({ children }: { children: React.ReactNode }) {
return (
Containr
Self-hosted container platform
{children}
Sessions are cookie-based ยท first account becomes platform owner
);
}
function AuthErrorNotice({ message }: { message: string }) {
return (
);
}
function AuthInfoNotice({ message }: { message: string }) {
return (
{message}
);
}
export function SignInPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [searchParams] = useSearchParams();
const redirectPath = useMemo(() => sanitizeRedirect(searchParams.get('redirect')), [searchParams]);
const sessionQuery = useAuthSession();
const bootstrapQuery = useQuery({
queryKey: ['auth-bootstrap'],
queryFn: getAuthBootstrap,
});
const providers = bootstrapQuery.data?.providers ?? [];
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const [info, setInfo] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
if (sessionQuery.data) {
return ;
}
if (bootstrapQuery.data?.mode === 'register') {
return ;
}
const submitEmailPassword = async (event: FormEvent) => {
event.preventDefault();
setError(null);
setInfo(null);
setIsSubmitting(true);
try {
await signInWithEmail(email.trim(), password);
await queryClient.invalidateQueries({ queryKey: ['auth-session'] });
navigate(redirectPath, { replace: true });
} catch (exception) {
const message = exception instanceof AuthError ? exception.message : 'Failed to sign in';
setError(message);
} finally {
setIsSubmitting(false);
}
};
const signInWithGitHubProvider = async () => {
setError(null);
try {
await startGitHubSignIn(buildOAuthCallbackURL(redirectPath));
} catch (exception) {
const message = exception instanceof AuthError ? exception.message : 'GitHub sign-in failed';
setError(message);
}
};
const signInWithGoogleProvider = async () => {
setError(null);
try {
await startGoogleSignIn(buildOAuthCallbackURL(redirectPath));
} catch (exception) {
const message = exception instanceof AuthError ? exception.message : 'Google sign-in failed';
setError(message);
}
};
return (
0 ? 'Use email/password or a connected provider.' : 'Use your email and password.'}
>
{error ? : null}
{info ? : null}
{bootstrapQuery.isLoading ? : null}
{bootstrapQuery.isError ? : null}
{providers.length > 0 ? (
<>
{providers.includes('github') ? (
) : null}
{providers.includes('google') ? (
) : null}
>
) : null}
Need access?
Ask platform owner to create account.
);
}
export function SignUpPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [searchParams] = useSearchParams();
const redirectPath = useMemo(() => sanitizeRedirect(searchParams.get('redirect')), [searchParams]);
const sessionQuery = useAuthSession();
const bootstrapQuery = useQuery({
queryKey: ['auth-bootstrap'],
queryFn: getAuthBootstrap,
});
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
if (sessionQuery.data) {
return ;
}
// Registration stays available when the owner reopened it in Settings.
if (bootstrapQuery.data?.mode === 'login' && !bootstrapQuery.data.signupEnabled) {
return ;
}
const submitSignUp = async (event: FormEvent) => {
event.preventDefault();
setError(null);
setIsSubmitting(true);
try {
await signUpWithEmail(name.trim(), email.trim(), password);
await queryClient.invalidateQueries({ queryKey: ['auth-session'] });
navigate(redirectPath, { replace: true });
} catch (exception) {
const message = exception instanceof AuthError ? exception.message : 'Failed to create account';
setError(message);
} finally {
setIsSubmitting(false);
}
};
return (
{error ? : null}
{bootstrapQuery.isLoading ? : null}
{bootstrapQuery.isError ? : null}
Already signed up?
Go to sign in
);
}