Implement multi account login

This commit is contained in:
Hakan Shehu
2024-11-11 00:22:17 +01:00
parent 42ffc0509b
commit c7316e41c9
12 changed files with 226 additions and 113 deletions

View File

@@ -76,6 +76,10 @@ export class LogoutMutationHandler
type: 'app',
table: 'accounts',
},
{
type: 'app',
table: 'workspaces',
},
],
};
}

View File

@@ -1,24 +1,26 @@
import React from 'react';
import { Login } from '@/renderer/components/accounts/login';
import { AppLoading } from '@/renderer/app-loading';
import { AccountContext } from '@/renderer/contexts/account';
import { Outlet, useParams } from 'react-router-dom';
import { AccountLogout } from '@/renderer/components/accounts/account-logout';
import { Outlet, useNavigate } from 'react-router-dom';
import { DelayedComponent } from '@/renderer/components/ui/delayed-component';
import { useQuery } from '@/renderer/hooks/use-query';
import { AppContext } from '@/renderer/contexts/app';
import { AccountProvider } from '@/renderer/components/accounts/account-provider';
import { AccountLogout } from '@/renderer/components/accounts/account-logout';
import { AccountSettingsDialog } from '@/renderer/components/accounts/account-settings-dialog';
export const App = () => {
const [showLogout, setShowLogout] = React.useState(false);
const [showAccountSettings, setShowAccountSettings] = React.useState(false);
const { userId } = useParams<{ userId: string }>();
const navigate = useNavigate();
const [logoutId, setLogoutId] = React.useState<string | null>(null);
const [settingsId, setSettingsId] = React.useState<string | null>(null);
const { data: servers, isPending: isPendingServers } = useQuery({
type: 'server_list',
});
const { data: accounts, isPending: isPendingAccounts } = useQuery({
type: 'account_list',
});
const { data: workspaces, isPending: isPendingWorkspaces } = useQuery({
type: 'workspace_list',
});
@@ -34,67 +36,72 @@ export const App = () => {
);
}
if (!accounts || accounts.length === 0) {
return <Login />;
}
const workspace = workspaces?.find(
(workspace) => workspace.userId === userId
);
let account = accounts[0];
if (workspace) {
const workspaceAccount = accounts.find(
(account) => account.id === workspace.accountId
);
if (workspaceAccount) {
account = workspaceAccount;
}
}
const server = servers?.find((server) => server.domain === account?.server);
if (!server) {
return <p>Server not found.</p>;
}
const accountWorkspaces = workspaces?.filter(
(workspace) => workspace.accountId === account?.id
);
return (
<AccountContext.Provider
<AppContext.Provider
value={{
...account,
workspaces: accountWorkspaces ?? [],
logout: () => {
setShowLogout(true);
accounts: accounts ?? [],
workspaces: workspaces ?? [],
servers: servers ?? [],
showAccountLogin: () => {
navigate('/login');
},
openSettings: () => {
setShowAccountSettings(true);
showAccountLogout: (id) => {
setLogoutId(id);
},
showAccountSettings: (id) => {
setSettingsId(id);
},
setAccount: (id) => {
const account = accounts?.find((a) => a.id === id);
if (!account) {
return;
}
const accountWorkspaces =
workspaces?.filter((w) => w.accountId === id) ?? [];
if (accountWorkspaces.length === 0) {
return;
}
navigate(`/${accountWorkspaces[0].userId}`);
},
}}
>
<Outlet />
{showLogout && (
<AccountProvider>
<Outlet />
</AccountProvider>
{logoutId && (
<AccountLogout
id={account?.id ?? ''}
onCancel={() => {
setShowLogout(false);
}}
id={logoutId}
onCancel={() => setLogoutId(null)}
onLogout={() => {
setShowLogout(false);
setLogoutId(null);
const activeAccounts =
accounts?.filter((a) => a.id !== logoutId) ?? [];
if (activeAccounts.length > 0) {
const activeWorkspaces =
workspaces?.filter((w) =>
activeAccounts.some((a) => a.id === w.accountId)
) ?? [];
if (activeWorkspaces.length > 0) {
navigate(`/${activeWorkspaces[0].userId}`);
return;
}
}
navigate('/login');
}}
/>
)}
{showAccountSettings && (
{settingsId && (
<AccountSettingsDialog
id={account?.id ?? ''}
open={showAccountSettings}
onOpenChange={setShowAccountSettings}
id={settingsId}
open={true}
onOpenChange={() => setSettingsId(null)}
/>
)}
</AccountContext.Provider>
</AppContext.Provider>
);
};

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { AccountContext } from '@/renderer/contexts/account';
import { useApp } from '@/renderer/contexts/app';
import { Login } from '@/renderer/components/accounts/login';
import { useParams } from 'react-router-dom';
interface AccountProviderProps {
children: React.ReactNode;
}
export const AccountProvider = ({ children }: AccountProviderProps) => {
const app = useApp();
const { userId } = useParams<{ userId: string }>();
if (app.accounts.length === 0) {
return <Login />;
}
const workspace = app.workspaces.find(
(workspace) => workspace.userId === userId
);
let account = app.accounts[0];
if (workspace) {
const workspaceAccount = app.accounts.find(
(account) => account.id === workspace.accountId
);
if (workspaceAccount) {
account = workspaceAccount;
}
}
const workspaces = app.workspaces.filter(
(workspace) => workspace.accountId === account.id
);
return (
<AccountContext.Provider
value={{
...account,
workspaces,
}}
>
{children}
</AccountContext.Provider>
);
};

View File

@@ -10,10 +10,10 @@ import {
TabsTrigger,
} from '@/renderer/components/ui/tabs';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { Avatar } from '../avatars/avatar';
import { useAccount } from '@/renderer/contexts/account';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { Info, Trash2 } from 'lucide-react';
import { AccountUpdate } from '@/renderer/components/accounts/account-update';
import { useApp } from '@/renderer/contexts/app';
interface AccountSettingsDialogProps {
id: string;
@@ -22,10 +22,16 @@ interface AccountSettingsDialogProps {
}
export const AccountSettingsDialog = ({
id,
open,
onOpenChange,
}: AccountSettingsDialogProps) => {
const account = useAccount();
const app = useApp();
const account = app.accounts.find((a) => a.id === id);
if (!account) {
return null;
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -75,7 +81,7 @@ export const AccountSettingsDialog = ({
className="focus-visible:ring-0 focus-visible:ring-offset-0"
value="info"
>
<AccountUpdate />
<AccountUpdate account={account} />
</TabsContent>
<TabsContent
key="tab-content-delete"

View File

@@ -10,7 +10,6 @@ import {
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useAccount } from '@/renderer/contexts/account';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { toast } from '@/renderer/hooks/use-toast';
import { Avatar } from '@/renderer/components/avatars/avatar';
@@ -19,6 +18,7 @@ import { Spinner } from '@/renderer/components/ui/spinner';
import { Upload } from 'lucide-react';
import { Input } from '@/renderer/components/ui/input';
import { Button } from '@/renderer/components/ui/button';
import { Account } from '@/types/accounts';
const formSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters long.'),
@@ -28,8 +28,7 @@ const formSchema = z.object({
type formSchemaType = z.infer<typeof formSchema>;
export const AccountUpdate = () => {
const account = useAccount();
export const AccountUpdate = ({ account }: { account: Account }) => {
const { mutate: uploadAvatar, isPending: isUploadingAvatar } = useMutation();
const { mutate: updateAccount, isPending: isUpdatingAccount } = useMutation();

View File

@@ -1,20 +1,20 @@
import React from 'react';
import { Server } from '@/types/servers';
import { EmailRegister } from '@/renderer/components/accounts/email-register';
import { EmailLogin } from '@/renderer/components/accounts/email-login';
import { ServerDropdown } from '@/renderer/components/servers/server-dropdown';
import { useApp } from '@/renderer/contexts/app';
import { useNavigate } from 'react-router-dom';
interface LoginFormProps {
servers: Server[];
}
export const LoginForm = () => {
const app = useApp();
const navigate = useNavigate();
export const LoginForm = ({ servers }: LoginFormProps) => {
const [showRegister, setShowRegister] = React.useState(false);
const [server, setServer] = React.useState(servers[0]);
const [server, setServer] = React.useState(app.servers[0]);
return (
<div className="flex flex-col gap-4">
<ServerDropdown servers={servers} value={server} onChange={setServer} />
<ServerDropdown value={server} onChange={setServer} />
{showRegister ? (
<EmailRegister server={server} />
) : (
@@ -30,6 +30,16 @@ export const LoginForm = ({ servers }: LoginFormProps) => {
? 'Already have an account? Login'
: 'No account yet? Register'}
</p>
{app.accounts.length > 0 && (
<p
className="text-center text-sm text-muted-foreground hover:cursor-pointer hover:underline"
onClick={() => {
navigate(-1);
}}
>
Cancel
</p>
)}
</div>
);
};

View File

@@ -1,11 +1,6 @@
import { LoginForm } from '@/renderer/components/accounts/login-form';
import { useQuery } from '@/renderer/hooks/use-query';
export const Login = () => {
const { data, isPending } = useQuery({
type: 'server_list',
});
return (
<div className="grid h-screen min-h-screen w-full grid-cols-5">
<div className="col-span-2 flex items-center justify-center bg-zinc-950">
@@ -21,7 +16,7 @@ export const Login = () => {
Use one of the following methods to login
</p>
</div>
{isPending ? null : <LoginForm servers={data ?? []} />}
<LoginForm />
</div>
</div>
</div>

View File

@@ -10,19 +10,17 @@ import { Server } from '@/types/servers';
import { ServerCreateDialog } from '@/renderer/components/servers/server-create-dialog';
import { DropdownMenuSeparator } from '@radix-ui/react-dropdown-menu';
import { ChevronDown } from 'lucide-react';
import { useApp } from '@/renderer/contexts/app';
interface ServerDropdownProps {
servers: Server[];
value: Server;
onChange: (server: Server) => void;
}
export const ServerDropdown = ({
servers,
value,
onChange,
}: ServerDropdownProps) => {
export const ServerDropdown = ({ value, onChange }: ServerDropdownProps) => {
const app = useApp();
const [openCreate, setOpenCreate] = React.useState(false);
return (
<React.Fragment>
<DropdownMenu>
@@ -37,7 +35,7 @@ export const ServerDropdown = ({
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-96">
{servers.map((server) => (
{app.servers.map((server) => (
<DropdownMenuItem
key={server.domain}
onSelect={() => {

View File

@@ -1,3 +1,4 @@
import React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
@@ -14,16 +15,20 @@ import {
} from '@/renderer/components/ui/sidebar';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useAccount } from '@/renderer/contexts/account';
import { ChevronsUpDown, LogOut, Settings } from 'lucide-react';
import { ChevronsUpDown, LogOut, Plus, Settings } from 'lucide-react';
import { useApp } from '@/renderer/contexts/app';
export function SidebarFooter() {
const [open, setOpen] = React.useState(false);
const app = useApp();
const account = useAccount();
const sidebar = useSidebar();
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
@@ -43,43 +48,63 @@ export function SidebarFooter() {
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
className="w-[--radix-dropdown-menu-trigger-width] min-w-80 rounded-lg"
side={sidebar.isMobile ? 'bottom' : 'right'}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar
className="h-8 w-8 rounded-lg"
id={account.id}
name={account.name}
avatar={account.avatar}
/>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{account.name}</span>
<span className="truncate text-xs">{account.email}</span>
<DropdownMenuLabel className="mb-1">Accounts</DropdownMenuLabel>
{app.accounts.map((account) => (
<DropdownMenuItem
key={account.id}
className="p-0"
onClick={() => {
app.setAccount(account.id);
}}
>
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar
className="h-8 w-8 rounded-lg"
id={account.id}
name={account.name}
avatar={account.avatar}
/>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{account.name}
</span>
<span className="truncate text-xs">{account.email}</span>
</div>
<div className="ml-auto flex items-center gap-2 text-muted-foreground mr-1">
<LogOut
className="size-4 hover:cursor-pointer hover:text-red-500"
onClick={(e) => {
e.stopPropagation();
app.showAccountLogout(account.id);
setOpen(false);
}}
/>
<Settings
className="size-4 hover:cursor-pointer hover:text-sidebar-accent-foreground"
onClick={(e) => {
e.stopPropagation();
app.showAccountSettings(account.id);
setOpen(false);
}}
/>
</div>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
</DropdownMenuItem>
))}
<DropdownMenuSeparator className="my-1" />
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
account.openSettings();
app.showAccountLogin();
}}
>
<Settings className="size-4" />
Settings
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
account.logout();
}}
>
<LogOut className="size-4" />
Log out
<Plus className="size-4" />
Add account
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -4,8 +4,6 @@ import { Workspace } from '@/types/workspaces';
interface AccountContext extends Account {
workspaces: Workspace[];
logout: () => void;
openSettings: () => void;
}
export const AccountContext = createContext<AccountContext>(

View File

@@ -0,0 +1,18 @@
import { createContext, useContext } from 'react';
import { Account } from '@/types/accounts';
import { Workspace } from '@/types/workspaces';
import { Server } from '@/types/servers';
interface AppContext {
accounts: Account[];
workspaces: Workspace[];
servers: Server[];
showAccountLogin: () => void;
setAccount: (id: string) => void;
showAccountLogout: (id: string) => void;
showAccountSettings: (id: string) => void;
}
export const AppContext = createContext<AppContext>({} as AppContext);
export const useApp = () => useContext(AppContext);

View File

@@ -10,6 +10,7 @@ import { createHashRouter, RouterProvider } from 'react-router-dom';
import { Container } from '@/renderer/components/workspaces/containers/container';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Login } from '@/renderer/components/accounts/login';
const router = createHashRouter([
{
@@ -24,6 +25,10 @@ const router = createHashRouter([
path: '/create',
element: <WorkspaceCreate />,
},
{
path: '/login',
element: <Login />,
},
{
path: ':userId',
element: <Workspace />,