mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Init server features
This commit is contained in:
@@ -14,9 +14,9 @@ import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { LoginOutput } from '@/types/accounts';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
import {parseApiError} from "@/lib/axios";
|
||||
import {Icon} from "@/components/ui/icon";
|
||||
import axios from "axios";
|
||||
import { parseApiError } from '@/lib/axios';
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
import axios from 'axios';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
@@ -29,7 +29,10 @@ interface EmailRegisterProps {
|
||||
onRegister: (output: LoginOutput) => void;
|
||||
}
|
||||
|
||||
export const EmailRegister = ({ serverUrl, onRegister }: EmailRegisterProps) => {
|
||||
export const EmailRegister = ({
|
||||
serverUrl,
|
||||
onRegister,
|
||||
}: EmailRegisterProps) => {
|
||||
const [isPending, setIsPending] = React.useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@@ -46,7 +49,7 @@ export const EmailRegister = ({ serverUrl, onRegister }: EmailRegisterProps) =>
|
||||
try {
|
||||
const { data } = await axios.post<LoginOutput>(
|
||||
`${serverUrl}/v1/accounts/register/email`,
|
||||
values
|
||||
values,
|
||||
);
|
||||
|
||||
onRegister(data);
|
||||
@@ -60,7 +63,7 @@ export const EmailRegister = ({ serverUrl, onRegister }: EmailRegisterProps) =>
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@@ -117,4 +120,4 @@ export const EmailRegister = ({ serverUrl, onRegister }: EmailRegisterProps) =>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
121
desktop/src/components/accounts/login-form.tsx
Normal file
121
desktop/src/components/accounts/login-form.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { useAppDatabase } from '@/contexts/app-database';
|
||||
import { LoginOutput } from '@/types/accounts';
|
||||
import { Server } from '@/types/servers';
|
||||
import { EmailRegister } from '@/components/accounts/email-register';
|
||||
import { EmailLogin } from '@/components/accounts/email-login';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
|
||||
interface LoginFormProps {
|
||||
servers: Server[];
|
||||
}
|
||||
|
||||
export const LoginForm = ({ servers }: LoginFormProps) => {
|
||||
const appDatabase = useAppDatabase();
|
||||
const [showRegister, setShowRegister] = React.useState(false);
|
||||
const [server, setServer] = React.useState(servers[0]);
|
||||
const serverUrl = `https://${server.domain}`;
|
||||
|
||||
const handleLogin = async (output: LoginOutput) => {
|
||||
const insertAccountQuery = appDatabase.database
|
||||
.insertInto('accounts')
|
||||
.values({
|
||||
id: output.account.id,
|
||||
name: output.account.name,
|
||||
avatar: output.account.avatar,
|
||||
device_id: output.account.deviceId,
|
||||
email: output.account.email,
|
||||
token: output.account.token,
|
||||
})
|
||||
.compile();
|
||||
|
||||
await appDatabase.mutate(insertAccountQuery);
|
||||
|
||||
if (output.workspaces.length > 0) {
|
||||
const insertWorkspacesQuery = appDatabase.database
|
||||
.insertInto('workspaces')
|
||||
.values(
|
||||
output.workspaces.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
account_id: output.account.id,
|
||||
avatar: workspace.avatar,
|
||||
role: workspace.role,
|
||||
description: workspace.description,
|
||||
synced: 0,
|
||||
user_id: workspace.userId,
|
||||
version_id: workspace.versionId,
|
||||
})),
|
||||
)
|
||||
.compile();
|
||||
|
||||
await appDatabase.mutate(insertWorkspacesQuery);
|
||||
}
|
||||
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex w-full flex-grow flex-row items-center gap-2 rounded-md border border-input p-2 hover:cursor-pointer hover:bg-gray-100">
|
||||
<Avatar id={server.domain} name={server.name} />
|
||||
<div className="flex-grow">
|
||||
<p className="flex-grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{server.domain}</p>
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
name="arrow-down-s-line"
|
||||
className="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-96">
|
||||
{servers.map((server) => (
|
||||
<DropdownMenuItem
|
||||
key={server.domain}
|
||||
onSelect={() => {
|
||||
setServer(server);
|
||||
}}
|
||||
className="flex w-full flex-grow flex-row items-center gap-2 rounded-md border-b border-input p-2 hover:cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<Avatar id={server.domain} name={server.name} />
|
||||
<div className="flex-grow">
|
||||
<p className="flex-grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{server.domain}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{showRegister ? (
|
||||
<EmailRegister serverUrl={serverUrl} onRegister={handleLogin} />
|
||||
) : (
|
||||
<EmailLogin serverUrl={serverUrl} onLogin={handleLogin} />
|
||||
)}
|
||||
<p
|
||||
className="text-center text-sm text-muted-foreground hover:cursor-pointer hover:underline"
|
||||
onClick={() => {
|
||||
setShowRegister(!showRegister);
|
||||
}}
|
||||
>
|
||||
{showRegister
|
||||
? 'Already have an account? Login'
|
||||
: 'No account yet? Register'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,86 +1,26 @@
|
||||
import React from 'react';
|
||||
import { EmailLogin } from '@/components/accounts/email-login';
|
||||
import { LoginOutput } from '@/types/accounts';
|
||||
import { EmailRegister } from '@/components/accounts/email-register';
|
||||
import { useAppDatabase } from '@/contexts/app-database';
|
||||
|
||||
const SERVER_URL = 'http://localhost:3000';
|
||||
import { LoginForm } from './login-form';
|
||||
import { useServersQuery } from '@/queries/use-servers-query';
|
||||
|
||||
export const Login = () => {
|
||||
const appDatabase = useAppDatabase();
|
||||
const [showRegister, setShowRegister] = React.useState(false);
|
||||
|
||||
const handleLogin = async (output: LoginOutput) => {
|
||||
const insertAccountQuery = appDatabase.database
|
||||
.insertInto('accounts')
|
||||
.values({
|
||||
id: output.account.id,
|
||||
name: output.account.name,
|
||||
avatar: output.account.avatar,
|
||||
device_id: output.account.deviceId,
|
||||
email: output.account.email,
|
||||
token: output.account.token,
|
||||
})
|
||||
.compile();
|
||||
|
||||
await appDatabase.mutate(insertAccountQuery);
|
||||
|
||||
if (output.workspaces.length > 0) {
|
||||
const insertWorkspacesQuery = appDatabase.database
|
||||
.insertInto('workspaces')
|
||||
.values(
|
||||
output.workspaces.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
account_id: output.account.id,
|
||||
avatar: workspace.avatar,
|
||||
role: workspace.role,
|
||||
description: workspace.description,
|
||||
synced: 0,
|
||||
user_id: workspace.userId,
|
||||
version_id: workspace.versionId,
|
||||
})),
|
||||
)
|
||||
.compile();
|
||||
|
||||
await appDatabase.mutate(insertWorkspacesQuery);
|
||||
}
|
||||
|
||||
window.location.href = '/';
|
||||
};
|
||||
const { data, isPending } = useServersQuery();
|
||||
|
||||
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">
|
||||
<h1 className="font-neotrax text-6xl text-white">neuron</h1>
|
||||
<h1 className="font-neotrax text-6xl text-white">colabron</h1>
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center justify-center py-12">
|
||||
<div className="mx-auto grid w-96 gap-6">
|
||||
<div className="grid gap-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Login to Neuron
|
||||
Login to Colabron
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use one of the following methods to login
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{showRegister ? (
|
||||
<EmailRegister serverUrl={SERVER_URL} onRegister={handleLogin} />
|
||||
) : (
|
||||
<EmailLogin serverUrl={SERVER_URL} onLogin={handleLogin} />
|
||||
)}
|
||||
<p
|
||||
className="text-center text-sm text-muted-foreground hover:cursor-pointer hover:underline"
|
||||
onClick={() => {
|
||||
setShowRegister(!showRegister);
|
||||
}}
|
||||
>
|
||||
{showRegister
|
||||
? 'Already have an account? Login'
|
||||
: 'No account yet? Register'}
|
||||
</p>
|
||||
</div>
|
||||
{isPending ? null : <LoginForm servers={data} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,6 +116,7 @@ export const App = () => {
|
||||
email: account.email,
|
||||
deviceId: account.device_id,
|
||||
status: account.status,
|
||||
server: account.server,
|
||||
workspaces: workspacesQuery.data?.rows.map((row) => {
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SqliteDialect,
|
||||
} from 'kysely';
|
||||
import { AccountManager } from '@/data/account-manager';
|
||||
import { ServerManager } from '@/data/server-manager';
|
||||
import { AppDatabaseSchema } from '@/data/schemas/app';
|
||||
import { appDatabaseMigrations } from '@/data/migrations/app';
|
||||
import { Account } from '@/types/accounts';
|
||||
@@ -20,10 +21,12 @@ import { Workspace } from '@/types/workspaces';
|
||||
import { SubscribedQueryContext, SubscribedQueryResult } from '@/types/queries';
|
||||
import { eventBus } from '@/lib/event-bus';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Server } from '@/types/servers';
|
||||
|
||||
const EVENT_LOOP_INTERVAL = 1000;
|
||||
|
||||
class AppManager {
|
||||
private readonly servers: Map<string, ServerManager>;
|
||||
private readonly accounts: Map<string, AccountManager>;
|
||||
private readonly appPath: string;
|
||||
private readonly database: Kysely<AppDatabaseSchema>;
|
||||
@@ -209,6 +212,12 @@ class AppManager {
|
||||
private async executeInit(): Promise<void> {
|
||||
await this.migrate();
|
||||
|
||||
const servers = await this.getServers();
|
||||
for (const server of servers) {
|
||||
const serverManager = new ServerManager(server);
|
||||
this.servers.set(server.domain, serverManager);
|
||||
}
|
||||
|
||||
const accounts = await this.getAccounts();
|
||||
const workspaces = await this.getWorkspaces();
|
||||
for (const account of accounts) {
|
||||
@@ -265,6 +274,25 @@ class AppManager {
|
||||
await migrator.migrateToLatest();
|
||||
}
|
||||
|
||||
private async getServers(): Promise<Server[]> {
|
||||
const servers = await this.database
|
||||
.selectFrom('servers')
|
||||
.selectAll()
|
||||
.execute();
|
||||
|
||||
return servers.map((server) => ({
|
||||
domain: server.domain,
|
||||
name: server.name,
|
||||
avatar: server.avatar,
|
||||
attributes: JSON.parse(server.attributes),
|
||||
version: server.version,
|
||||
createdAt: new Date(server.created_at),
|
||||
lastSyncedAt: server.last_synced_at
|
||||
? new Date(server.last_synced_at)
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getAccounts(): Promise<Account[]> {
|
||||
const accounts = await this.database
|
||||
.selectFrom('accounts')
|
||||
@@ -278,6 +306,7 @@ class AppManager {
|
||||
avatar: account.avatar,
|
||||
token: account.token,
|
||||
deviceId: account.device_id,
|
||||
server: account.server,
|
||||
status: account.status,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,70 @@
|
||||
import { Migration } from 'kysely';
|
||||
|
||||
const createServersTable: Migration = {
|
||||
up: async (db) => {
|
||||
await db.schema
|
||||
.createTable('servers')
|
||||
.addColumn('domain', 'text', (col) => col.notNull().primaryKey())
|
||||
.addColumn('name', 'text', (col) => col.notNull())
|
||||
.addColumn('avatar', 'text', (col) => col.notNull())
|
||||
.addColumn('attributes', 'text', (col) => col.notNull())
|
||||
.addColumn('version', 'text', (col) => col.notNull())
|
||||
.addColumn('created_at', 'text', (col) => col.notNull())
|
||||
.addColumn('last_synced_at', 'text')
|
||||
.execute();
|
||||
},
|
||||
down: async (db) => {
|
||||
await db.schema.dropTable('servers').execute();
|
||||
},
|
||||
};
|
||||
|
||||
const addNeuronServers: Migration = {
|
||||
up: async (db) => {
|
||||
await db
|
||||
.insertInto('servers')
|
||||
.values([
|
||||
{
|
||||
domain: 'localhost:3000',
|
||||
name: 'Localhost',
|
||||
avatar: '',
|
||||
attributes: '{}',
|
||||
version: '0.1.0',
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
domain: 'eu.neuronapp.io',
|
||||
name: 'Neuron Cloud (EU)',
|
||||
avatar: '',
|
||||
attributes: '{}',
|
||||
version: '0.1.0',
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
domain: 'us.neuronapp.io',
|
||||
name: 'Neuron Cloud (US)',
|
||||
avatar: '',
|
||||
attributes: '{}',
|
||||
version: '0.1.0',
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
},
|
||||
down: async (db) => {
|
||||
await db
|
||||
.deleteFrom('servers')
|
||||
.where('id', 'in', ['localhost', 'eu.neuronapp.io', 'us.neuronapp.io'])
|
||||
.execute();
|
||||
},
|
||||
};
|
||||
|
||||
const createAccountsTable: Migration = {
|
||||
up: async (db) => {
|
||||
await db.schema
|
||||
.createTable('accounts')
|
||||
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
|
||||
.addColumn('device_id', 'text', (col) => col.notNull())
|
||||
.addColumn('server', 'text', (col) => col.notNull())
|
||||
.addColumn('name', 'text', (col) => col.notNull())
|
||||
.addColumn('email', 'text', (col) => col.notNull())
|
||||
.addColumn('avatar', 'text')
|
||||
@@ -41,6 +100,8 @@ const createWorkspacesTable: Migration = {
|
||||
};
|
||||
|
||||
export const appDatabaseMigrations: Record<string, Migration> = {
|
||||
'00001_create_accounts_table': createAccountsTable,
|
||||
'00002_create_workspaces_table': createWorkspacesTable,
|
||||
'00001_create_servers_table': createServersTable,
|
||||
'00002_add_neuron_servers': addNeuronServers,
|
||||
'00003_create_accounts_table': createAccountsTable,
|
||||
'00004_create_workspaces_table': createWorkspacesTable,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { ColumnType, Insertable, Selectable, Updateable } from 'kysely';
|
||||
|
||||
interface ServerTable {
|
||||
domain: ColumnType<string, string, never>;
|
||||
name: ColumnType<string, string, string>;
|
||||
avatar: ColumnType<string, string, string>;
|
||||
attributes: ColumnType<string, string, string>;
|
||||
version: ColumnType<string, string, string>;
|
||||
created_at: ColumnType<string, string, string>;
|
||||
last_synced_at: ColumnType<string | null, string | null, string>;
|
||||
}
|
||||
|
||||
export type SelectServer = Selectable<ServerTable>;
|
||||
export type CreateServer = Insertable<ServerTable>;
|
||||
export type UpdateServer = Updateable<ServerTable>;
|
||||
|
||||
interface AccountTable {
|
||||
id: ColumnType<string, string, never>;
|
||||
server: ColumnType<string, string, never>;
|
||||
name: ColumnType<string, string, string>;
|
||||
email: ColumnType<string, string, never>;
|
||||
avatar: ColumnType<string | null, string | null, string | null>;
|
||||
@@ -31,6 +46,7 @@ export type CreateWorkspace = Insertable<WorkspaceTable>;
|
||||
export type UpdateWorkspace = Updateable<WorkspaceTable>;
|
||||
|
||||
export interface AppDatabaseSchema {
|
||||
servers: ServerTable;
|
||||
accounts: AccountTable;
|
||||
workspaces: WorkspaceTable;
|
||||
}
|
||||
|
||||
9
desktop/src/data/server-manager.ts
Normal file
9
desktop/src/data/server-manager.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Server } from '@/types/servers';
|
||||
|
||||
export class ServerManager {
|
||||
public readonly server: Server;
|
||||
|
||||
constructor(server: Server) {
|
||||
this.server = server;
|
||||
}
|
||||
}
|
||||
41
desktop/src/queries/use-servers-query.tsx
Normal file
41
desktop/src/queries/use-servers-query.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useAppDatabase } from '@/contexts/app-database';
|
||||
import { SelectServer } from '@/data/schemas/app';
|
||||
import { Server } from '@/types/servers';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { QueryResult } from 'kysely';
|
||||
|
||||
export const useServersQuery = () => {
|
||||
const appDatabase = useAppDatabase();
|
||||
return useQuery<QueryResult<SelectServer>, Error, Server[], string[]>({
|
||||
queryKey: ['servers'],
|
||||
queryFn: async ({ queryKey }) => {
|
||||
const query = appDatabase.database
|
||||
.selectFrom('servers')
|
||||
.selectAll()
|
||||
.compile();
|
||||
|
||||
const servers = await appDatabase.queryAndSubscribe({
|
||||
key: ['servers'],
|
||||
query: query,
|
||||
});
|
||||
|
||||
return servers;
|
||||
},
|
||||
select: (data: QueryResult<SelectServer>): Server[] => {
|
||||
const rows = data?.rows ?? [];
|
||||
return rows.map(mapServer);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mapServer = (row: SelectServer): Server => {
|
||||
return {
|
||||
domain: row.domain,
|
||||
name: row.name,
|
||||
avatar: row.avatar,
|
||||
attributes: JSON.parse(row.attributes),
|
||||
version: row.version,
|
||||
createdAt: new Date(row.created_at),
|
||||
lastSyncedAt: row.last_synced_at ? new Date(row.last_synced_at) : null,
|
||||
};
|
||||
};
|
||||
@@ -13,4 +13,5 @@ export type Account = {
|
||||
token: string;
|
||||
deviceId: string;
|
||||
status: string;
|
||||
server: string;
|
||||
};
|
||||
|
||||
13
desktop/src/types/servers.ts
Normal file
13
desktop/src/types/servers.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type Server = {
|
||||
domain: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
attributes: ServerAttributes;
|
||||
version: string;
|
||||
createdAt: Date;
|
||||
lastSyncedAt: Date | null;
|
||||
};
|
||||
|
||||
export type ServerAttributes = {
|
||||
name: string;
|
||||
};
|
||||
Reference in New Issue
Block a user