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
+
+
+
+ >
+ )}
+
+ );
+}
+
+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
+
+
+
+
+ );
+}
+
+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 (
+
+ );
+};
+
+
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",