mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Implement multi account login
This commit is contained in:
@@ -76,6 +76,10 @@ export class LogoutMutationHandler
|
||||
type: 'app',
|
||||
table: 'accounts',
|
||||
},
|
||||
{
|
||||
type: 'app',
|
||||
table: 'workspaces',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,8 +4,6 @@ import { Workspace } from '@/types/workspaces';
|
||||
|
||||
interface AccountContext extends Account {
|
||||
workspaces: Workspace[];
|
||||
logout: () => void;
|
||||
openSettings: () => void;
|
||||
}
|
||||
|
||||
export const AccountContext = createContext<AccountContext>(
|
||||
|
||||
18
apps/desktop/src/renderer/contexts/app.ts
Normal file
18
apps/desktop/src/renderer/contexts/app.ts
Normal 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);
|
||||
@@ -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 />,
|
||||
|
||||
Reference in New Issue
Block a user