Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
adding approval gate for closed beta#56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
cb8b4ee3618e795d0704ff8ed72870d2dd2d36d363File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ALTER TABLE public.users ADD COLUMN IF NOT EXISTS is_approved BOOLEAN NOT NULL DEFAULT false; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { signSession, SESSION_MAX_AGE } from '@/lib/sessionToken'; | ||
| const API_URL = process.env.NEXT_PUBLIC_API_URL; | ||
| export async function POST(request: NextRequest) { | ||
| if (!API_URL) { | ||
| return NextResponse.json({ error: 'NEXT_PUBLIC_API_URL not configured' }, { status: 500 }); | ||
| } | ||
| const { userId } = await request.json(); | ||
| if (!userId || typeof userId !== 'string') { | ||
| return NextResponse.json({ error: 'Missing userId' }, { status: 400 }); | ||
| } | ||
| // Verify with the backend that the user exists and is approved. | ||
| let approved = false; | ||
| try { | ||
| const controller = new AbortController(); | ||
| const timeout = setTimeout(() => controller.abort(), 3000); | ||
| let res: Response; | ||
| try { | ||
| res = await fetch(`${API_URL}/auth/me?user_id=${encodeURIComponent(userId)}`, { signal: controller.signal }); | ||
| } finally { | ||
| clearTimeout(timeout); | ||
| } | ||
| if (!res.ok) { | ||
| return NextResponse.json({ error: 'User not found' }, { status: 401 }); | ||
| } | ||
| const data = await res.json(); | ||
| approved = data.is_approved === true; | ||
| } catch { | ||
| return NextResponse.json({ error: 'Backend unreachable' }, { status: 502 }); | ||
| } | ||
| if (!approved) { | ||
| return NextResponse.json({ error: 'Not approved' }, { status: 403 }); | ||
| } | ||
| const token = await signSession(userId); | ||
| const response = NextResponse.json({ ok: true }); | ||
| response.cookies.set('sapling_session', token, { | ||
| httpOnly: true, | ||
| sameSite: 'lax', | ||
| path: '/', | ||
| maxAge: SESSION_MAX_AGE, | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return response; | ||
| } | ||
| export async function DELETE() { | ||
| const response = NextResponse.json({ ok: true }); | ||
| response.cookies.set('sapling_session', '', { httpOnly: true, maxAge: 0, path: '/' }); | ||
| return response; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| 'use client'; | ||
| import { useRouter } from 'next/navigation'; | ||
| import Image from 'next/image'; | ||
| import { useUser } from '@/context/UserContext'; | ||
| export default function PendingPage() { | ||
| const router = useRouter(); | ||
| const { signOut } = useUser(); | ||
| async function handleSignOut() { | ||
| await signOut(); | ||
| router.replace('/signin'); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return ( | ||
| <div style={{ | ||
| minHeight: '100vh', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| background: 'var(--bg-base, #0d1117)', | ||
| color: 'var(--brand-text1, #e6edf3)', | ||
| padding: '24px', | ||
| }}> | ||
| <Image | ||
| src="/sapling-word-icon.png" | ||
| alt="Sapling" | ||
| width={140} | ||
| height={40} | ||
| style={{ marginBottom: '40px', objectFit: 'contain' }} | ||
| /> | ||
| <h1 style={{ | ||
| fontSize: '28px', | ||
| fontWeight: 600, | ||
| marginBottom: '12px', | ||
| textAlign: 'center', | ||
| letterSpacing: '-0.02em', | ||
| }}> | ||
| You're on the waitlist | ||
| </h1> | ||
| <p style={{ | ||
| fontSize: '15px', | ||
| color: 'var(--brand-text2, #8b949e)', | ||
| textAlign: 'center', | ||
| maxWidth: '340px', | ||
| lineHeight: 1.6, | ||
| marginBottom: '40px', | ||
| }}> | ||
| We'll reach out when your access is approved. | ||
| </p> | ||
| <button | ||
| onClick={handleSignOut} | ||
| style={{ | ||
| background: 'transparent', | ||
| border: '1px solid rgba(255,255,255,0.15)', | ||
| color: 'var(--brand-text2, #8b949e)', | ||
| padding: '10px 24px', | ||
| borderRadius: '8px', | ||
| fontSize: '14px', | ||
| cursor: 'pointer', | ||
| transition: 'border-color 0.2s, color 0.2s', | ||
| }} | ||
| onMouseEnter={e => { | ||
| (e.currentTarget as HTMLButtonElement).style.borderColor = 'rgba(255,255,255,0.35)'; | ||
| (e.currentTarget as HTMLButtonElement).style.color = 'var(--brand-text1, #e6edf3)'; | ||
| }} | ||
| onMouseLeave={e => { | ||
| (e.currentTarget as HTMLButtonElement).style.borderColor = 'rgba(255,255,255,0.15)'; | ||
| (e.currentTarget as HTMLButtonElement).style.color = 'var(--brand-text2, #8b949e)'; | ||
| }} | ||
| > | ||
| Sign out | ||
| </button> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| 'use client'; | ||
| import { Suspense } from 'react'; | ||
| import { useSearchParams } from 'next/navigation'; | ||
| import Image from 'next/image'; | ||
| const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:5000'; | ||
| function SignInInner() { | ||
| const searchParams = useSearchParams(); | ||
| const error = searchParams.get('error'); | ||
| return ( | ||
| <div style={{ | ||
| minHeight: '100vh', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| background: 'var(--bg-base, #0d1117)', | ||
| padding: '24px', | ||
| }}> | ||
| <Image | ||
| src="/sapling-word-icon.png" | ||
| alt="Sapling" | ||
| width={140} | ||
| height={40} | ||
| style={{ marginBottom: '40px', objectFit: 'contain' }} | ||
| /> | ||
| {error === 'not_approved' && ( | ||
| <div style={{ | ||
| marginBottom: '24px', | ||
| padding: '12px 20px', | ||
| borderRadius: '8px', | ||
| background: 'rgba(234, 179, 8, 0.12)', | ||
| border: '1px solid rgba(234, 179, 8, 0.3)', | ||
| color: '#ca8a04', | ||
| fontSize: '14px', | ||
| textAlign: 'center', | ||
| maxWidth: '360px', | ||
| }}> | ||
| Your account is pending approval. | ||
| </div> | ||
| )} | ||
| <div style={{ | ||
| background: 'var(--bg-panel, #161b22)', | ||
| border: '1px solid rgba(255,255,255,0.08)', | ||
| borderRadius: '16px', | ||
| padding: '40px', | ||
| width: '100%', | ||
| maxWidth: '380px', | ||
| }}> | ||
| <h2 style={{ | ||
| fontSize: '22px', | ||
| fontWeight: 600, | ||
| color: 'var(--brand-text1, #e6edf3)', | ||
| textAlign: 'center', | ||
| marginBottom: '8px', | ||
| letterSpacing: '-0.02em', | ||
| }}> | ||
| Welcome back | ||
| </h2> | ||
| <p style={{ | ||
| fontSize: '14px', | ||
| color: 'var(--brand-text2, #8b949e)', | ||
| textAlign: 'center', | ||
| marginBottom: '28px', | ||
| }}> | ||
| Sign in to continue | ||
| </p> | ||
| <button | ||
| onClick={() => { window.location.href = `${API_URL}/api/auth/google`; }} | ||
| style={{ | ||
| width: '100%', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| gap: '12px', | ||
| padding: '12px 20px', | ||
| background: '#ffffff', | ||
| border: '1px solid #d1d5db', | ||
| borderRadius: '10px', | ||
| fontSize: '14px', | ||
| fontWeight: 500, | ||
| color: '#111827', | ||
| cursor: 'pointer', | ||
| transition: 'box-shadow 0.2s', | ||
| }} | ||
| onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.12)'; }} | ||
| onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.boxShadow = 'none'; }} | ||
| > | ||
| <svg width="18" height="18" viewBox="0 0 24 24"> | ||
| <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" /> | ||
| <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" /> | ||
| <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" /> | ||
| <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" /> | ||
| </svg> | ||
| Continue with Google | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| export default function SignInPage() { | ||
| return ( | ||
| <Suspense fallback={<div style={{ minHeight: '100vh', background: '#0d1117' }} />}> | ||
| <SignInInner /> | ||
| </Suspense> | ||
| ); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the obvious inline comment to match repo style.
Line 12 explains directly-following code and is not non-obvious logic.
As per coding guidelines, "No docstrings or comments unless the logic is non-obvious".
🤖 Prompt for AI Agents