From d32d0f5c03ed38773a73fadb5866303831d7fa35 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Tue, 17 Feb 2026 16:51:47 +0000 Subject: [PATCH] refactored Authentication --- src/apps/officer-web/App.backup.tsx | 73 +++++ src/apps/officer-web/App.tsx | 98 ++++--- .../ForgotPassword/ForgotPassword.tsx | 90 ++++++ .../ForgotPassword/ResetPassword.tsx | 75 +++++ .../Authentication/ForgotPassword/index.tsx | 2 + .../ForgotPassword/useResetPassword.ts | 72 +++++ .../Authentication/LandingPage/Bootstrap.tsx | 63 ++++ .../LandingPage/LandingPage.tsx | 27 ++ .../Authentication/LandingPage/Login.tsx | 103 +++++++ .../Authentication/LandingPage/index.tsx | 1 + .../Layout/AuthenticationLayout.tsx | 21 ++ .../Authentication/Layout/Background.tsx | 19 ++ .../Layout}/DuckAvatar/DebugPanel.tsx | 0 .../Layout}/DuckAvatar/DuckAvatar.tsx | 4 +- .../Layout}/DuckAvatar/DuckModel.tsx | 0 .../Layout}/DuckAvatar/index.tsx | 0 .../Screens/Authentication/Layout/index.tsx | 1 + .../index.tsx => Authentication/Signout.tsx} | 1 + .../Screens/Authentication/Verify.tsx | 94 ++++++ .../VerifyScreen/VerifyScreen.tsx | 107 +++++++ .../Authentication/VerifyScreen/index.tsx | 1 + .../VerifyScreen/useVerifyScreen.ts | 83 ++++++ .../Screens/Authentication/index.tsx | 14 + .../Screens/Dashboard/Chat/ChatList/index.tsx | 11 + .../Screens/LandingPage/Layout.tsx | 2 +- src/apps/officer-web/state/useLandingPage.ts | 15 + .../migrations/0001_fat_blonde_phantom.sql | 1 + .../migrations/meta/0001_snapshot.json | 275 ++++++++++++++++++ .../officer_db/migrations/meta/_journal.json | 7 + src/databases/officer_db/src/schema/users.ts | 1 + src/servers/api/auth/auth.ts | 2 + src/servers/api/auth/bootstrap.ts | 63 ++++ src/servers/api/auth/forgot-password.ts | 2 +- src/servers/api/auth/verify-token.ts | 10 +- .../landing-page-data/landing-page-data.ts | 10 + .../api/server-settings/server-settings.ts | 6 - src/servers/hono.ts | 2 + src/workspaces/components/Card.tsx | 2 +- .../components/Logos/DevSvgLogo.tsx | 0 .../components/Logos/PastilhasSvgLogo.tsx | 0 .../components/Logos/index.tsx | 0 .../components/PixelGrid.tsx | 0 src/workspaces/hooks/src/useAuth/useAuth.ts | 6 - src/workspaces/types/globals.d.ts | 20 ++ tsconfig.json | 4 + 45 files changed, 1322 insertions(+), 66 deletions(-) create mode 100644 src/apps/officer-web/App.backup.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/ForgotPassword/ForgotPassword.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/ForgotPassword/ResetPassword.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/ForgotPassword/index.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/ForgotPassword/useResetPassword.ts create mode 100644 src/apps/officer-web/Screens/Authentication/LandingPage/Bootstrap.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/LandingPage/LandingPage.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/LandingPage/Login.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/LandingPage/index.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/Layout/AuthenticationLayout.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/Layout/Background.tsx rename src/apps/officer-web/Screens/{LandingPage/components => Authentication/Layout}/DuckAvatar/DebugPanel.tsx (100%) rename src/apps/officer-web/Screens/{LandingPage/components => Authentication/Layout}/DuckAvatar/DuckAvatar.tsx (95%) rename src/apps/officer-web/Screens/{LandingPage/components => Authentication/Layout}/DuckAvatar/DuckModel.tsx (100%) rename src/apps/officer-web/Screens/{LandingPage/components => Authentication/Layout}/DuckAvatar/index.tsx (100%) create mode 100644 src/apps/officer-web/Screens/Authentication/Layout/index.tsx rename src/apps/officer-web/Screens/{Dashboard/SignoutScreen/index.tsx => Authentication/Signout.tsx} (99%) create mode 100644 src/apps/officer-web/Screens/Authentication/Verify.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/VerifyScreen/VerifyScreen.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/VerifyScreen/index.tsx create mode 100644 src/apps/officer-web/Screens/Authentication/VerifyScreen/useVerifyScreen.ts create mode 100644 src/apps/officer-web/Screens/Authentication/index.tsx create mode 100644 src/apps/officer-web/Screens/Dashboard/Chat/ChatList/index.tsx create mode 100644 src/apps/officer-web/state/useLandingPage.ts create mode 100644 src/databases/officer_db/migrations/0001_fat_blonde_phantom.sql create mode 100644 src/databases/officer_db/migrations/meta/0001_snapshot.json create mode 100644 src/servers/api/auth/bootstrap.ts create mode 100644 src/servers/api/landing-page-data/landing-page-data.ts rename src/{apps/officer-web/Screens/LandingPage => workspaces}/components/Logos/DevSvgLogo.tsx (100%) rename src/{apps/officer-web/Screens/LandingPage => workspaces}/components/Logos/PastilhasSvgLogo.tsx (100%) rename src/{apps/officer-web/Screens/LandingPage => workspaces}/components/Logos/index.tsx (100%) rename src/{apps/officer-web/Screens/LandingPage => workspaces}/components/PixelGrid.tsx (100%) diff --git a/src/apps/officer-web/App.backup.tsx b/src/apps/officer-web/App.backup.tsx new file mode 100644 index 00000000..9ccb2743 --- /dev/null +++ b/src/apps/officer-web/App.backup.tsx @@ -0,0 +1,73 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router'; +import { LandingPage, AuthLayout } from './Screens/LandingPage'; +import { Home } from './Screens/Dashboard/Home'; +import { ProfileSettings } from './Screens/Dashboard/Settings/ProfileSettings'; +import { ClaudeChat, OpenCodeChat, NewChat } from './Screens/Dashboard/Chat'; +import { Plans } from './Screens/Dashboard/Plans'; +import { Skills } from './Screens/Dashboard/Skills'; +import { Tasks } from './Screens/Dashboard/Tasks'; +import { Processes } from './Screens/Dashboard/Processes'; +import { TaskLogs } from './Screens/Dashboard/TaskLogs'; +import { SignoutScreen } from './Screens/Dashboard/SignoutScreen'; +import { Screen as Files } from 'plugins/FileBrowser/client'; +import { Screen as Terminal } from 'plugins/Terminal/client'; +import { AISettings } from './Screens/Dashboard/Settings/AISettings'; +import { ServerSettings } from './Screens/Dashboard/Settings/ServerSettings'; +import { ResourceSettings } from './Screens/Dashboard/Settings/ResourceSettings'; +import { OnboardingAdmin } from './Screens/Dashboard/OnboardingAdmin'; + +import { ChatList } from './Screens/Dashboard/Chat/ChatList'; + +import { useAuth } from 'hooks/useAuth'; +import { useServerSettings } from '@/state/useServerSettings'; +import { useInitialData } from '@/state/useInitialData'; + +export function App() { + const { isLoading, isAuthenticated } = useAuth(); + const { onboardingComplete, plugins, isLoading: isServerSettingsLoading } = useServerSettings(); + useInitialData(); + + if (isLoading || isServerSettingsLoading) return null; + + return ( + + {!isAuthenticated && ( + + } /> + } /> + } /> + + )} + {isAuthenticated && !onboardingComplete && ( + + } /> + } /> + } /> + + )} + {isAuthenticated && onboardingComplete && ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + )} + + ); +} diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx index 26b88784..f3cb7f95 100644 --- a/src/apps/officer-web/App.tsx +++ b/src/apps/officer-web/App.tsx @@ -1,24 +1,12 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router'; +import { BrowserRouter, Routes, Route, Navigate, Link } from 'react-router'; +import * as Authentication from './Screens/Authentication'; +// import { Home } from './Screens/Dashboard/Home'; + +// import { ChatList } from './Screens/Dashboard/Chat/ChatList'; + import { useAuth } from 'hooks/useAuth'; import { useServerSettings } from '@/state/useServerSettings'; import { useInitialData } from '@/state/useInitialData'; -import { LandingPage, AuthLayout } from './Screens/LandingPage'; -import { Home } from './Screens/Dashboard/Home'; -import { ProfileSettings } from './Screens/Dashboard/Settings/ProfileSettings'; -import { ClaudeChat, OpenCodeChat, NewChat } from './Screens/Dashboard/Chat'; -import { Screen as ClaudeSessions } from 'plugins/ChatHistory/client'; -import { Plans } from './Screens/Dashboard/Plans'; -import { Skills } from './Screens/Dashboard/Skills'; -import { Tasks } from './Screens/Dashboard/Tasks'; -import { Processes } from './Screens/Dashboard/Processes'; -import { TaskLogs } from './Screens/Dashboard/TaskLogs'; -import { SignoutScreen } from './Screens/Dashboard/SignoutScreen'; -import { Screen as Files } from 'plugins/FileBrowser/client'; -import { Screen as Terminal } from 'plugins/Terminal/client'; -import { AISettings } from './Screens/Dashboard/Settings/AISettings'; -import { ServerSettings } from './Screens/Dashboard/Settings/ServerSettings'; -import { ResourceSettings } from './Screens/Dashboard/Settings/ResourceSettings'; -import { OnboardingAdmin } from './Screens/Dashboard/OnboardingAdmin'; export function App() { const { isLoading, isAuthenticated } = useAuth(); @@ -30,42 +18,56 @@ export function App() { return ( {!isAuthenticated && ( - - } /> - } /> - } /> - - )} - {isAuthenticated && !onboardingComplete && ( - - } /> - } /> - } /> - + + + } /> + } /> + } /> + } /> + } /> + + )} + {/* {isAuthenticated && !onboardingComplete && ( */} + {/* */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* */} + {/* )} */} {isAuthenticated && onboardingComplete && ( - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + {/* } /> */} + } /> } /> )} ); } + + +const HomeScreen = () => { + return ( +
+

Home

+ Signout +
+ ); +}; diff --git a/src/apps/officer-web/Screens/Authentication/ForgotPassword/ForgotPassword.tsx b/src/apps/officer-web/Screens/Authentication/ForgotPassword/ForgotPassword.tsx new file mode 100644 index 00000000..605f1a07 --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/ForgotPassword/ForgotPassword.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { Link } from 'react-router'; +import { toast } from 'sonner'; +import { useClient } from 'hooks/useClient'; +import { useForm } from 'hooks/useForm'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/Card'; + +export function ForgotPassword() { + const client = useClient('/api/auth'); + const [isSubmitting, setIsSubmitting] = useState(false); + const [done, setDone] = useState(false); + const { formRef, state, isValid } = useForm({}, (s) => !!s.email); + + const handleSubmit = async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!isValid || isSubmitting) return; + + setIsSubmitting(true); + try { + await client.post('/forgot-password', { email: state.email!.trim() }); + setDone(true); + } catch (ex) { + const error = ex as { message?: string }; + toast.error(error.message || 'Something went wrong. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {done ? ( +
+

Check your email

+
+

+ A password reset link has been sent to {state.email}. +

+

Click it to reset your password.

+
+
+ ) : ( + <> +
+
Forgot Password
+
Enter your email to receive a reset link
+
+ +
+ + +
+ +
+ +
+

+ + Back to login + +

+
+
+ + )} +
+ ); +} + +type ForgotPasswordFormState = { + email?: string; +}; diff --git a/src/apps/officer-web/Screens/Authentication/ForgotPassword/ResetPassword.tsx b/src/apps/officer-web/Screens/Authentication/ForgotPassword/ResetPassword.tsx new file mode 100644 index 00000000..5f11b636 --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/ForgotPassword/ResetPassword.tsx @@ -0,0 +1,75 @@ +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/Card'; +import { cn } from '@/lib/utils'; +import { useResetPassword } from './useResetPassword'; + +export function ResetPassword() { + const { formRef, isValid, tokenStatus, isSubmitting, handleSubmit } = useResetPassword(); + const loading = tokenStatus === 'loading'; + const invalid = tokenStatus === 'invalid'; + const hideform = loading || invalid; + + return ( + <> + {loading && ( + +
Verifying...
+
+ )} + + {invalid && ( + +
+
Invalid Link
+
This reset link is invalid or has expired.
+
+
+ )} + + +
+
Reset Password
+
Enter your new password
+
+ +
+
+ + + +
+ +
+ +
+
+
+ + ); +} diff --git a/src/apps/officer-web/Screens/Authentication/ForgotPassword/index.tsx b/src/apps/officer-web/Screens/Authentication/ForgotPassword/index.tsx new file mode 100644 index 00000000..4ce8e836 --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/ForgotPassword/index.tsx @@ -0,0 +1,2 @@ +export { ForgotPassword } from './ForgotPassword'; +export { ResetPassword } from './ResetPassword'; diff --git a/src/apps/officer-web/Screens/Authentication/ForgotPassword/useResetPassword.ts b/src/apps/officer-web/Screens/Authentication/ForgotPassword/useResetPassword.ts new file mode 100644 index 00000000..80944bc8 --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/ForgotPassword/useResetPassword.ts @@ -0,0 +1,72 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { toast } from 'sonner'; +import { useMounted } from 'hooks/useMounted'; +import { useForm } from 'hooks/useForm'; +import { useClient } from 'hooks/useClient'; + +export const useResetPassword = () => { + const isMounted = useMounted(); + const navigate = useNavigate(); + const client = useClient('/api/auth'); + const { state, formRef, isValid } = useForm({}, validateForm); + + const [verificationCode] = useState(() => new URL(window.location.href).searchParams.get('verificationCode') ?? ''); + const [tokenStatus, setTokenStatus] = useState('loading'); + const [isSubmitting, setIsSubmitting] = useState(false); + + const verifyToken = async () => { + if (!verificationCode) return; + try { + const data = await client.post<{ ok: boolean }>('/verify-token', { verificationCode }); + setTokenStatus(data.ok ? 'valid' : 'invalid'); + } catch { + setTokenStatus('invalid'); + } + }; + + useEffect(() => { + if (!isMounted) return; + if (!verificationCode) { + setTokenStatus('invalid'); + return; + } + verifyToken(); + }, [isMounted]); + + const handleSubmit = async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!isValid || isSubmitting) return; + + setIsSubmitting(true); + try { + await client.post('/reset-password', { + password: state.password, + verificationCode, + }); + toast.success('Password reset successfully. Please sign in.'); + navigate('/'); + } catch (ex) { + const error = ex as { message?: string }; + toast.error(error.message || 'Failed to reset password. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + return { formRef, isValid, tokenStatus, isSubmitting, handleSubmit }; +}; + +type ResetPasswordFormState = { + password?: string; + confirmPassword?: string; +}; + +type TokenStatus = 'loading' | 'valid' | 'invalid'; + +const validateForm = (state: Partial) => { + const { password, confirmPassword } = state; + if (!password || !confirmPassword) return false; + if (password !== confirmPassword) return false; + return true; +}; diff --git a/src/apps/officer-web/Screens/Authentication/LandingPage/Bootstrap.tsx b/src/apps/officer-web/Screens/Authentication/LandingPage/Bootstrap.tsx new file mode 100644 index 00000000..4725c0e4 --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/LandingPage/Bootstrap.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import { toast } from 'sonner'; +import { useClient } from 'hooks/useClient'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/Card'; +import { useForm } from 'hooks/useForm'; + +export function Bootstrap() { + const authClient = useClient('/api/auth'); + const [isSubmitting, setIsSubmitting] = useState(false); + const [done, setDone] = useState(false); + const { formRef, state, isValid } = useForm({}, (state) => !!state.email); + + const handleBootstrap = async () => { + const trimmed = state.email.trim(); + if (!trimmed || isSubmitting) return; + + setIsSubmitting(true); + try { + await authClient.post('/bootstrap', { email: trimmed }); + setDone(true); + } catch (ex) { + const error = ex as { message?: string }; + toast.error(error.message || 'Bootstrap failed. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {done ? ( +
+

Check your email

+
+

A verification link has been sent to {state.email}.

+

Click it to activate your account.

+
+
+ ) : ( + <> +

Welcome, Admin

+

Enter your email to create your administrator account.

+
+ + +
+ + )} +
+ ); +} diff --git a/src/apps/officer-web/Screens/Authentication/LandingPage/LandingPage.tsx b/src/apps/officer-web/Screens/Authentication/LandingPage/LandingPage.tsx new file mode 100644 index 00000000..07a97b4e --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/LandingPage/LandingPage.tsx @@ -0,0 +1,27 @@ +import { OfficerSvgLogo } from '@/components/Logos'; +import { useLandingPage } from '@/state/useLandingPage'; +import { Bootstrap } from './Bootstrap'; +import { Login } from './Login'; + +export function LandingPage() { + const { registrationOpen, isLoading } = useLandingPage(); + if (isLoading) return null; + + return ( +
+
+
+ +
+
+ +
+
+ +
+ {registrationOpen ? : } +
+
+ ); +} + diff --git a/src/apps/officer-web/Screens/Authentication/LandingPage/Login.tsx b/src/apps/officer-web/Screens/Authentication/LandingPage/Login.tsx new file mode 100644 index 00000000..bacc31c7 --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/LandingPage/Login.tsx @@ -0,0 +1,103 @@ +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Link } from 'react-router'; +import { Card } from '@/components/Card'; +import { useNavigate } from 'react-router'; +import { useState } from 'react'; +import { toast } from 'sonner'; +import { useForm } from 'hooks/useForm'; +import { useAuth } from 'hooks/useAuth'; + +export function Login() { + const navigate = useNavigate(); + const [isSubmitting, setIsSubmitting] = useState(false); + const { state, formRef, update, isValid } = useForm({}, validateForm); + const { signin } = useAuth(); + + const handleSubmit = async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!isValid || isSubmitting) return; + + setIsSubmitting(true); + try { + await signin({ email: state.email!, password: state.password! }); + window.location.href = '/'; + } catch (ex) { + const error = ex as { message?: string }; + toast.error(error.message || 'Login failed. Please try again.'); + update({ ...state, password: '' }); + } finally { + setIsSubmitting(false); + } + }; + + return ( + +
+
Welcome Back
+
Sign in to your account
+
+ +
+ + + + +
+ +
+ +
+ {/*

*/} + {/* Don't have an account?{' '} */} + {/* */} + {/* Register */} + {/* */} + {/*

*/} +

+ + Forgot password? + +

+
+
+
+ ); +} + +type LoginFormState = { + email?: string; + password?: string; +}; + +const validateForm = (state: Partial) => { + const { email, password } = state; + if (!email || !password) return false; + return true; +}; + diff --git a/src/apps/officer-web/Screens/Authentication/LandingPage/index.tsx b/src/apps/officer-web/Screens/Authentication/LandingPage/index.tsx new file mode 100644 index 00000000..49287ad1 --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/LandingPage/index.tsx @@ -0,0 +1 @@ +export * from './LandingPage'; diff --git a/src/apps/officer-web/Screens/Authentication/Layout/AuthenticationLayout.tsx b/src/apps/officer-web/Screens/Authentication/Layout/AuthenticationLayout.tsx new file mode 100644 index 00000000..cf503f6b --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/Layout/AuthenticationLayout.tsx @@ -0,0 +1,21 @@ +import { Background } from './Background'; +type AuthenticationLayoutProps = { + children?: React.ReactNode; +}; +export function AuthenticationLayout({ children }: AuthenticationLayoutProps) { + return ( +
+
+ +
+
+ {children} +
+
+
+ +
+ ); +}; + + diff --git a/src/apps/officer-web/Screens/Authentication/Layout/Background.tsx b/src/apps/officer-web/Screens/Authentication/Layout/Background.tsx new file mode 100644 index 00000000..b9511e7e --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/Layout/Background.tsx @@ -0,0 +1,19 @@ +import { DuckAvatar } from "./DuckAvatar"; +import { PixelGrid } from "@/components/PixelGrid"; +import landscapebg from './landscape1.jpg'; + +export function Background() { + return ( +
+ + +
+ ); +}; diff --git a/src/apps/officer-web/Screens/LandingPage/components/DuckAvatar/DebugPanel.tsx b/src/apps/officer-web/Screens/Authentication/Layout/DuckAvatar/DebugPanel.tsx similarity index 100% rename from src/apps/officer-web/Screens/LandingPage/components/DuckAvatar/DebugPanel.tsx rename to src/apps/officer-web/Screens/Authentication/Layout/DuckAvatar/DebugPanel.tsx diff --git a/src/apps/officer-web/Screens/LandingPage/components/DuckAvatar/DuckAvatar.tsx b/src/apps/officer-web/Screens/Authentication/Layout/DuckAvatar/DuckAvatar.tsx similarity index 95% rename from src/apps/officer-web/Screens/LandingPage/components/DuckAvatar/DuckAvatar.tsx rename to src/apps/officer-web/Screens/Authentication/Layout/DuckAvatar/DuckAvatar.tsx index 9007b3d5..f0f90e83 100644 --- a/src/apps/officer-web/Screens/LandingPage/components/DuckAvatar/DuckAvatar.tsx +++ b/src/apps/officer-web/Screens/Authentication/Layout/DuckAvatar/DuckAvatar.tsx @@ -72,12 +72,12 @@ export function DuckAvatar({ showDebug, fullControlMode, currentSection }: DuckA const [isLoaded, setIsLoaded] = useState(false); - const heroClasses = 'fixed bottom-0 left-1/2 -translate-x-1/2 w-[60vh] h-[75vh]'; + const heroClasses = 'fixed top-12 md:top-auto md:bottom-0 left-1/2 -translate-x-1/2 w-[60vh] h-[75vh]'; const miniClasses = 'fixed bottom-4 right-4 w-[20vh] h-[25vh]'; return (
{ return null; }; + diff --git a/src/apps/officer-web/Screens/Authentication/Verify.tsx b/src/apps/officer-web/Screens/Authentication/Verify.tsx new file mode 100644 index 00000000..5a7e16ab --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/Verify.tsx @@ -0,0 +1,94 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { toast } from 'sonner'; +import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { useForm } from 'hooks/useForm'; +import { useAuth } from 'hooks/useAuth'; +import { useClient } from 'hooks/useClient'; + +export const Verify = () => { + const navigate = useNavigate(); + const client = useClient('/api/auth'); + const { verify } = useAuth(); + const { state, formRef, update, isValid } = useForm({}, validateForm); + + const [verificationCode] = useState(() => new URL(window.location.href).searchParams.get('verificationCode') ?? ''); + const [tokenStatus, setTokenStatus] = useState('loading'); + const [isSubmitting, setIsSubmitting] = useState(false); + const [verified, setVerified] = useState(false); + const [resending, setResending] = useState(false); + const [resent, setResent] = useState(false); + + const handleResend = async () => { + if (!email || resending) return; + setResending(true); + try { + await client.post('/resend-verification', { email }); + setResent(true); + } catch (ex) { + const error = ex as { message?: string }; + toast.error(error.message || 'Failed to resend. Please try again.'); + } finally { + setResending(false); + } + }; + useEffect(() => { + // if (!open || !verificationCode) { + // if (!verificationCode) setTokenStatus('invalid'); + // return; + // } + // + // window.history.replaceState(null, '', '/auth/verify'); + // + // client + // .post<{ email: string }>('/verify-token', { verificationCode }) + // .then((data) => { + // setEmail(data.email); + // setTokenStatus('valid'); + // }) + // .catch(() => { + // // Try to decode email from expired token for resend + // try { + // const payload = JSON.parse(atob(verificationCode.split('.')[1]!)); + // if (payload.email) setEmail(payload.email); + // } catch { + // // ignore + // } + // setTokenStatus('invalid'); + // }); + }, [open]); + + // Redirect after successful verification + useEffect(() => { + // if (!verified) return; + // const timer = setTimeout(() => navigate('/'), 3000); + // return () => clearTimeout(timer); + }, [verified]); + + return ( +
+
+
Verify your account
+
Enter your verification code
+
+
+ ); +}; + + +const validateForm = (state: Partial) => { + const { name, password, confirmPassword } = state; + if (!name || !password || !confirmPassword) return false; + if (password !== confirmPassword) return false; + return true; +}; + +type VerifyFormState = { + name?: string; + password?: string; + confirmPassword?: string; +}; +type TokenStatus = 'loading' | 'valid' | 'invalid'; diff --git a/src/apps/officer-web/Screens/Authentication/VerifyScreen/VerifyScreen.tsx b/src/apps/officer-web/Screens/Authentication/VerifyScreen/VerifyScreen.tsx new file mode 100644 index 00000000..f2e7b05a --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/VerifyScreen/VerifyScreen.tsx @@ -0,0 +1,107 @@ +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/Card'; +import { cn } from '@/lib/utils'; +import { useVerifyScreen } from './useVerifyScreen'; + +export const VerifyScreen = () => { + const { formRef, isValid, tokenStatus, isSubmitting, handleSubmit } = useVerifyScreen(); + const loading = tokenStatus === 'loading'; + const invalid = tokenStatus === 'invalid'; + const hideform = loading || invalid; + + return ( + <> + {loading && ( + +
Verifying...
+
+ )} + + {invalid && ( + +
+
Invalid Link
+
This verification link is invalid or has expired.
+
+
+ )} + + +
+
Create Your Account
+
Set up your administrator profile
+
+ +
+ + + + + + +
+ + + +
+ +
+ +
+
+
+ + ); +}; diff --git a/src/apps/officer-web/Screens/Authentication/VerifyScreen/index.tsx b/src/apps/officer-web/Screens/Authentication/VerifyScreen/index.tsx new file mode 100644 index 00000000..9a19e608 --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/VerifyScreen/index.tsx @@ -0,0 +1 @@ +export * from './VerifyScreen'; diff --git a/src/apps/officer-web/Screens/Authentication/VerifyScreen/useVerifyScreen.ts b/src/apps/officer-web/Screens/Authentication/VerifyScreen/useVerifyScreen.ts new file mode 100644 index 00000000..8a9df7aa --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/VerifyScreen/useVerifyScreen.ts @@ -0,0 +1,83 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { toast } from 'sonner'; +import { useMounted } from 'hooks/useMounted'; +import { useForm } from 'hooks/useForm'; +import { useClient } from 'hooks/useClient'; + +export const useVerifyScreen = () => { + const isMounted = useMounted(); + const navigate = useNavigate(); + const client = useClient('/api/auth'); + const { state, formRef, update, isValid } = useForm({ email: '' }, validateForm); + + const [verificationCode] = useState(() => new URL(window.location.href).searchParams.get('verificationCode') ?? ''); + const [tokenStatus, setTokenStatus] = useState('loading'); + const [isSubmitting, setIsSubmitting] = useState(false); + + const verifyToken = async () => { + if (!verificationCode) return; + try { + const data = await client.post<{ ok: boolean; email: string }>('/verify-token', { verificationCode }); + if (data.ok) { + setTokenStatus('valid'); + requestAnimationFrame(() => update({ email: data.email })); + } else { + setTokenStatus('invalid'); + } + } catch { + setTokenStatus('invalid'); + } + }; + + useEffect(() => { + if (!isMounted) return; + if (!verificationCode) { + setTokenStatus('invalid'); + return; + } + verifyToken(); + }, [isMounted]); + + const handleSubmit = async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!isValid || isSubmitting) return; + + setIsSubmitting(true); + try { + await client.post('/bootstrap', { + token: verificationCode, + name: state.name, + username: state.username, + password: state.password, + confirmPassword: state.confirmPassword, + }); + toast.success('Account created. Please sign in.'); + navigate('/auth/login'); + } catch (ex) { + const error = ex as { message?: string }; + toast.error(error.message || 'Failed to create account. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + return { formRef, state, isValid, tokenStatus, isSubmitting, handleSubmit }; +}; + +type VerifyFormState = { + email?: string; + name?: string; + username?: string; + password?: string; + confirmPassword?: string; +}; + +type TokenStatus = 'loading' | 'valid' | 'invalid'; + +const validateForm = (state: Partial) => { + const { name, username, password, confirmPassword } = state; + if (!name || !username || !password || !confirmPassword) return false; + if (password !== confirmPassword) return false; + return true; +}; diff --git a/src/apps/officer-web/Screens/Authentication/index.tsx b/src/apps/officer-web/Screens/Authentication/index.tsx new file mode 100644 index 00000000..949ea65e --- /dev/null +++ b/src/apps/officer-web/Screens/Authentication/index.tsx @@ -0,0 +1,14 @@ +import { AuthenticationLayout } from './Layout'; +import { LandingPage } from './LandingPage'; +import { SignoutScreen } from './Signout'; +import { VerifyScreen } from './VerifyScreen'; +import { ForgotPassword, ResetPassword } from './ForgotPassword'; + +export { + AuthenticationLayout, + LandingPage, + SignoutScreen, + VerifyScreen, + ForgotPassword, + ResetPassword, +}; diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/ChatList/index.tsx b/src/apps/officer-web/Screens/Dashboard/Chat/ChatList/index.tsx new file mode 100644 index 00000000..598e3e1b --- /dev/null +++ b/src/apps/officer-web/Screens/Dashboard/Chat/ChatList/index.tsx @@ -0,0 +1,11 @@ +import { DashboardLayout } from '@/Screens/Dashboard/Layout'; + +export const ChatList = () => { + + return ( + +

ChatList

+
+ ); +}; + diff --git a/src/apps/officer-web/Screens/LandingPage/Layout.tsx b/src/apps/officer-web/Screens/LandingPage/Layout.tsx index 5eb7f22d..5024fd65 100644 --- a/src/apps/officer-web/Screens/LandingPage/Layout.tsx +++ b/src/apps/officer-web/Screens/LandingPage/Layout.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; import { useLocation, useNavigate } from 'react-router'; import { useAuth } from 'hooks/useAuth'; -import { PixelGrid } from './components/PixelGrid'; +import { PixelGrid } from '../../../../workspaces/components/PixelGrid'; import { DuckAvatar } from './components/DuckAvatar'; import { SignupModal } from './components/SignupModal'; import { LoginModal } from './components/LoginModal'; diff --git a/src/apps/officer-web/state/useLandingPage.ts b/src/apps/officer-web/state/useLandingPage.ts new file mode 100644 index 00000000..1294c7e3 --- /dev/null +++ b/src/apps/officer-web/state/useLandingPage.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import { useClient } from 'hooks/useClient'; +export const useLandingPage = () => { + const apiClient = useClient(); + + const { data: setupData, isLoading } = useQuery({ + queryKey: ['LANDING_PAGE_DATA'], + queryFn: () => apiClient.get<{ registrationOpen: boolean }>('/landing-page-data'), + }); + + return { + isLoading, + registrationOpen: setupData?.registrationOpen, + }; +}; diff --git a/src/databases/officer_db/migrations/0001_fat_blonde_phantom.sql b/src/databases/officer_db/migrations/0001_fat_blonde_phantom.sql new file mode 100644 index 00000000..55133f60 --- /dev/null +++ b/src/databases/officer_db/migrations/0001_fat_blonde_phantom.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "username" varchar(128); \ No newline at end of file diff --git a/src/databases/officer_db/migrations/meta/0001_snapshot.json b/src/databases/officer_db/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..c9e905d9 --- /dev/null +++ b/src/databases/officer_db/migrations/meta/0001_snapshot.json @@ -0,0 +1,275 @@ +{ + "id": "7c0d3634-8b25-41e9-b6f3-24fad2005532", + "prevId": "b9130f42-0743-4c4b-aaf1-dce52e708e22", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'Member'" + }, + "status": { + "name": "status", + "type": "user_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'Unverified'" + }, + "name": { + "name": "name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "varchar(512000)", + "primaryKey": false, + "notNull": false + }, + "password_changed_at": { + "name": "password_changed_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkeys": { + "name": "passkeys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey_challenges": { + "name": "passkey_challenges", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "challenge": { + "name": "challenge", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_passkey_challenges_created_at": { + "name": "idx_passkey_challenges_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "passkey_challenges_email_origin_pk": { + "name": "passkey_challenges_email_origin_pk", + "columns": [ + "email", + "origin" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_blacklist": { + "name": "token_blacklist", + "schema": "", + "columns": { + "jti": { + "name": "jti", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_token_blacklist_expires_at": { + "name": "idx_token_blacklist_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_roles": { + "name": "user_roles", + "schema": "public", + "values": [ + "Member", + "Admin", + "Owner", + "Super Admin" + ] + }, + "public.user_status": { + "name": "user_status", + "schema": "public", + "values": [ + "Unverified", + "Active", + "Prospect", + "Invited", + "Blocked", + "Banned", + "Deleted" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/databases/officer_db/migrations/meta/_journal.json b/src/databases/officer_db/migrations/meta/_journal.json index 72d2bf04..b8206cab 100644 --- a/src/databases/officer_db/migrations/meta/_journal.json +++ b/src/databases/officer_db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1770915839349, "tag": "0000_broken_gauntlet", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1771340427681, + "tag": "0001_fat_blonde_phantom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/databases/officer_db/src/schema/users.ts b/src/databases/officer_db/src/schema/users.ts index 02d346f6..224e7808 100644 --- a/src/databases/officer_db/src/schema/users.ts +++ b/src/databases/officer_db/src/schema/users.ts @@ -14,6 +14,7 @@ export const Users = pgTable('users', { role: userRolesEnum('role').default(USER_ROLES[0]), status: userStatusEnum('status').default(USER_STATUSES[0]), name: varchar('name', { length: 128 }), + username: varchar('username', { length: 128 }), avatar: varchar('avatar', { length: 512000 }), passwordChangedAt: bigint('password_changed_at', { mode: 'number' }), }); diff --git a/src/servers/api/auth/auth.ts b/src/servers/api/auth/auth.ts index 147ab0da..1a0d3300 100644 --- a/src/servers/api/auth/auth.ts +++ b/src/servers/api/auth/auth.ts @@ -17,6 +17,7 @@ import { resendVerificationHandler } from './resend-verification'; import { changePasswordHandler } from './change-password'; import { forgotPasswordHandler } from './forgot-password'; import { resetPasswordHandler } from './reset-password'; +import { bootstrapHandler } from './bootstrap'; import { usersMe } from './users-me'; import { passkeyRouter } from './passkey-router'; @@ -32,6 +33,7 @@ authRouter.get('/me', userMiddleware, usersMe); authRouter.post('/signin', signinRateLimiter, signinHandler); authRouter.post('/signout', userMiddleware, signoutHandler); authRouter.post('/signup', signupRateLimiter, signupHandler); +authRouter.post('/bootstrap', signupRateLimiter, bootstrapHandler); authRouter.post('/verify', verifyHandler); authRouter.post('/verify-token', verifyTokenHandler); authRouter.post('/resend-verification', resendVerificationHandler); diff --git a/src/servers/api/auth/bootstrap.ts b/src/servers/api/auth/bootstrap.ts new file mode 100644 index 00000000..225c3a05 --- /dev/null +++ b/src/servers/api/auth/bootstrap.ts @@ -0,0 +1,63 @@ +import type { Handler } from 'hono'; +import { sendMail } from 'emailer'; +import { officerdb, count, Users } from 'officerdb'; +import { sign, verify } from '@@/jwt'; +import argon2 from 'argon2'; +import * as errors from '@@/custom-errors'; +import { validatePassword } from './validate-password'; + +export const bootstrapHandler: Handler = async function (ctx) { + const body = ctx.get('body'); + const origin = ctx.get('origin'); + const token = body.token as string; + const email = body.email as string; + + const result = await officerdb.select({ count: count() }).from(Users); + const userCount = result[0]?.count ?? 0; + if (userCount > 0) throw errors.FORBIDDEN('Registration is closed'); + + if (!token) { + if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + throw errors.BAD_REQUEST('Invalid email address'); + } + + const verificationCode = await sign({ email }, '24h'); + const url = `${origin}/auth/verify?verificationCode=${verificationCode}`; + + await sendMail({ + template: 'VerifyAdmin', + subject: 'Verify your Officer account', + to: email, + data: { name: email, url }, + }); + + return ctx.json({ ok: true }); + } + + const payload = await verify(token).catch(() => null) as { email: string } | null; + if (!payload?.email) throw errors.BAD_REQUEST('Invalid or expired token'); + + const name = body.name as string; + const username = body.username as string; + const password = body.password as string; + const confirmPassword = body.confirmPassword as string; + + if (!name || !name.trim()) throw errors.BAD_REQUEST('Name is required'); + if (!username || !username.trim()) throw errors.BAD_REQUEST('Username is required'); + validatePassword(password); + if (password !== confirmPassword) throw errors.BAD_REQUEST('Passwords do not match'); + + const passwordHash = await argon2.hash(password); + + const insertedUsers = await officerdb.insert(Users).values({ + email: payload.email, + password: passwordHash, + name: name.trim(), + username: username.trim(), + role: 'Super Admin', + status: 'Active', + }).returning(); + + if (!insertedUsers || insertedUsers.length === 0) throw errors.INTERNAL_SERVER_ERROR('Failed to create user'); + return ctx.json({ ok: true }); +}; diff --git a/src/servers/api/auth/forgot-password.ts b/src/servers/api/auth/forgot-password.ts index 2b7b6edb..ad2c6af5 100644 --- a/src/servers/api/auth/forgot-password.ts +++ b/src/servers/api/auth/forgot-password.ts @@ -13,7 +13,7 @@ export const forgotPasswordHandler: Handler = async function (ctx) { }); if (!dbUser) return ctx.json({ ok: true }); - const verificationCode = await sign({ id: dbUser.id, email }, '6h'); + const verificationCode = await sign({ id: dbUser.id, email, purpose: 'reset-password' }, '6h'); const url = `${PUBLIC_URL}/auth/reset-password?verificationCode=${verificationCode}`; await sendMail({ diff --git a/src/servers/api/auth/verify-token.ts b/src/servers/api/auth/verify-token.ts index a5561681..72028f5d 100644 --- a/src/servers/api/auth/verify-token.ts +++ b/src/servers/api/auth/verify-token.ts @@ -15,13 +15,21 @@ export const verifyTokenHandler: Handler = async function (ctx) { throw errors.BAD_REQUEST('Token is invalid or expired'); } + // Bootstrap token: has email but no id (user not yet created) + if (userInfo?.email && !userInfo?.id) { + return ctx.json({ ok: true, email: userInfo.email }); + } + if (!userInfo?.id) throw errors.BAD_REQUEST('Token is invalid or expired'); const user = await officerdb.query.Users.findFirst({ where: eq(Users.id, userInfo.id), }); if (!user) throw errors.NOT_FOUND('User not found'); - if (user.status !== 'Unverified') throw errors.BAD_REQUEST('Account is already verified'); + + // Reset-password tokens skip the verification status check + const isResetToken = (userInfo as Record).purpose === 'reset-password'; + if (!isResetToken && user.status !== 'Unverified') throw errors.BAD_REQUEST('Account is already verified'); return ctx.json({ ok: true, email: user.email }); }; diff --git a/src/servers/api/landing-page-data/landing-page-data.ts b/src/servers/api/landing-page-data/landing-page-data.ts new file mode 100644 index 00000000..a05ed21d --- /dev/null +++ b/src/servers/api/landing-page-data/landing-page-data.ts @@ -0,0 +1,10 @@ +import { createRouter } from '../../create-router'; +import { officerdb, count, Users } from 'officerdb'; + +export const landingPageDataRouter = createRouter(); + +landingPageDataRouter.get('/', async (ctx) => { + const result = await officerdb.select({ count: count() }).from(Users); + const userCount = result[0]?.count ?? 0; + return ctx.json({ registrationOpen: userCount === 0 }); +}); diff --git a/src/servers/api/server-settings/server-settings.ts b/src/servers/api/server-settings/server-settings.ts index eb70c6b3..001026d6 100644 --- a/src/servers/api/server-settings/server-settings.ts +++ b/src/servers/api/server-settings/server-settings.ts @@ -23,12 +23,6 @@ serverSettingsRouter.route('/claude-code', claudeCodeRouter); serverSettingsRouter.route('/opencode', opencodeRouter); serverSettingsRouter.route('/applications', applicationsRouter); -serverSettingsRouter.get('/', async (ctx) => { - const result = await officerdb.select({ count: count() }).from(Users); - const userCount = result[0]?.count ?? 0; - return ctx.json({ registrationOpen: userCount === 0 }); -}); - serverSettingsRouter.get('/settings', async (ctx) => { const settings = await Bun.file(settingsPath).json(); return ctx.json(settings); diff --git a/src/servers/hono.ts b/src/servers/hono.ts index 178788f4..4ed09325 100644 --- a/src/servers/hono.ts +++ b/src/servers/hono.ts @@ -4,6 +4,7 @@ import { createRouter } from './create-router'; import type { HonoVariables } from './create-router'; import { authRouter } from './api/auth'; import { serverSettingsRouter, settingsPath } from './api/server-settings/server-settings'; +import { landingPageDataRouter } from './api/landing-page-data/landing-page-data'; import { updateUserHandler } from './api/users/update-user'; import { plansRouter } from './api/plans/plans'; import { skillsRouter } from './api/skills/skills'; @@ -37,6 +38,7 @@ honoServer.use( honoServer.get('/api', (ctx) => ctx.json({ officerAPI: 'ok' })); honoServer.route('/api/auth', authRouter); honoServer.route('/api/server-settings', serverSettingsRouter); +honoServer.route('/api/landing-page-data', landingPageDataRouter); const protectedRouter = createRouter(); protectedRouter.use(bodyParser()); diff --git a/src/workspaces/components/Card.tsx b/src/workspaces/components/Card.tsx index 8f71f65a..004cc6d8 100644 --- a/src/workspaces/components/Card.tsx +++ b/src/workspaces/components/Card.tsx @@ -16,7 +16,7 @@ type CardProps = ComponentPropsWithoutRef<'div'>; export const Card = ({ className, style, ...props }: CardProps) => { return (
diff --git a/src/apps/officer-web/Screens/LandingPage/components/Logos/DevSvgLogo.tsx b/src/workspaces/components/Logos/DevSvgLogo.tsx similarity index 100% rename from src/apps/officer-web/Screens/LandingPage/components/Logos/DevSvgLogo.tsx rename to src/workspaces/components/Logos/DevSvgLogo.tsx diff --git a/src/apps/officer-web/Screens/LandingPage/components/Logos/PastilhasSvgLogo.tsx b/src/workspaces/components/Logos/PastilhasSvgLogo.tsx similarity index 100% rename from src/apps/officer-web/Screens/LandingPage/components/Logos/PastilhasSvgLogo.tsx rename to src/workspaces/components/Logos/PastilhasSvgLogo.tsx diff --git a/src/apps/officer-web/Screens/LandingPage/components/Logos/index.tsx b/src/workspaces/components/Logos/index.tsx similarity index 100% rename from src/apps/officer-web/Screens/LandingPage/components/Logos/index.tsx rename to src/workspaces/components/Logos/index.tsx diff --git a/src/apps/officer-web/Screens/LandingPage/components/PixelGrid.tsx b/src/workspaces/components/PixelGrid.tsx similarity index 100% rename from src/apps/officer-web/Screens/LandingPage/components/PixelGrid.tsx rename to src/workspaces/components/PixelGrid.tsx diff --git a/src/workspaces/hooks/src/useAuth/useAuth.ts b/src/workspaces/hooks/src/useAuth/useAuth.ts index 60bff422..33f8b02b 100644 --- a/src/workspaces/hooks/src/useAuth/useAuth.ts +++ b/src/workspaces/hooks/src/useAuth/useAuth.ts @@ -16,11 +16,6 @@ export const useAuth = (props: UseAuthProps = {}) => { const apiClient = useClient(apiUrl); const passKeyManager = usePasskeys(); - const { data: setupData } = useQuery({ - queryKey: ['AUTH_SETUP'], - queryFn: () => apiClient.get<{ registrationOpen: boolean }>('/server-settings'), - }); - const registrationOpen = setupData?.registrationOpen ?? false; const { isLoading } = useQuery({ queryKey: ['CURRENT_USER'], @@ -115,7 +110,6 @@ export const useAuth = (props: UseAuthProps = {}) => { user, isAuthenticated: !!user, isLoading, - registrationOpen, refreshUser, signin, signout, diff --git a/src/workspaces/types/globals.d.ts b/src/workspaces/types/globals.d.ts index 60bb51dc..a643b763 100644 --- a/src/workspaces/types/globals.d.ts +++ b/src/workspaces/types/globals.d.ts @@ -1,3 +1,23 @@ +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.svg' { + const src: string; + export default src; +} + declare global { type SelectOption = { value: number | string; label?: string; href?: string; hidden?: boolean }; diff --git a/tsconfig.json b/tsconfig.json index 4afcbbb4..eccc77ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,10 @@ "ESNext", "DOM" ], + "types": [ + "react", + "react-dom" + ], "target": "ESNext", "module": "Preserve", "moduleDetection": "force",