diff --git a/.gitignore b/.gitignore index 53a7010f..2d1171b3 100644 --- a/.gitignore +++ b/.gitignore @@ -155,7 +155,9 @@ src/scripts/icons/temp/ # Ignore desktop assets apps/desktop/assets/emojis.db apps/desktop/assets/icons.db -apps/desktop/assets/fonts/neotrax.otf +apps/desktop/assets/fonts/satoshi-variable.woff2 +apps/desktop/assets/fonts/satoshi-variable-italic.woff2 +apps/desktop/assets/fonts/antonio.ttf apps/desktop/assets/colanode-logo.png apps/desktop/assets/colanode-logo.ico apps/desktop/assets/colanode-logo.icns @@ -165,7 +167,9 @@ apps/web/public/assets/emojis.db apps/web/public/assets/icons.db apps/web/public/assets/emojis.svg apps/web/public/assets/icons.svg -apps/web/public/assets/fonts/neotrax.otf +apps/web/public/assets/fonts/satoshi-variable.woff2 +apps/web/public/assets/fonts/satoshi-variable-italic.woff2 +apps/web/public/assets/fonts/antonio.ttf apps/web/public/assets/colanode-logo-192.jpg apps/web/public/assets/colanode-logo-512.jpg @@ -173,7 +177,9 @@ apps/web/public/assets/colanode-logo-512.jpg apps/mobile/assets/ui/index.html apps/mobile/assets/emojis.db apps/mobile/assets/icons.db -apps/mobile/assets/fonts/neotrax.otf +apps/mobile/assets/fonts/satoshi-variable.woff2 +apps/mobile/assets/fonts/satoshi-variable-italic.woff2 +apps/mobile/assets/fonts/antonio.ttf .expo web-build/ diff --git a/apps/desktop/assets/fonts/neotrax.otf b/apps/desktop/assets/fonts/neotrax.otf deleted file mode 100644 index fb2c0fb7..00000000 Binary files a/apps/desktop/assets/fonts/neotrax.otf and /dev/null differ diff --git a/apps/server/src/api/client/routes/accounts/account-update.ts b/apps/server/src/api/client/routes/accounts/account-update.ts index 25d218aa..4493d72b 100644 --- a/apps/server/src/api/client/routes/accounts/account-update.ts +++ b/apps/server/src/api/client/routes/accounts/account-update.ts @@ -17,7 +17,7 @@ export const accountUpdateRoute: FastifyPluginCallbackZod = ( ) => { instance.route({ method: 'PATCH', - url: '/', + url: '/me', schema: { body: accountUpdateInputSchema, response: { diff --git a/apps/server/src/api/client/routes/accounts/index.ts b/apps/server/src/api/client/routes/accounts/index.ts index ac9e4017..3a4533d1 100644 --- a/apps/server/src/api/client/routes/accounts/index.ts +++ b/apps/server/src/api/client/routes/accounts/index.ts @@ -1,36 +1,16 @@ import { FastifyPluginCallback } from 'fastify'; import { accountAuthenticator } from '@colanode/server/api/client/plugins/account-auth'; -import { authIpRateLimiter } from '@colanode/server/api/client/plugins/auth-ip-rate-limit'; import { accountSyncRoute } from './account-sync'; import { accountUpdateRoute } from './account-update'; -import { emailLoginRoute } from './email-login'; -import { emailPasswordResetCompleteRoute } from './email-password-reset-complete'; -import { emailPasswordResetInitRoute } from './email-password-reset-init'; -import { emailRegisterRoute } from './email-register'; -import { emailVerifyRoute } from './email-verify'; -import { googleLoginRoute } from './google-login'; -import { logoutRoute } from './logout'; export const accountRoutes: FastifyPluginCallback = (instance, _, done) => { - instance.register((subInstance) => { - subInstance.register(authIpRateLimiter); - - subInstance.register(emailLoginRoute); - subInstance.register(emailRegisterRoute); - subInstance.register(emailVerifyRoute); - subInstance.register(emailPasswordResetInitRoute); - subInstance.register(emailPasswordResetCompleteRoute); - subInstance.register(googleLoginRoute); - }); - instance.register((subInstance) => { subInstance.register(accountAuthenticator); subInstance.register(accountSyncRoute); subInstance.register(accountUpdateRoute); - subInstance.register(logoutRoute); }); done(); diff --git a/apps/server/src/api/client/routes/accounts/email-login.ts b/apps/server/src/api/client/routes/auth/email-login.ts similarity index 98% rename from apps/server/src/api/client/routes/accounts/email-login.ts rename to apps/server/src/api/client/routes/auth/email-login.ts index 036455bb..f142c5b9 100644 --- a/apps/server/src/api/client/routes/accounts/email-login.ts +++ b/apps/server/src/api/client/routes/auth/email-login.ts @@ -23,7 +23,7 @@ export const emailLoginRoute: FastifyPluginCallbackZod = ( ) => { instance.route({ method: 'POST', - url: '/emails/login', + url: '/email/login', schema: { body: emailLoginInputSchema, response: { diff --git a/apps/server/src/api/client/routes/accounts/email-password-reset-complete.ts b/apps/server/src/api/client/routes/auth/email-password-reset-complete.ts similarity index 98% rename from apps/server/src/api/client/routes/accounts/email-password-reset-complete.ts rename to apps/server/src/api/client/routes/auth/email-password-reset-complete.ts index 2fcf4786..9fcdc9b4 100644 --- a/apps/server/src/api/client/routes/accounts/email-password-reset-complete.ts +++ b/apps/server/src/api/client/routes/auth/email-password-reset-complete.ts @@ -21,7 +21,7 @@ export const emailPasswordResetCompleteRoute: FastifyPluginCallbackZod = ( ) => { instance.route({ method: 'POST', - url: '/emails/passwords/reset/complete', + url: '/email/password-reset/complete', schema: { body: emailPasswordResetCompleteInputSchema, response: { diff --git a/apps/server/src/api/client/routes/accounts/email-password-reset-init.ts b/apps/server/src/api/client/routes/auth/email-password-reset-init.ts similarity index 98% rename from apps/server/src/api/client/routes/accounts/email-password-reset-init.ts rename to apps/server/src/api/client/routes/auth/email-password-reset-init.ts index 3a1e8f7c..ff49228b 100644 --- a/apps/server/src/api/client/routes/accounts/email-password-reset-init.ts +++ b/apps/server/src/api/client/routes/auth/email-password-reset-init.ts @@ -27,7 +27,7 @@ export const emailPasswordResetInitRoute: FastifyPluginCallbackZod = ( ) => { instance.route({ method: 'POST', - url: '/emails/passwords/reset/init', + url: '/email/password-reset/init', schema: { body: emailPasswordResetInitInputSchema, response: { diff --git a/apps/server/src/api/client/routes/accounts/email-register.ts b/apps/server/src/api/client/routes/auth/email-register.ts similarity index 99% rename from apps/server/src/api/client/routes/accounts/email-register.ts rename to apps/server/src/api/client/routes/auth/email-register.ts index 37d4f71e..a928a339 100644 --- a/apps/server/src/api/client/routes/accounts/email-register.ts +++ b/apps/server/src/api/client/routes/auth/email-register.ts @@ -26,7 +26,7 @@ export const emailRegisterRoute: FastifyPluginCallbackZod = ( ) => { instance.route({ method: 'POST', - url: '/emails/register', + url: '/email/register', schema: { body: emailRegisterInputSchema, response: { diff --git a/apps/server/src/api/client/routes/accounts/email-verify.ts b/apps/server/src/api/client/routes/auth/email-verify.ts similarity index 98% rename from apps/server/src/api/client/routes/accounts/email-verify.ts rename to apps/server/src/api/client/routes/auth/email-verify.ts index 83e43501..487a0413 100644 --- a/apps/server/src/api/client/routes/accounts/email-verify.ts +++ b/apps/server/src/api/client/routes/auth/email-verify.ts @@ -20,7 +20,7 @@ export const emailVerifyRoute: FastifyPluginCallbackZod = ( ) => { instance.route({ method: 'POST', - url: '/emails/verify', + url: '/email/verify', schema: { body: emailVerifyInputSchema, response: { diff --git a/apps/server/src/api/client/routes/accounts/google-login.ts b/apps/server/src/api/client/routes/auth/google-login.ts similarity index 100% rename from apps/server/src/api/client/routes/accounts/google-login.ts rename to apps/server/src/api/client/routes/auth/google-login.ts diff --git a/apps/server/src/api/client/routes/auth/index.ts b/apps/server/src/api/client/routes/auth/index.ts new file mode 100644 index 00000000..321ae416 --- /dev/null +++ b/apps/server/src/api/client/routes/auth/index.ts @@ -0,0 +1,32 @@ +import { FastifyPluginCallback } from 'fastify'; + +import { accountAuthenticator } from '@colanode/server/api/client/plugins/account-auth'; +import { authIpRateLimiter } from '@colanode/server/api/client/plugins/auth-ip-rate-limit'; + +import { emailLoginRoute } from './email-login'; +import { emailPasswordResetCompleteRoute } from './email-password-reset-complete'; +import { emailPasswordResetInitRoute } from './email-password-reset-init'; +import { emailRegisterRoute } from './email-register'; +import { emailVerifyRoute } from './email-verify'; +import { googleLoginRoute } from './google-login'; +import { logoutRoute } from './logout'; + +export const authRoutes: FastifyPluginCallback = (instance, _, done) => { + instance.register((subInstance) => { + subInstance.register(authIpRateLimiter); + + subInstance.register(emailLoginRoute); + subInstance.register(emailRegisterRoute); + subInstance.register(emailVerifyRoute); + subInstance.register(emailPasswordResetInitRoute); + subInstance.register(emailPasswordResetCompleteRoute); + subInstance.register(googleLoginRoute); + }); + + instance.register((subInstance) => { + subInstance.register(accountAuthenticator); + subInstance.register(logoutRoute); + }); + + done(); +}; diff --git a/apps/server/src/api/client/routes/accounts/logout.ts b/apps/server/src/api/client/routes/auth/logout.ts similarity index 100% rename from apps/server/src/api/client/routes/accounts/logout.ts rename to apps/server/src/api/client/routes/auth/logout.ts diff --git a/apps/server/src/api/client/routes/index.ts b/apps/server/src/api/client/routes/index.ts index ccb09132..b769cbf4 100644 --- a/apps/server/src/api/client/routes/index.ts +++ b/apps/server/src/api/client/routes/index.ts @@ -1,6 +1,7 @@ import { FastifyPluginCallback } from 'fastify'; import { accountRoutes } from '@colanode/server/api/client/routes/accounts'; +import { authRoutes } from '@colanode/server/api/client/routes/auth'; import { avatarRoutes } from '@colanode/server/api/client/routes/avatars'; import { socketRoutes } from '@colanode/server/api/client/routes/sockets'; import { workspaceRoutes } from '@colanode/server/api/client/routes/workspaces'; @@ -8,6 +9,7 @@ import { workspaceRoutes } from '@colanode/server/api/client/routes/workspaces'; export const clientRoutes: FastifyPluginCallback = (instance, _, done) => { instance.register(socketRoutes, { prefix: '/sockets' }); instance.register(accountRoutes, { prefix: '/accounts' }); + instance.register(authRoutes, { prefix: '/auth' }); instance.register(avatarRoutes, { prefix: '/avatars' }); instance.register(workspaceRoutes, { prefix: '/workspaces' }); diff --git a/assets/fonts/antonio.ttf b/assets/fonts/antonio.ttf new file mode 100644 index 00000000..581a3f4a Binary files /dev/null and b/assets/fonts/antonio.ttf differ diff --git a/assets/fonts/neotrax.otf b/assets/fonts/neotrax.otf deleted file mode 100644 index fb2c0fb7..00000000 Binary files a/assets/fonts/neotrax.otf and /dev/null differ diff --git a/assets/fonts/satoshi-variable-italic.woff2 b/assets/fonts/satoshi-variable-italic.woff2 new file mode 100644 index 00000000..e7ab3a09 Binary files /dev/null and b/assets/fonts/satoshi-variable-italic.woff2 differ diff --git a/assets/fonts/satoshi-variable.woff2 b/assets/fonts/satoshi-variable.woff2 new file mode 100644 index 00000000..b00e833e Binary files /dev/null and b/assets/fonts/satoshi-variable.woff2 differ diff --git a/packages/client/src/handlers/mutations/accounts/account-update.ts b/packages/client/src/handlers/mutations/accounts/account-update.ts index c36bdf86..84a5d935 100644 --- a/packages/client/src/handlers/mutations/accounts/account-update.ts +++ b/packages/client/src/handlers/mutations/accounts/account-update.ts @@ -38,7 +38,7 @@ export class AccountUpdateMutationHandler }; const response = await accountService.client - .patch(`v1/accounts`, { + .patch(`v1/accounts/me`, { json: body, }) .json(); diff --git a/packages/client/src/handlers/mutations/accounts/base.ts b/packages/client/src/handlers/mutations/auth/base.ts similarity index 98% rename from packages/client/src/handlers/mutations/accounts/base.ts rename to packages/client/src/handlers/mutations/auth/base.ts index c1009e50..f836c99c 100644 --- a/packages/client/src/handlers/mutations/accounts/base.ts +++ b/packages/client/src/handlers/mutations/auth/base.ts @@ -5,7 +5,7 @@ import { AppService } from '@colanode/client/services/app-service'; import { ServerService } from '@colanode/client/services/server-service'; import { LoginSuccessOutput } from '@colanode/core'; -export abstract class AccountMutationHandlerBase { +export abstract class AuthMutationHandlerBase { protected readonly app: AppService; constructor(app: AppService) { diff --git a/packages/client/src/handlers/mutations/accounts/email-login.ts b/packages/client/src/handlers/mutations/auth/email-login.ts similarity index 86% rename from packages/client/src/handlers/mutations/accounts/email-login.ts rename to packages/client/src/handlers/mutations/auth/email-login.ts index 2fcaedee..d29aef49 100644 --- a/packages/client/src/handlers/mutations/accounts/email-login.ts +++ b/packages/client/src/handlers/mutations/auth/email-login.ts @@ -1,13 +1,13 @@ -import { AccountMutationHandlerBase } from '@colanode/client/handlers/mutations/accounts/base'; +import { AuthMutationHandlerBase } from '@colanode/client/handlers/mutations/auth/base'; import { parseApiError } from '@colanode/client/lib/ky'; import { MutationHandler } from '@colanode/client/lib/types'; import { MutationError, MutationErrorCode } from '@colanode/client/mutations'; -import { EmailLoginMutationInput } from '@colanode/client/mutations/accounts/email-login'; +import { EmailLoginMutationInput } from '@colanode/client/mutations/auth/email-login'; import { AppService } from '@colanode/client/services/app-service'; import { EmailLoginInput, LoginOutput } from '@colanode/core'; export class EmailLoginMutationHandler - extends AccountMutationHandlerBase + extends AuthMutationHandlerBase implements MutationHandler { constructor(appService: AppService) { @@ -31,7 +31,7 @@ export class EmailLoginMutationHandler }; const response = await this.app.client - .post(`${server.httpBaseUrl}/v1/accounts/emails/login`, { + .post(`${server.httpBaseUrl}/v1/auth/email/login`, { json: body, }) .json(); diff --git a/packages/client/src/handlers/mutations/accounts/email-password-reset-complete.ts b/packages/client/src/handlers/mutations/auth/email-password-reset-complete.ts similarity index 80% rename from packages/client/src/handlers/mutations/accounts/email-password-reset-complete.ts rename to packages/client/src/handlers/mutations/auth/email-password-reset-complete.ts index bca9d70b..e8559d3c 100644 --- a/packages/client/src/handlers/mutations/accounts/email-password-reset-complete.ts +++ b/packages/client/src/handlers/mutations/auth/email-password-reset-complete.ts @@ -1,11 +1,11 @@ -import { AccountMutationHandlerBase } from '@colanode/client/handlers/mutations/accounts/base'; +import { AuthMutationHandlerBase } from '@colanode/client/handlers/mutations/auth/base'; import { MutationHandler } from '@colanode/client/lib'; import { parseApiError } from '@colanode/client/lib/ky'; import { MutationError, MutationErrorCode } from '@colanode/client/mutations'; import { EmailPasswordResetCompleteMutationInput, EmailPasswordResetCompleteMutationOutput, -} from '@colanode/client/mutations/accounts/email-password-reset-complete'; +} from '@colanode/client/mutations/auth/email-password-reset-complete'; import { AppService } from '@colanode/client/services/app-service'; import { EmailPasswordResetCompleteInput, @@ -13,7 +13,7 @@ import { } from '@colanode/core'; export class EmailPasswordResetCompleteMutationHandler - extends AccountMutationHandlerBase + extends AuthMutationHandlerBase implements MutationHandler { constructor(appService: AppService) { @@ -40,12 +40,9 @@ export class EmailPasswordResetCompleteMutationHandler }; const response = await this.app.client - .post( - `${server.httpBaseUrl}/v1/accounts/emails/passwords/reset/complete`, - { - json: body, - } - ) + .post(`${server.httpBaseUrl}/v1/auth/email/password-reset/complete`, { + json: body, + }) .json(); return response; diff --git a/packages/client/src/handlers/mutations/accounts/email-password-reset-init.ts b/packages/client/src/handlers/mutations/auth/email-password-reset-init.ts similarity index 83% rename from packages/client/src/handlers/mutations/accounts/email-password-reset-init.ts rename to packages/client/src/handlers/mutations/auth/email-password-reset-init.ts index 551d9394..0d89061a 100644 --- a/packages/client/src/handlers/mutations/accounts/email-password-reset-init.ts +++ b/packages/client/src/handlers/mutations/auth/email-password-reset-init.ts @@ -1,11 +1,11 @@ -import { AccountMutationHandlerBase } from '@colanode/client/handlers/mutations/accounts/base'; +import { AuthMutationHandlerBase } from '@colanode/client/handlers/mutations/auth/base'; import { parseApiError } from '@colanode/client/lib/ky'; import { MutationHandler } from '@colanode/client/lib/types'; import { MutationError, MutationErrorCode } from '@colanode/client/mutations'; import { EmailPasswordResetInitMutationInput, EmailPasswordResetInitMutationOutput, -} from '@colanode/client/mutations/accounts/email-password-reset-init'; +} from '@colanode/client/mutations/auth/email-password-reset-init'; import { AppService } from '@colanode/client/services/app-service'; import { EmailPasswordResetInitInput, @@ -13,7 +13,7 @@ import { } from '@colanode/core'; export class EmailPasswordResetInitMutationHandler - extends AccountMutationHandlerBase + extends AuthMutationHandlerBase implements MutationHandler { constructor(appService: AppService) { @@ -38,7 +38,7 @@ export class EmailPasswordResetInitMutationHandler }; const response = await this.app.client - .post(`${server.httpBaseUrl}/v1/accounts/emails/passwords/reset/init`, { + .post(`${server.httpBaseUrl}/v1/auth/email/password-reset/init`, { json: body, }) .json(); diff --git a/packages/client/src/handlers/mutations/accounts/email-register.ts b/packages/client/src/handlers/mutations/auth/email-register.ts similarity index 86% rename from packages/client/src/handlers/mutations/accounts/email-register.ts rename to packages/client/src/handlers/mutations/auth/email-register.ts index 045a9938..55d92b25 100644 --- a/packages/client/src/handlers/mutations/accounts/email-register.ts +++ b/packages/client/src/handlers/mutations/auth/email-register.ts @@ -1,13 +1,13 @@ -import { AccountMutationHandlerBase } from '@colanode/client/handlers/mutations/accounts/base'; +import { AuthMutationHandlerBase } from '@colanode/client/handlers/mutations/auth/base'; import { parseApiError } from '@colanode/client/lib/ky'; import { MutationHandler } from '@colanode/client/lib/types'; import { MutationError, MutationErrorCode } from '@colanode/client/mutations'; -import { EmailRegisterMutationInput } from '@colanode/client/mutations/accounts/email-register'; +import { EmailRegisterMutationInput } from '@colanode/client/mutations/auth/email-register'; import { AppService } from '@colanode/client/services/app-service'; import { EmailRegisterInput, LoginOutput } from '@colanode/core'; export class EmailRegisterMutationHandler - extends AccountMutationHandlerBase + extends AuthMutationHandlerBase implements MutationHandler { constructor(appService: AppService) { @@ -34,7 +34,7 @@ export class EmailRegisterMutationHandler }; const response = await this.app.client - .post(`${server.httpBaseUrl}/v1/accounts/emails/register`, { + .post(`${server.httpBaseUrl}/v1/auth/email/register`, { json: body, }) .json(); diff --git a/packages/client/src/handlers/mutations/accounts/email-verify.ts b/packages/client/src/handlers/mutations/auth/email-verify.ts similarity index 87% rename from packages/client/src/handlers/mutations/accounts/email-verify.ts rename to packages/client/src/handlers/mutations/auth/email-verify.ts index ff8628f1..17f7539d 100644 --- a/packages/client/src/handlers/mutations/accounts/email-verify.ts +++ b/packages/client/src/handlers/mutations/auth/email-verify.ts @@ -1,13 +1,13 @@ -import { AccountMutationHandlerBase } from '@colanode/client/handlers/mutations/accounts/base'; +import { AuthMutationHandlerBase } from '@colanode/client/handlers/mutations/auth/base'; import { parseApiError } from '@colanode/client/lib/ky'; import { MutationHandler } from '@colanode/client/lib/types'; import { MutationError, MutationErrorCode } from '@colanode/client/mutations'; -import { EmailVerifyMutationInput } from '@colanode/client/mutations/accounts/email-verify'; +import { EmailVerifyMutationInput } from '@colanode/client/mutations/auth/email-verify'; import { AppService } from '@colanode/client/services/app-service'; import { EmailVerifyInput, LoginOutput } from '@colanode/core'; export class EmailVerifyMutationHandler - extends AccountMutationHandlerBase + extends AuthMutationHandlerBase implements MutationHandler { constructor(appService: AppService) { @@ -31,7 +31,7 @@ export class EmailVerifyMutationHandler }; const response = await this.app.client - .post(`${server.httpBaseUrl}/v1/accounts/emails/verify`, { + .post(`${server.httpBaseUrl}/v1/auth/email/verify`, { json: body, }) .json(); diff --git a/packages/client/src/handlers/mutations/accounts/google-login.ts b/packages/client/src/handlers/mutations/auth/google-login.ts similarity index 87% rename from packages/client/src/handlers/mutations/accounts/google-login.ts rename to packages/client/src/handlers/mutations/auth/google-login.ts index 2586f76c..20f0e48c 100644 --- a/packages/client/src/handlers/mutations/accounts/google-login.ts +++ b/packages/client/src/handlers/mutations/auth/google-login.ts @@ -1,4 +1,4 @@ -import { AccountMutationHandlerBase } from '@colanode/client/handlers/mutations/accounts/base'; +import { AuthMutationHandlerBase } from '@colanode/client/handlers/mutations/auth/base'; import { parseApiError } from '@colanode/client/lib/ky'; import { MutationHandler } from '@colanode/client/lib/types'; import { @@ -10,7 +10,7 @@ import { AppService } from '@colanode/client/services/app-service'; import { GoogleLoginInput, LoginOutput } from '@colanode/core'; export class GoogleLoginMutationHandler - extends AccountMutationHandlerBase + extends AuthMutationHandlerBase implements MutationHandler { constructor(appService: AppService) { @@ -33,7 +33,7 @@ export class GoogleLoginMutationHandler }; const response = await this.app.client - .post(`${server.httpBaseUrl}/v1/accounts/google/login`, { + .post(`${server.httpBaseUrl}/v1/auth/google/login`, { json: body, }) .json(); diff --git a/packages/client/src/handlers/mutations/index.ts b/packages/client/src/handlers/mutations/index.ts index f4d0b16c..a97decad 100644 --- a/packages/client/src/handlers/mutations/index.ts +++ b/packages/client/src/handlers/mutations/index.ts @@ -4,17 +4,17 @@ import { AppService } from '@colanode/client/services'; import { AccountLogoutMutationHandler } from './accounts/account-logout'; import { AccountUpdateMutationHandler } from './accounts/account-update'; -import { EmailLoginMutationHandler } from './accounts/email-login'; -import { EmailPasswordResetCompleteMutationHandler } from './accounts/email-password-reset-complete'; -import { EmailPasswordResetInitMutationHandler } from './accounts/email-password-reset-init'; -import { EmailRegisterMutationHandler } from './accounts/email-register'; -import { EmailVerifyMutationHandler } from './accounts/email-verify'; -import { GoogleLoginMutationHandler } from './accounts/google-login'; import { MetadataDeleteMutationHandler } from './apps/metadata-delete'; import { MetadataUpdateMutationHandler } from './apps/metadata-update'; import { TabCreateMutationHandler } from './apps/tab-create'; import { TabDeleteMutationHandler } from './apps/tab-delete'; import { TabUpdateMutationHandler } from './apps/tab-update'; +import { EmailLoginMutationHandler } from './auth/email-login'; +import { EmailPasswordResetCompleteMutationHandler } from './auth/email-password-reset-complete'; +import { EmailPasswordResetInitMutationHandler } from './auth/email-password-reset-init'; +import { EmailRegisterMutationHandler } from './auth/email-register'; +import { EmailVerifyMutationHandler } from './auth/email-verify'; +import { GoogleLoginMutationHandler } from './auth/google-login'; import { AvatarUploadMutationHandler } from './avatars/avatar-upload'; import { ChannelCreateMutationHandler } from './channels/channel-create'; import { ChannelDeleteMutationHandler } from './channels/channel-delete'; diff --git a/packages/client/src/jobs/token-delete.ts b/packages/client/src/jobs/token-delete.ts index ac2079e7..27ac4b0e 100644 --- a/packages/client/src/jobs/token-delete.ts +++ b/packages/client/src/jobs/token-delete.ts @@ -52,7 +52,7 @@ export class TokenDeleteJobHandler implements JobHandler { } try { - await this.app.client.delete(`${server.httpBaseUrl}/v1/accounts/logout`, { + await this.app.client.delete(`${server.httpBaseUrl}/v1/auth/logout`, { headers: { Authorization: `Bearer ${input.token}`, }, diff --git a/packages/client/src/mutations/accounts/email-login.ts b/packages/client/src/mutations/auth/email-login.ts similarity index 100% rename from packages/client/src/mutations/accounts/email-login.ts rename to packages/client/src/mutations/auth/email-login.ts diff --git a/packages/client/src/mutations/accounts/email-password-reset-complete.ts b/packages/client/src/mutations/auth/email-password-reset-complete.ts similarity index 100% rename from packages/client/src/mutations/accounts/email-password-reset-complete.ts rename to packages/client/src/mutations/auth/email-password-reset-complete.ts diff --git a/packages/client/src/mutations/accounts/email-password-reset-init.ts b/packages/client/src/mutations/auth/email-password-reset-init.ts similarity index 100% rename from packages/client/src/mutations/accounts/email-password-reset-init.ts rename to packages/client/src/mutations/auth/email-password-reset-init.ts diff --git a/packages/client/src/mutations/accounts/email-register.ts b/packages/client/src/mutations/auth/email-register.ts similarity index 100% rename from packages/client/src/mutations/accounts/email-register.ts rename to packages/client/src/mutations/auth/email-register.ts diff --git a/packages/client/src/mutations/accounts/email-verify.ts b/packages/client/src/mutations/auth/email-verify.ts similarity index 100% rename from packages/client/src/mutations/accounts/email-verify.ts rename to packages/client/src/mutations/auth/email-verify.ts diff --git a/packages/client/src/mutations/accounts/google-login.ts b/packages/client/src/mutations/auth/google-login.ts similarity index 100% rename from packages/client/src/mutations/accounts/google-login.ts rename to packages/client/src/mutations/auth/google-login.ts diff --git a/packages/client/src/mutations/index.ts b/packages/client/src/mutations/index.ts index b09631a4..e43f6427 100644 --- a/packages/client/src/mutations/index.ts +++ b/packages/client/src/mutations/index.ts @@ -1,11 +1,11 @@ export * from './accounts/account-logout'; export * from './accounts/account-update'; -export * from './accounts/email-login'; -export * from './accounts/email-password-reset-complete'; -export * from './accounts/email-password-reset-init'; -export * from './accounts/email-register'; -export * from './accounts/email-verify'; -export * from './accounts/google-login'; +export * from './auth/email-login'; +export * from './auth/email-password-reset-complete'; +export * from './auth/email-password-reset-init'; +export * from './auth/email-register'; +export * from './auth/email-verify'; +export * from './auth/google-login'; export * from './apps/metadata-delete'; export * from './apps/metadata-update'; export * from './avatars/avatar-upload'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 13eb405c..a64cb4c0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,3 +38,4 @@ export * from './types/avatars'; export * from './types/build'; export * from './lib/servers'; export * from './types/storage'; +export * from './types/auth'; diff --git a/packages/core/src/types/accounts.ts b/packages/core/src/types/accounts.ts index adb7ee17..96f0b242 100644 --- a/packages/core/src/types/accounts.ts +++ b/packages/core/src/types/accounts.ts @@ -1,7 +1,5 @@ import { z } from 'zod/v4'; -import { workspaceOutputSchema } from '@colanode/core/types/workspaces'; - export enum AccountStatus { Pending = 0, Active = 1, @@ -31,103 +29,3 @@ export const accountUpdateOutputSchema = z.object({ }); export type AccountUpdateOutput = z.infer; - -export const emailRegisterInputSchema = z.object({ - name: z.string({ error: 'Name is required' }), - email: z.string({ error: 'Email is required' }).email({ - message: 'Invalid email address', - }), - password: z.string({ error: 'Password is required' }), -}); - -export type EmailRegisterInput = z.infer; - -export const emailLoginInputSchema = z.object({ - email: z.string({ error: 'Email is required' }).email({ - message: 'Invalid email address', - }), - password: z.string({ error: 'Password is required' }), -}); - -export type EmailLoginInput = z.infer; - -export const loginSuccessOutputSchema = z.object({ - type: z.literal('success'), - account: accountOutputSchema, - workspaces: z.array(workspaceOutputSchema), - deviceId: z.string(), - token: z.string(), -}); - -export type LoginSuccessOutput = z.infer; - -export const loginVerifyOutputSchema = z.object({ - type: z.literal('verify'), - id: z.string(), - expiresAt: z.date(), -}); - -export type LoginVerifyOutput = z.infer; - -export const loginOutputSchema = z.discriminatedUnion('type', [ - loginSuccessOutputSchema, - loginVerifyOutputSchema, -]); - -export type LoginOutput = z.infer; - -export const accountSyncOutputSchema = z.object({ - account: accountOutputSchema, - workspaces: z.array(workspaceOutputSchema), - token: z.string().optional(), -}); - -export type AccountSyncOutput = z.infer; - -export const emailVerifyInputSchema = z.object({ - id: z.string(), - otp: z.string(), -}); - -export type EmailVerifyInput = z.infer; - -export const emailPasswordResetInitInputSchema = z.object({ - email: z.email(), -}); - -export type EmailPasswordResetInitInput = z.infer< - typeof emailPasswordResetInitInputSchema ->; - -export const emailPasswordResetCompleteInputSchema = z.object({ - id: z.string(), - otp: z.string(), - password: z.string(), -}); - -export type EmailPasswordResetCompleteInput = z.infer< - typeof emailPasswordResetCompleteInputSchema ->; - -export const emailPasswordResetInitOutputSchema = z.object({ - id: z.string(), - expiresAt: z.date(), -}); - -export type EmailPasswordResetInitOutput = z.infer< - typeof emailPasswordResetInitOutputSchema ->; - -export const emailPasswordResetCompleteOutputSchema = z.object({ - success: z.boolean(), -}); - -export type EmailPasswordResetCompleteOutput = z.infer< - typeof emailPasswordResetCompleteOutputSchema ->; - -export const googleLoginInputSchema = z.object({ - code: z.string(), -}); - -export type GoogleLoginInput = z.infer; diff --git a/packages/core/src/types/auth.ts b/packages/core/src/types/auth.ts new file mode 100644 index 00000000..906084a3 --- /dev/null +++ b/packages/core/src/types/auth.ts @@ -0,0 +1,104 @@ +import { z } from 'zod/v4'; + +import { accountOutputSchema } from '@colanode/core/types/accounts'; +import { workspaceOutputSchema } from '@colanode/core/types/workspaces'; + +export const emailRegisterInputSchema = z.object({ + name: z.string({ error: 'Name is required' }), + email: z.string({ error: 'Email is required' }).email({ + message: 'Invalid email address', + }), + password: z.string({ error: 'Password is required' }), +}); + +export type EmailRegisterInput = z.infer; + +export const emailLoginInputSchema = z.object({ + email: z.string({ error: 'Email is required' }).email({ + message: 'Invalid email address', + }), + password: z.string({ error: 'Password is required' }), +}); + +export type EmailLoginInput = z.infer; + +export const loginSuccessOutputSchema = z.object({ + type: z.literal('success'), + account: accountOutputSchema, + workspaces: z.array(workspaceOutputSchema), + deviceId: z.string(), + token: z.string(), +}); + +export type LoginSuccessOutput = z.infer; + +export const loginVerifyOutputSchema = z.object({ + type: z.literal('verify'), + id: z.string(), + expiresAt: z.date(), +}); + +export type LoginVerifyOutput = z.infer; + +export const loginOutputSchema = z.discriminatedUnion('type', [ + loginSuccessOutputSchema, + loginVerifyOutputSchema, +]); + +export type LoginOutput = z.infer; + +export const accountSyncOutputSchema = z.object({ + account: accountOutputSchema, + workspaces: z.array(workspaceOutputSchema), + token: z.string().optional(), +}); + +export type AccountSyncOutput = z.infer; + +export const emailVerifyInputSchema = z.object({ + id: z.string(), + otp: z.string(), +}); + +export type EmailVerifyInput = z.infer; + +export const emailPasswordResetInitInputSchema = z.object({ + email: z.email(), +}); + +export type EmailPasswordResetInitInput = z.infer< + typeof emailPasswordResetInitInputSchema +>; + +export const emailPasswordResetCompleteInputSchema = z.object({ + id: z.string(), + otp: z.string(), + password: z.string(), +}); + +export type EmailPasswordResetCompleteInput = z.infer< + typeof emailPasswordResetCompleteInputSchema +>; + +export const emailPasswordResetInitOutputSchema = z.object({ + id: z.string(), + expiresAt: z.date(), +}); + +export type EmailPasswordResetInitOutput = z.infer< + typeof emailPasswordResetInitOutputSchema +>; + +export const emailPasswordResetCompleteOutputSchema = z.object({ + success: z.boolean(), +}); + +export type EmailPasswordResetCompleteOutput = z.infer< + typeof emailPasswordResetCompleteOutputSchema +>; + +export const googleLoginInputSchema = z.object({ + code: z.string(), +}); + +export type GoogleLoginInput = z.infer; diff --git a/packages/ui/src/components/accounts/login-form.tsx b/packages/ui/src/components/accounts/login-form.tsx deleted file mode 100644 index 29d311f9..00000000 --- a/packages/ui/src/components/accounts/login-form.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { useLiveQuery } from '@tanstack/react-db'; -import { useRouter } from '@tanstack/react-router'; -import { HouseIcon } from 'lucide-react'; -import { useState, Fragment, useEffect, useCallback } from 'react'; -import { match } from 'ts-pattern'; - -import { isFeatureSupported } from '@colanode/client/lib'; -import { LoginSuccessOutput } from '@colanode/core'; -import { collections } from '@colanode/ui/collections'; -import { EmailLogin } from '@colanode/ui/components/accounts/email-login'; -import { EmailPasswordResetComplete } from '@colanode/ui/components/accounts/email-password-reset-complete'; -import { EmailPasswordResetInit } from '@colanode/ui/components/accounts/email-password-reset-init'; -import { EmailRegister } from '@colanode/ui/components/accounts/email-register'; -import { EmailVerify } from '@colanode/ui/components/accounts/email-verify'; -import { ServerDropdown } from '@colanode/ui/components/servers/server-dropdown'; -import { Button } from '@colanode/ui/components/ui/button'; -import { Separator } from '@colanode/ui/components/ui/separator'; -import { ServerContext } from '@colanode/ui/contexts/server'; - -type LoginPanelState = { - type: 'login'; -}; - -type RegisterPanelState = { - type: 'register'; -}; - -type VerifyPanelState = { - type: 'verify'; - id: string; - expiresAt: Date; -}; - -type PasswordResetInitPanelState = { - type: 'password_reset_init'; -}; - -type PasswordResetCompletePanelState = { - type: 'password_reset_complete'; - id: string; - expiresAt: Date; -}; - -type PanelState = - | LoginPanelState - | RegisterPanelState - | VerifyPanelState - | PasswordResetInitPanelState - | PasswordResetCompletePanelState; - -export const LoginForm = () => { - const router = useRouter(); - const serversQuery = useLiveQuery((q) => - q.from({ servers: collections.servers }) - ); - const servers = serversQuery.data; - - const workspacesQuery = useLiveQuery((q) => - q.from({ workspaces: collections.workspaces }).select(({ workspaces }) => ({ - userId: workspaces.userId, - })) - ); - const workspaces = workspacesQuery.data; - - const [serverDomain, setServerDomain] = useState( - servers[0]?.domain ?? null - ); - - const [panel, setPanel] = useState({ - type: 'login', - }); - - useEffect(() => { - const serverExists = - serverDomain !== null && servers.some((s) => s.domain === serverDomain); - if (!serverExists && servers.length > 0) { - setServerDomain(servers[0]!.domain); - } - }, [serverDomain, servers]); - - const server = serverDomain - ? servers.find((s) => s.domain === serverDomain) - : null; - - const handleLoginSuccess = useCallback( - (output: LoginSuccessOutput) => { - const workspace = output.workspaces[0]; - if (workspace) { - router.navigate({ - to: '/workspace/$userId', - params: { userId: workspace.user.id }, - }); - } else { - router.navigate({ - to: '/create', - }); - } - }, - [router] - ); - - return ( -
- { - setServerDomain(serverDomain); - }} - servers={servers} - readonly={panel.type === 'verify'} - /> - {server && ( - { - return isFeatureSupported(feature, server.version); - }, - }} - > -
- {match(panel) - .with({ type: 'login' }, () => ( - { - if (output.type === 'success') { - handleLoginSuccess(output); - } else if (output.type === 'verify') { - setPanel({ - type: 'verify', - id: output.id, - expiresAt: new Date(output.expiresAt), - }); - } - }} - onForgotPassword={() => { - setPanel({ - type: 'password_reset_init', - }); - }} - onRegister={() => { - setPanel({ - type: 'register', - }); - }} - /> - )) - .with({ type: 'register' }, () => ( - { - if (output.type === 'success') { - handleLoginSuccess(output); - } else if (output.type === 'verify') { - setPanel({ - type: 'verify', - id: output.id, - expiresAt: new Date(output.expiresAt), - }); - } - }} - onLogin={() => { - setPanel({ - type: 'login', - }); - }} - /> - )) - .with({ type: 'verify' }, (p) => ( - { - if (output.type === 'success') { - handleLoginSuccess(output); - } - }} - onBack={() => { - setPanel({ - type: 'login', - }); - }} - /> - )) - .with({ type: 'password_reset_init' }, () => ( - { - setPanel({ - type: 'password_reset_complete', - id: output.id, - expiresAt: new Date(output.expiresAt), - }); - }} - onBack={() => { - setPanel({ - type: 'login', - }); - }} - /> - )) - .with({ type: 'password_reset_complete' }, (p) => ( - { - setPanel({ - type: 'login', - }); - }} - /> - )) - .exhaustive()} -
-
- )} - - {workspaces.length > 0 && ( - - - - - )} -
- ); -}; diff --git a/packages/ui/src/components/accounts/login-tab.tsx b/packages/ui/src/components/accounts/login-tab.tsx deleted file mode 100644 index 1ddde3ff..00000000 --- a/packages/ui/src/components/accounts/login-tab.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Tab } from '@colanode/ui/components/layouts/tabs/tab'; - -export const LoginTab = () => { - return ; -}; diff --git a/packages/ui/src/components/accounts/login.tsx b/packages/ui/src/components/accounts/login.tsx deleted file mode 100644 index ee6f33b4..00000000 --- a/packages/ui/src/components/accounts/login.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { LoginForm } from '@colanode/ui/components/accounts/login-form'; - -export const Login = () => { - return ( -
-
-

colanode

-
-
-
-
-

- Login to Colanode -

-

- Use one of the following methods to login -

-
- -
-
-
- ); -}; diff --git a/packages/ui/src/components/app/app-assets.tsx b/packages/ui/src/components/app/app-assets.tsx index e4255cd8..6eff7fbe 100644 --- a/packages/ui/src/components/app/app-assets.tsx +++ b/packages/ui/src/components/app/app-assets.tsx @@ -2,22 +2,41 @@ import { useApp } from '@colanode/ui/contexts/app'; export const AppAssets = () => { const app = useApp(); - const fontUrl = - app.type === 'web' - ? `/assets/fonts/neotrax.otf` - : `local://fonts/neotrax.otf`; + const fontPrefix = app.type === 'web' ? `/assets/fonts` : `local://fonts`; return ( ); diff --git a/packages/ui/src/components/app/app-loading.tsx b/packages/ui/src/components/app/app-loading.tsx index 35ace32f..f78f7da9 100644 --- a/packages/ui/src/components/app/app-loading.tsx +++ b/packages/ui/src/components/app/app-loading.tsx @@ -6,7 +6,7 @@ export const AppLoading = () => {
-

loading your workspaces

+

loading your workspaces

diff --git a/packages/ui/src/components/auth/auth-cancel.tsx b/packages/ui/src/components/auth/auth-cancel.tsx new file mode 100644 index 00000000..ceca4d96 --- /dev/null +++ b/packages/ui/src/components/auth/auth-cancel.tsx @@ -0,0 +1,44 @@ +import { useLiveQuery } from '@tanstack/react-db'; +import { useRouter } from '@tanstack/react-router'; +import { Home } from 'lucide-react'; + +import { collections } from '@colanode/ui/collections'; +import { Button } from '@colanode/ui/components/ui/button'; +import { getDefaultWorkspaceUserId } from '@colanode/ui/routes/utils'; + +export const AuthCancel = () => { + const router = useRouter(); + + const workspacesQuery = useLiveQuery((q) => + q.from({ workspaces: collections.workspaces }).select(({ workspaces }) => ({ + userId: workspaces.userId, + })) + ); + const workspaces = workspacesQuery.data; + + if (workspaces.length === 0) { + return null; + } + + return ( + + ); +}; diff --git a/packages/ui/src/components/auth/auth-layout.tsx b/packages/ui/src/components/auth/auth-layout.tsx new file mode 100644 index 00000000..a353bf5a --- /dev/null +++ b/packages/ui/src/components/auth/auth-layout.tsx @@ -0,0 +1,38 @@ +import { Outlet } from '@tanstack/react-router'; +import { useState } from 'react'; + +import { Server } from '@colanode/client/types'; +import { AuthCancel } from '@colanode/ui/components/auth/auth-cancel'; +import { AuthServer } from '@colanode/ui/components/auth/auth-server'; +import { ColanodeLogo } from '@colanode/ui/components/ui/logo'; +import { AuthContext } from '@colanode/ui/contexts/auth'; + +export const AuthLayout = () => { + const [server, setServer] = useState(null); + + return ( +
+ +
+
+
+ +

+ Your all-in-one
collaboration platform +

+
+
+ +
+ {server ? ( + + + + ) : ( + + )} +
+
+
+ ); +}; diff --git a/packages/ui/src/components/auth/auth-server.tsx b/packages/ui/src/components/auth/auth-server.tsx new file mode 100644 index 00000000..68e797ed --- /dev/null +++ b/packages/ui/src/components/auth/auth-server.tsx @@ -0,0 +1,114 @@ +import { useLiveQuery } from '@tanstack/react-db'; +import { PlusIcon, SettingsIcon } from 'lucide-react'; +import { useState } from 'react'; + +import { Server } from '@colanode/client/types'; +import { collections } from '@colanode/ui/collections'; +import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar'; +import { ServerCreateDialog } from '@colanode/ui/components/servers/server-create-dialog'; +import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog'; +import { ServerSettingsDialog } from '@colanode/ui/components/servers/server-settings-dialog'; + +interface AuthServerProps { + onSelect: (server: Server) => void; +} + +export const AuthServer = ({ onSelect }: AuthServerProps) => { + const [openCreate, setOpenCreate] = useState(false); + const [settingsDomain, setSettingsDomain] = useState(null); + const [deleteDomain, setDeleteDomain] = useState(null); + const serversQuery = useLiveQuery((q) => + q.from({ servers: collections.servers }) + ); + const servers = serversQuery.data ?? []; + const settingsServer = servers.find( + (server) => server.domain === settingsDomain + ); + const deleteServer = servers.find((server) => server.domain === deleteDomain); + + return ( +
+
+

+ {servers.length > 0 ? 'Select a server' : 'Add a server'} +

+

+ {servers.length > 0 + ? 'Choose the server you want to connect to' + : 'Add a server to get started'} +

+
+
+ {servers.map((server) => ( + + + ))} + +
+ {openCreate && ( + setOpenCreate(false)} /> + )} + {deleteServer && ( + { + if (!open) { + setDeleteDomain(null); + } + }} + /> + )} + {settingsServer && ( + { + if (!open) { + setSettingsDomain(null); + } + }} + onDelete={() => { + setSettingsDomain(null); + setDeleteDomain(settingsServer.domain); + }} + /> + )} +
+ ); +}; diff --git a/packages/ui/src/components/accounts/email-login.tsx b/packages/ui/src/components/auth/email-login-form.tsx similarity index 64% rename from packages/ui/src/components/accounts/email-login.tsx rename to packages/ui/src/components/auth/email-login-form.tsx index dda3b279..349961c6 100644 --- a/packages/ui/src/components/accounts/email-login.tsx +++ b/packages/ui/src/components/auth/email-login-form.tsx @@ -1,11 +1,9 @@ import { zodResolver } from '@hookform/resolvers/zod'; +import { useNavigate } from '@tanstack/react-router'; import { Mail } from 'lucide-react'; import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; import { z } from 'zod/v4'; -import { LoginOutput } from '@colanode/core'; -import { GoogleLogin } from '@colanode/ui/components/accounts/google-login'; import { Button } from '@colanode/ui/components/ui/button'; import { Form, @@ -17,28 +15,19 @@ import { import { Input } from '@colanode/ui/components/ui/input'; import { Label } from '@colanode/ui/components/ui/label'; import { Spinner } from '@colanode/ui/components/ui/spinner'; -import { useServer } from '@colanode/ui/contexts/server'; -import { useMutation } from '@colanode/ui/hooks/use-mutation'; const formSchema = z.object({ email: z.string().min(2).email(), password: z.string().min(8), }); -interface EmailLoginProps { - onSuccess: (output: LoginOutput) => void; - onForgotPassword: () => void; - onRegister: () => void; +interface LoginFormProps { + isPending: boolean; + onSubmit: (values: z.infer) => void; } -export const EmailLogin = ({ - onSuccess, - onForgotPassword, - onRegister, -}: EmailLoginProps) => { - const server = useServer(); - const { mutate, isPending } = useMutation(); - +export const LoginForm = ({ isPending, onSubmit }: LoginFormProps) => { + const navigate = useNavigate(); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -47,26 +36,9 @@ export const EmailLogin = ({ }, }); - const handleSubmit = async (values: z.infer) => { - mutate({ - input: { - type: 'email.login', - email: values.email, - password: values.password, - server: server.domain, - }, - onSuccess(output) { - onSuccess(output); - }, - onError(error) { - toast.error(error.message); - }, - }); - }; - return (
- + Password

{ + navigate({ to: '/auth/reset' }); + }} > Forgot password?

@@ -123,15 +97,6 @@ export const EmailLogin = ({ )} Login - - ); diff --git a/packages/ui/src/components/accounts/email-password-reset-complete.tsx b/packages/ui/src/components/auth/email-password-reset-complete-form.tsx similarity index 62% rename from packages/ui/src/components/accounts/email-password-reset-complete.tsx rename to packages/ui/src/components/auth/email-password-reset-complete-form.tsx index db25964d..13c0856c 100644 --- a/packages/ui/src/components/accounts/email-password-reset-complete.tsx +++ b/packages/ui/src/components/auth/email-password-reset-complete-form.tsx @@ -1,8 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { CheckCircle, Lock } from 'lucide-react'; -import { useState } from 'react'; +import { Lock } from 'lucide-react'; import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; import { z } from 'zod/v4'; import { Button } from '@colanode/ui/components/ui/button'; @@ -16,9 +14,7 @@ import { import { Input } from '@colanode/ui/components/ui/input'; import { Label } from '@colanode/ui/components/ui/label'; import { Spinner } from '@colanode/ui/components/ui/spinner'; -import { useServer } from '@colanode/ui/contexts/server'; import { useCountdown } from '@colanode/ui/hooks/use-countdown'; -import { useMutation } from '@colanode/ui/hooks/use-mutation'; const formSchema = z .object({ @@ -39,20 +35,17 @@ const formSchema = z path: ['confirmPassword'], // path of error }); -interface EmailPasswordResetCompleteProps { - id: string; +interface PasswordResetCompleteFormProps { expiresAt: Date; - onBack: () => void; + isPending: boolean; + onSubmit: (values: z.infer) => void; } -export const EmailPasswordResetComplete = ({ - id, +export const PasswordResetCompleteForm = ({ expiresAt, - onBack, -}: EmailPasswordResetCompleteProps) => { - const server = useServer(); - const { mutate, isPending } = useMutation(); - + isPending, + onSubmit, +}: PasswordResetCompleteFormProps) => { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -62,57 +55,8 @@ export const EmailPasswordResetComplete = ({ }, }); - const [showSuccess, setShowSuccess] = useState(false); const [remainingSeconds, formattedTime] = useCountdown(expiresAt); - const onSubmit = async (values: z.infer) => { - if (remainingSeconds <= 0) { - toast.error('Code has expired'); - return; - } - - mutate({ - input: { - type: 'email.password.reset.complete', - otp: values.otp, - password: values.password, - server: server.domain, - id: id, - }, - onSuccess() { - setShowSuccess(true); - }, - onError(error) { - toast.error(error.message); - }, - }); - }; - - if (showSuccess) { - return ( -
-
- -

- Your password has been reset. You can now login with your new - password. -

-

- You have been logged out of all devices. -

-
- -
- ); - } - return (
@@ -172,7 +116,7 @@ export const EmailPasswordResetComplete = ({ type="submit" variant="outline" className="w-full" - disabled={isPending} + disabled={isPending || remainingSeconds <= 0} > {isPending ? ( @@ -181,14 +125,6 @@ export const EmailPasswordResetComplete = ({ )} Reset password - ); diff --git a/packages/ui/src/components/accounts/email-password-reset-init.tsx b/packages/ui/src/components/auth/email-password-reset-init-form.tsx similarity index 56% rename from packages/ui/src/components/accounts/email-password-reset-init.tsx rename to packages/ui/src/components/auth/email-password-reset-init-form.tsx index b74b7056..25c4b06e 100644 --- a/packages/ui/src/components/accounts/email-password-reset-init.tsx +++ b/packages/ui/src/components/auth/email-password-reset-init-form.tsx @@ -1,10 +1,8 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Mail } from 'lucide-react'; import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; import { z } from 'zod/v4'; -import { EmailPasswordResetInitOutput } from '@colanode/core'; import { Button } from '@colanode/ui/components/ui/button'; import { Form, @@ -16,25 +14,20 @@ import { import { Input } from '@colanode/ui/components/ui/input'; import { Label } from '@colanode/ui/components/ui/label'; import { Spinner } from '@colanode/ui/components/ui/spinner'; -import { useServer } from '@colanode/ui/contexts/server'; -import { useMutation } from '@colanode/ui/hooks/use-mutation'; const formSchema = z.object({ email: z.string().min(2).email(), }); -interface EmailPasswordResetInitProps { - onSuccess: (output: EmailPasswordResetInitOutput) => void; - onBack: () => void; +interface PasswordResetInitFormProps { + isPending: boolean; + onSubmit: (values: z.infer) => void; } -export const EmailPasswordResetInit = ({ - onSuccess, - onBack, -}: EmailPasswordResetInitProps) => { - const server = useServer(); - const { mutate, isPending } = useMutation(); - +export const PasswordResetInitForm = ({ + isPending, + onSubmit, +}: PasswordResetInitFormProps) => { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -42,25 +35,9 @@ export const EmailPasswordResetInit = ({ }, }); - const handleSubmit = async (values: z.infer) => { - mutate({ - input: { - type: 'email.password.reset.init', - email: values.email, - server: server.domain, - }, - onSuccess(output) { - onSuccess(output); - }, - onError(error) { - toast.error(error.message); - }, - }); - }; - return (
- + - ); diff --git a/packages/ui/src/components/accounts/email-register.tsx b/packages/ui/src/components/auth/email-register-form.tsx similarity index 75% rename from packages/ui/src/components/accounts/email-register.tsx rename to packages/ui/src/components/auth/email-register-form.tsx index 0f7f097f..8a573f08 100644 --- a/packages/ui/src/components/accounts/email-register.tsx +++ b/packages/ui/src/components/auth/email-register-form.tsx @@ -1,11 +1,8 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Mail } from 'lucide-react'; import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; import { z } from 'zod/v4'; -import { LoginOutput } from '@colanode/core'; -import { GoogleLogin } from '@colanode/ui/components/accounts/google-login'; import { Button } from '@colanode/ui/components/ui/button'; import { Form, @@ -17,8 +14,6 @@ import { import { Input } from '@colanode/ui/components/ui/input'; import { Label } from '@colanode/ui/components/ui/label'; import { Spinner } from '@colanode/ui/components/ui/spinner'; -import { useServer } from '@colanode/ui/contexts/server'; -import { useMutation } from '@colanode/ui/hooks/use-mutation'; const formSchema = z .object({ @@ -40,15 +35,12 @@ const formSchema = z path: ['confirmPassword'], // path of error }); -interface EmailRegisterProps { - onSuccess: (output: LoginOutput) => void; - onLogin: () => void; +interface RegisterFormProps { + isPending: boolean; + onSubmit: (values: z.infer) => void; } -export const EmailRegister = ({ onSuccess, onLogin }: EmailRegisterProps) => { - const server = useServer(); - const { mutate, isPending } = useMutation(); - +export const RegisterForm = ({ isPending, onSubmit }: RegisterFormProps) => { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -59,24 +51,6 @@ export const EmailRegister = ({ onSuccess, onLogin }: EmailRegisterProps) => { }, }); - const onSubmit = async (values: z.infer) => { - mutate({ - input: { - type: 'email.register', - name: values.name, - email: values.email, - password: values.password, - server: server.domain, - }, - onSuccess(output) { - onSuccess(output); - }, - onError(error) { - toast.error(error.message); - }, - }); - }; - return (
@@ -159,15 +133,6 @@ export const EmailRegister = ({ onSuccess, onLogin }: EmailRegisterProps) => { )} Register - - ); diff --git a/packages/ui/src/components/accounts/email-verify.tsx b/packages/ui/src/components/auth/email-verify-form.tsx similarity index 63% rename from packages/ui/src/components/accounts/email-verify.tsx rename to packages/ui/src/components/auth/email-verify-form.tsx index 778e25a6..4be5b855 100644 --- a/packages/ui/src/components/accounts/email-verify.tsx +++ b/packages/ui/src/components/auth/email-verify-form.tsx @@ -1,10 +1,8 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Mail } from 'lucide-react'; import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; import { z } from 'zod/v4'; -import { LoginOutput } from '@colanode/core'; import { Button } from '@colanode/ui/components/ui/button'; import { Form, @@ -16,30 +14,23 @@ import { import { Input } from '@colanode/ui/components/ui/input'; import { Label } from '@colanode/ui/components/ui/label'; import { Spinner } from '@colanode/ui/components/ui/spinner'; -import { useServer } from '@colanode/ui/contexts/server'; import { useCountdown } from '@colanode/ui/hooks/use-countdown'; -import { useMutation } from '@colanode/ui/hooks/use-mutation'; const formSchema = z.object({ otp: z.string().min(2), }); -interface EmailVerifyProps { - id: string; +interface EmailVerifyFormProps { expiresAt: Date; - onSuccess: (output: LoginOutput) => void; - onBack: () => void; + isPending: boolean; + onSubmit: (values: z.infer) => void; } -export const EmailVerify = ({ - id, +export const EmailVerifyForm = ({ expiresAt, - onSuccess, - onBack, -}: EmailVerifyProps) => { - const server = useServer(); - const { mutate, isPending } = useMutation(); - + isPending, + onSubmit, +}: EmailVerifyFormProps) => { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -49,31 +40,9 @@ export const EmailVerify = ({ const [remainingSeconds, formattedTime] = useCountdown(expiresAt); - const handleSubmit = async (values: z.infer) => { - if (remainingSeconds <= 0) { - toast.error('Code has expired'); - return; - } - - mutate({ - input: { - type: 'email.verify', - otp: values.otp, - server: server.domain, - id, - }, - onSuccess(output) { - onSuccess(output); - }, - onError(error) { - toast.error(error.message); - }, - }); - }; - return (
- + - ); diff --git a/packages/ui/src/components/accounts/google-login.tsx b/packages/ui/src/components/auth/google-login.tsx similarity index 54% rename from packages/ui/src/components/accounts/google-login.tsx rename to packages/ui/src/components/auth/google-login.tsx index ea1fe1ed..403573c7 100644 --- a/packages/ui/src/components/accounts/google-login.tsx +++ b/packages/ui/src/components/auth/google-login.tsx @@ -1,38 +1,25 @@ import { GoogleOAuthProvider, useGoogleLogin } from '@react-oauth/google'; -import { toast } from 'sonner'; -import { LoginOutput } from '@colanode/core'; import { Button } from '@colanode/ui/components/ui/button'; import { GoogleIcon } from '@colanode/ui/components/ui/icons'; import { Spinner } from '@colanode/ui/components/ui/spinner'; import { useApp } from '@colanode/ui/contexts/app'; -import { useServer } from '@colanode/ui/contexts/server'; -import { useMutation } from '@colanode/ui/hooks/use-mutation'; +import { useAuth } from '@colanode/ui/contexts/auth'; interface GoogleLoginProps { context: 'login' | 'register'; - onSuccess: (output: LoginOutput) => void; + onLogin: (code: string) => void; + isPending: boolean; } -const GoogleLoginButton = ({ context, onSuccess }: GoogleLoginProps) => { - const server = useServer(); - const { mutate, isPending } = useMutation(); - +const GoogleLoginButton = ({ + context, + onLogin, + isPending, +}: GoogleLoginProps) => { const login = useGoogleLogin({ onSuccess: async (response) => { - mutate({ - input: { - type: 'google.login', - code: response.code, - server: server.domain, - }, - onSuccess(output) { - onSuccess(output); - }, - onError(error) { - toast.error(error.message); - }, - }); + onLogin(response.code); }, flow: 'auth-code', }); @@ -55,15 +42,23 @@ const GoogleLoginButton = ({ context, onSuccess }: GoogleLoginProps) => { ); }; -export const GoogleLogin = ({ context, onSuccess }: GoogleLoginProps) => { +export const GoogleLogin = ({ + context, + onLogin, + isPending, +}: GoogleLoginProps) => { const app = useApp(); - const server = useServer(); - const config = server.attributes.account?.google; + const auth = useAuth(); + const config = auth.server.attributes.account?.google; if (app.type === 'web' && config && config.enabled && config.clientId) { return ( - + ); } diff --git a/packages/ui/src/components/auth/login-tab.tsx b/packages/ui/src/components/auth/login-tab.tsx new file mode 100644 index 00000000..3eb95c95 --- /dev/null +++ b/packages/ui/src/components/auth/login-tab.tsx @@ -0,0 +1,6 @@ +import { Tab } from '@colanode/ui/components/layouts/tabs/tab'; +import { defaultIcons } from '@colanode/ui/lib/assets'; + +export const LoginTab = () => { + return ; +}; diff --git a/packages/ui/src/components/auth/login.tsx b/packages/ui/src/components/auth/login.tsx new file mode 100644 index 00000000..921ace69 --- /dev/null +++ b/packages/ui/src/components/auth/login.tsx @@ -0,0 +1,198 @@ +import { useNavigate } from '@tanstack/react-router'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +import { LoginOutput } from '@colanode/core'; +import { LoginForm } from '@colanode/ui/components/auth/email-login-form'; +import { EmailVerifyForm } from '@colanode/ui/components/auth/email-verify-form'; +import { GoogleLogin } from '@colanode/ui/components/auth/google-login'; +import { Button } from '@colanode/ui/components/ui/button'; +import { useAuth } from '@colanode/ui/contexts/auth'; +import { useMutation } from '@colanode/ui/hooks/use-mutation'; + +type LoginState = + | { + type: 'login'; + } + | { + type: 'verify'; + id: string; + expiresAt: Date; + }; + +export const Login = () => { + const navigate = useNavigate(); + const auth = useAuth(); + + const [state, setState] = useState({ type: 'login' }); + + const { mutate: mutateEmailLogin, isPending: isEmailLoginPending } = + useMutation(); + const { mutate: mutateEmailVerify, isPending: isEmailVerifyPending } = + useMutation(); + const { mutate: mutateGoogleLogin, isPending: isGoogleLoginPending } = + useMutation(); + + const handleLoginSubmit = async (values: { + email: string; + password: string; + }) => { + if (isEmailLoginPending) return; + if (isEmailVerifyPending || isGoogleLoginPending) return; + if (state.type !== 'login') return; + + mutateEmailLogin({ + input: { + type: 'email.login', + email: values.email, + password: values.password, + server: auth.server.domain, + }, + onSuccess(output) { + if (output.type === 'success') { + if (output.workspaces.length > 0) { + navigate({ + to: '/workspace/$userId', + params: { userId: output.workspaces[0]!.user.id }, + }); + } else { + navigate({ to: '/create' }); + } + } else if (output.type === 'verify') { + setState({ + type: 'verify', + id: output.id, + expiresAt: output.expiresAt, + }); + } + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + const handleVerifySubmit = async (values: { otp: string }) => { + if (isEmailVerifyPending) return; + if (isEmailLoginPending || isGoogleLoginPending) return; + if (state.type !== 'verify') return; + + mutateEmailVerify({ + input: { + type: 'email.verify', + otp: values.otp, + server: auth.server.domain, + id: state.id, + }, + onSuccess(output: LoginOutput) { + if (output.type === 'success') { + navigate({ + to: '/workspace/$userId', + params: { userId: output.workspaces[0]!.user.id }, + }); + } else if (output.type === 'verify') { + setState({ + type: 'verify', + id: output.id, + expiresAt: output.expiresAt, + }); + } + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + const handleGoogleLogin = async (code: string) => { + if (isGoogleLoginPending) return; + if (isEmailLoginPending || isEmailVerifyPending) return; + if (state.type !== 'login') return; + + mutateGoogleLogin({ + input: { type: 'google.login', code, server: auth.server.domain }, + onSuccess(output) { + if (output.type === 'success') { + if (output.workspaces.length > 0) { + navigate({ + to: '/workspace/$userId', + params: { userId: output.workspaces[0]!.user.id }, + }); + } else { + navigate({ to: '/create' }); + } + } else if (output.type === 'verify') { + setState({ + type: 'verify', + id: output.id, + expiresAt: output.expiresAt, + }); + } + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + return ( +
+
+

+ {state.type === 'login' + ? 'Login to your account' + : 'Verify your email'} +

+

+ {state.type === 'login' + ? 'Enter your email and password to login to your account' + : 'Enter the code sent to your email'} +

+
+
+ {state.type === 'login' && ( + <> + + + + + )} + {state.type === 'verify' && ( + <> + + + + )} +
+
+ ); +}; diff --git a/packages/ui/src/components/accounts/account-logout-container.tsx b/packages/ui/src/components/auth/logout-container.tsx similarity index 97% rename from packages/ui/src/components/accounts/account-logout-container.tsx rename to packages/ui/src/components/auth/logout-container.tsx index 90d1ad12..d94b9dae 100644 --- a/packages/ui/src/components/accounts/account-logout-container.tsx +++ b/packages/ui/src/components/auth/logout-container.tsx @@ -7,7 +7,7 @@ import { Spinner } from '@colanode/ui/components/ui/spinner'; import { useWorkspace } from '@colanode/ui/contexts/workspace'; import { useMutation } from '@colanode/ui/hooks/use-mutation'; -export const AccountLogoutContainer = () => { +export const LogoutContainer = () => { const workspace = useWorkspace(); const navigate = useNavigate(); const { mutate, isPending } = useMutation(); diff --git a/packages/ui/src/components/accounts/account-logout-header.tsx b/packages/ui/src/components/auth/logout-header.tsx similarity index 85% rename from packages/ui/src/components/accounts/account-logout-header.tsx rename to packages/ui/src/components/auth/logout-header.tsx index a1e937e4..50b1f92f 100644 --- a/packages/ui/src/components/accounts/account-logout-header.tsx +++ b/packages/ui/src/components/auth/logout-header.tsx @@ -1,7 +1,7 @@ import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item'; import { defaultIcons } from '@colanode/ui/lib/assets'; -export const AccountLogoutHeader = () => { +export const LogoutHeader = () => { return ( ); diff --git a/packages/ui/src/components/accounts/account-logout-tab.tsx b/packages/ui/src/components/auth/logout-tab.tsx similarity index 84% rename from packages/ui/src/components/accounts/account-logout-tab.tsx rename to packages/ui/src/components/auth/logout-tab.tsx index ad74bba6..ac50c08b 100644 --- a/packages/ui/src/components/accounts/account-logout-tab.tsx +++ b/packages/ui/src/components/auth/logout-tab.tsx @@ -1,6 +1,6 @@ import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item'; import { defaultIcons } from '@colanode/ui/lib/assets'; -export const AccountLogoutTab = () => { +export const LogoutTab = () => { return ; }; diff --git a/packages/ui/src/components/auth/register-tab.tsx b/packages/ui/src/components/auth/register-tab.tsx new file mode 100644 index 00000000..69545a55 --- /dev/null +++ b/packages/ui/src/components/auth/register-tab.tsx @@ -0,0 +1,6 @@ +import { Tab } from '@colanode/ui/components/layouts/tabs/tab'; +import { defaultIcons } from '@colanode/ui/lib/assets'; + +export const RegisterTab = () => { + return ; +}; diff --git a/packages/ui/src/components/auth/register.tsx b/packages/ui/src/components/auth/register.tsx new file mode 100644 index 00000000..85e182da --- /dev/null +++ b/packages/ui/src/components/auth/register.tsx @@ -0,0 +1,201 @@ +import { useNavigate } from '@tanstack/react-router'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +import { LoginOutput } from '@colanode/core'; +import { RegisterForm } from '@colanode/ui/components/auth/email-register-form'; +import { EmailVerifyForm } from '@colanode/ui/components/auth/email-verify-form'; +import { GoogleLogin } from '@colanode/ui/components/auth/google-login'; +import { Button } from '@colanode/ui/components/ui/button'; +import { useAuth } from '@colanode/ui/contexts/auth'; +import { useMutation } from '@colanode/ui/hooks/use-mutation'; + +type RegisterState = + | { + type: 'register'; + } + | { + type: 'verify'; + id: string; + expiresAt: Date; + }; + +export const Register = () => { + const navigate = useNavigate(); + const auth = useAuth(); + + const [state, setState] = useState({ type: 'register' }); + + const { mutate: mutateEmailRegister, isPending: isEmailRegisterPending } = + useMutation(); + const { mutate: mutateEmailVerify, isPending: isEmailVerifyPending } = + useMutation(); + const { mutate: mutateGoogleRegister, isPending: isGoogleRegisterPending } = + useMutation(); + + const handleRegisterSubmit = async (values: { + name: string; + email: string; + password: string; + confirmPassword: string; + }) => { + if (isEmailRegisterPending) return; + if (isEmailVerifyPending || isGoogleRegisterPending) return; + if (state.type !== 'register') return; + + mutateEmailRegister({ + input: { + type: 'email.register', + name: values.name, + email: values.email, + password: values.password, + server: auth.server.domain, + }, + onSuccess(output) { + if (output.type === 'success') { + if (output.workspaces.length > 0) { + navigate({ + to: '/workspace/$userId', + params: { userId: output.workspaces[0]!.user.id }, + }); + } else { + navigate({ to: '/create' }); + } + } else if (output.type === 'verify') { + setState({ + type: 'verify', + id: output.id, + expiresAt: output.expiresAt, + }); + } + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + const handleVerifySubmit = async (values: { otp: string }) => { + if (isEmailVerifyPending) return; + if (isEmailRegisterPending || isGoogleRegisterPending) return; + if (state.type !== 'verify') return; + + mutateEmailVerify({ + input: { + type: 'email.verify', + otp: values.otp, + server: auth.server.domain, + id: state.id, + }, + onSuccess(output: LoginOutput) { + if (output.type === 'success') { + navigate({ + to: '/workspace/$userId', + params: { userId: output.workspaces[0]!.user.id }, + }); + } else if (output.type === 'verify') { + setState({ + type: 'verify', + id: output.id, + expiresAt: output.expiresAt, + }); + } + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + const handleGoogleRegister = async (code: string) => { + if (isGoogleRegisterPending) return; + if (isEmailRegisterPending || isEmailVerifyPending) return; + if (state.type !== 'register') return; + + mutateGoogleRegister({ + input: { type: 'google.login', code, server: auth.server.domain }, + onSuccess(output) { + if (output.type === 'success') { + if (output.workspaces.length > 0) { + navigate({ + to: '/workspace/$userId', + params: { userId: output.workspaces[0]!.user.id }, + }); + } else { + navigate({ to: '/create' }); + } + } else if (output.type === 'verify') { + setState({ + type: 'verify', + id: output.id, + expiresAt: output.expiresAt, + }); + } + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + return ( +
+
+

+ {state.type === 'register' + ? 'Create an account' + : 'Verify your email'} +

+

+ {state.type === 'register' + ? 'Sign up to get started with Colanode' + : 'Enter the code sent to your email'} +

+
+
+ {state.type === 'register' && ( + <> + + + + + )} + {state.type === 'verify' && ( + <> + + + + )} +
+
+ ); +}; diff --git a/packages/ui/src/components/auth/reset-tab.tsx b/packages/ui/src/components/auth/reset-tab.tsx new file mode 100644 index 00000000..cb82f559 --- /dev/null +++ b/packages/ui/src/components/auth/reset-tab.tsx @@ -0,0 +1,6 @@ +import { Tab } from '@colanode/ui/components/layouts/tabs/tab'; +import { defaultIcons } from '@colanode/ui/lib/assets'; + +export const ResetTab = () => { + return ; +}; diff --git a/packages/ui/src/components/auth/reset.tsx b/packages/ui/src/components/auth/reset.tsx new file mode 100644 index 00000000..8afe67f0 --- /dev/null +++ b/packages/ui/src/components/auth/reset.tsx @@ -0,0 +1,174 @@ +import { useNavigate } from '@tanstack/react-router'; +import { CheckCircle } from 'lucide-react'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +import { EmailPasswordResetInitOutput } from '@colanode/core'; +import { PasswordResetCompleteForm } from '@colanode/ui/components/auth/email-password-reset-complete-form'; +import { PasswordResetInitForm } from '@colanode/ui/components/auth/email-password-reset-init-form'; +import { Button } from '@colanode/ui/components/ui/button'; +import { useAuth } from '@colanode/ui/contexts/auth'; +import { useMutation } from '@colanode/ui/hooks/use-mutation'; + +type ResetState = + | { + type: 'init'; + } + | { + type: 'complete'; + id: string; + expiresAt: Date; + } + | { + type: 'success'; + }; + +export const Reset = () => { + const navigate = useNavigate(); + const auth = useAuth(); + + const [state, setState] = useState({ type: 'init' }); + + const { + mutate: mutatePasswordResetInit, + isPending: isPasswordResetInitPending, + } = useMutation(); + const { + mutate: mutatePasswordResetComplete, + isPending: isPasswordResetCompletePending, + } = useMutation(); + + const handleInitSubmit = async (values: { email: string }) => { + if (isPasswordResetInitPending) return; + if (isPasswordResetCompletePending) return; + if (state.type !== 'init') return; + + mutatePasswordResetInit({ + input: { + type: 'email.password.reset.init', + email: values.email, + server: auth.server.domain, + }, + onSuccess(output: EmailPasswordResetInitOutput) { + setState({ + type: 'complete', + id: output.id, + expiresAt: output.expiresAt, + }); + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + const handleCompleteSubmit = async (values: { + otp: string; + password: string; + confirmPassword: string; + }) => { + if (isPasswordResetCompletePending) return; + if (isPasswordResetInitPending) return; + if (state.type !== 'complete') return; + + mutatePasswordResetComplete({ + input: { + type: 'email.password.reset.complete', + otp: values.otp, + password: values.password, + server: auth.server.domain, + id: state.id, + }, + onSuccess() { + setState({ type: 'success' }); + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + return ( +
+
+

+ {state.type === 'init' + ? 'Reset your password' + : state.type === 'complete' + ? 'Reset your password' + : 'Password reset successful'} +

+

+ {state.type === 'init' + ? 'Enter your email to receive a password reset code' + : state.type === 'complete' + ? 'Enter the code sent to your email and your new password' + : 'Your password has been reset. You can now login with your new password.'} +

+
+
+ {state.type === 'init' && ( + <> + + + + )} + {state.type === 'complete' && ( + <> + + + + )} + {state.type === 'success' && ( + <> +
+ +

+ Your password has been reset. You can now login with your new + password. +

+

+ You have been logged out of all devices. +

+
+ + + )} +
+
+ ); +}; diff --git a/packages/ui/src/components/layouts/sidebars/sidebar-menu-footer.tsx b/packages/ui/src/components/layouts/sidebars/sidebar-menu-footer.tsx index ef15fa4f..d3d78a62 100644 --- a/packages/ui/src/components/layouts/sidebars/sidebar-menu-footer.tsx +++ b/packages/ui/src/components/layouts/sidebars/sidebar-menu-footer.tsx @@ -120,7 +120,7 @@ export function SidebarMenuFooter() { { - navigate({ to: '/login' }); + navigate({ to: '/auth/login' }); }} > diff --git a/packages/ui/src/components/layouts/sidebars/sidebar-settings.tsx b/packages/ui/src/components/layouts/sidebars/sidebar-settings.tsx index cd81bc9b..281a65a0 100644 --- a/packages/ui/src/components/layouts/sidebars/sidebar-settings.tsx +++ b/packages/ui/src/components/layouts/sidebars/sidebar-settings.tsx @@ -121,7 +121,7 @@ export const SidebarSettings = () => {
- + {({ isActive }) => ( void; - onCreate: (server: Server) => void; } -export const ServerCreateDialog = ({ - onCancel, - onCreate, -}: ServerCreateDialogProps) => { +export const ServerCreateDialog = ({ onCancel }: ServerCreateDialogProps) => { const [open, setOpen] = useState(true); const { mutate, isPending } = useMutation(); const [url, setUrl] = useState(''); @@ -63,9 +58,8 @@ export const ServerCreateDialog = ({ type: 'server.create', url, }, - onSuccess(output) { - onCreate(output.server); - toast.success('Server added successfully'); + onSuccess() { + setOpen(false); }, onError(error) { toast.error(error.message); diff --git a/packages/ui/src/components/servers/server-dropdown.tsx b/packages/ui/src/components/servers/server-dropdown.tsx deleted file mode 100644 index aaa1ff20..00000000 --- a/packages/ui/src/components/servers/server-dropdown.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { - ChevronDown, - PlusIcon, - ServerOffIcon, - SettingsIcon, -} from 'lucide-react'; -import { Fragment, useState } from 'react'; - -import { Server } from '@colanode/client/types'; -import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar'; -import { ServerCreateDialog } from '@colanode/ui/components/servers/server-create-dialog'; -import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog'; -import { ServerSettingsDialog } from '@colanode/ui/components/servers/server-settings-dialog'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@colanode/ui/components/ui/dropdown-menu'; - -interface ServerDropdownProps { - value: string | null; - onChange: (server: string) => void; - servers: Server[]; - readonly?: boolean; -} - -export const ServerDropdown = ({ - value, - onChange, - servers, - readonly = false, -}: ServerDropdownProps) => { - const [open, setOpen] = useState(false); - const [openCreate, setOpenCreate] = useState(false); - const [settingsDomain, setSettingsDomain] = useState(null); - const [deleteDomain, setDeleteDomain] = useState(null); - - const server = value - ? servers.find((server) => server.domain === value) - : null; - const settingsServer = servers.find( - (server) => server.domain === settingsDomain - ); - const deleteServer = servers.find((server) => server.domain === deleteDomain); - - return ( - - { - if (!readonly) { - setOpen(openValue); - } - }} - > - -
- {server ? ( - - ) : ( - - )} -
- {server ? ( - -

{server.name}

-

- {server.domain} -

-
- ) : ( -

Select a server

- )} -
- -
-
- - {servers.map((server) => ( - { - if (value !== server.domain) { - onChange(server.domain); - } - }} - className="group/server flex w-full grow flex-row items-center gap-3 p-2 cursor-pointer hover:bg-accent" - > -
- -
-

{server.name}

-

- {server.domain} -

-
-
- -
- ))} - - { - setOpenCreate(true); - }} - className="py-2" - > - - Add new server - -
-
- {openCreate && ( - setOpenCreate(false)} - onCreate={() => { - setOpenCreate(false); - }} - /> - )} - {deleteServer && ( - { - if (!open) { - setDeleteDomain(null); - } - }} - /> - )} - {settingsServer && ( - { - if (!open) { - setSettingsDomain(null); - } - }} - onDelete={() => { - setSettingsDomain(null); - setDeleteDomain(settingsServer.domain); - }} - /> - )} -
- ); -}; diff --git a/packages/ui/src/components/servers/server-settings-dialog.tsx b/packages/ui/src/components/servers/server-settings-dialog.tsx index fdfd1e04..af2af448 100644 --- a/packages/ui/src/components/servers/server-settings-dialog.tsx +++ b/packages/ui/src/components/servers/server-settings-dialog.tsx @@ -175,16 +175,23 @@ export const ServerSettingsDialog = ({ {canDelete && (
-

Delete server from this device

- +
+

+ Delete server from this device +

+
+ +
+
)} diff --git a/packages/ui/src/components/ui/logo.tsx b/packages/ui/src/components/ui/logo.tsx new file mode 100644 index 00000000..445abf47 --- /dev/null +++ b/packages/ui/src/components/ui/logo.tsx @@ -0,0 +1,25 @@ +type ColanodeLogoProps = React.HTMLAttributes; + +export const ColanodeLogo = (props: ColanodeLogoProps) => { + return ( + + + + + ); +}; diff --git a/packages/ui/src/contexts/auth.ts b/packages/ui/src/contexts/auth.ts new file mode 100644 index 00000000..8238079d --- /dev/null +++ b/packages/ui/src/contexts/auth.ts @@ -0,0 +1,17 @@ +import { createContext, useContext } from 'react'; + +import { Server } from '@colanode/client/types'; + +export interface AuthContextValue { + server: Server; +} + +export const AuthContext = createContext(null); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthLayout'); + } + return context; +}; diff --git a/packages/ui/src/lib/assets.ts b/packages/ui/src/lib/assets.ts index 951d17f2..e111ea05 100644 --- a/packages/ui/src/lib/assets.ts +++ b/packages/ui/src/lib/assets.ts @@ -12,6 +12,7 @@ export const defaultIcons = { bookmark: '01jhzfk3g4q40x7927qcm0hrjdic', folder: '01jhzfk3jrgc276z2gdabm4cwmic', apps: '01jhzfk4m7djqd1pw0e1671cmric', + login: '01jhzfk4ppvzdambh6hyw0mv91ic', logout: '01jhzfk4pv13qxjprqgqfeqp73ic', settings: '01jhzfk4ra4fvcay6qgrydgsf5ic', appearance: '01jhzfk39qxa7xtr7z69fyrb2pic', diff --git a/packages/ui/src/routes/auth/index.tsx b/packages/ui/src/routes/auth/index.tsx new file mode 100644 index 00000000..9553c6d8 --- /dev/null +++ b/packages/ui/src/routes/auth/index.tsx @@ -0,0 +1,10 @@ +import { createRoute } from '@tanstack/react-router'; + +import { AuthLayout } from '@colanode/ui/components/auth/auth-layout'; +import { rootRoute } from '@colanode/ui/routes/root'; + +export const authRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/auth', + component: AuthLayout, +}); diff --git a/packages/ui/src/routes/auth/login.tsx b/packages/ui/src/routes/auth/login.tsx new file mode 100644 index 00000000..4142e460 --- /dev/null +++ b/packages/ui/src/routes/auth/login.tsx @@ -0,0 +1,16 @@ +import { createRoute } from '@tanstack/react-router'; + +import { Login } from '@colanode/ui/components/auth/login'; +import { LoginTab } from '@colanode/ui/components/auth/login-tab'; +import { authRoute } from '@colanode/ui/routes/auth'; + +export const loginRoute = createRoute({ + getParentRoute: () => authRoute, + path: '/login', + component: Login, + context: () => { + return { + tab: , + }; + }, +}); diff --git a/packages/ui/src/routes/auth/register.tsx b/packages/ui/src/routes/auth/register.tsx new file mode 100644 index 00000000..77b84ebd --- /dev/null +++ b/packages/ui/src/routes/auth/register.tsx @@ -0,0 +1,16 @@ +import { createRoute } from '@tanstack/react-router'; + +import { Register } from '@colanode/ui/components/auth/register'; +import { RegisterTab } from '@colanode/ui/components/auth/register-tab'; +import { authRoute } from '@colanode/ui/routes/auth'; + +export const registerRoute = createRoute({ + getParentRoute: () => authRoute, + path: '/register', + component: Register, + context: () => { + return { + tab: , + }; + }, +}); diff --git a/packages/ui/src/routes/auth/reset.tsx b/packages/ui/src/routes/auth/reset.tsx new file mode 100644 index 00000000..b3483f06 --- /dev/null +++ b/packages/ui/src/routes/auth/reset.tsx @@ -0,0 +1,23 @@ +import { createRoute } from '@tanstack/react-router'; +import { z } from 'zod/v4'; + +import { Reset } from '@colanode/ui/components/auth/reset'; +import { ResetTab } from '@colanode/ui/components/auth/reset-tab'; +import { authRoute } from '@colanode/ui/routes/auth'; + +const resetSearchSchema = z.object({ + id: z.string().optional(), + expiresAt: z.string().optional(), +}); + +export const resetRoute = createRoute({ + getParentRoute: () => authRoute, + path: '/reset', + component: Reset, + validateSearch: resetSearchSchema, + context: () => { + return { + tab: , + }; + }, +}); diff --git a/packages/ui/src/routes/home.tsx b/packages/ui/src/routes/home.tsx index a67864dd..09157125 100644 --- a/packages/ui/src/routes/home.tsx +++ b/packages/ui/src/routes/home.tsx @@ -17,6 +17,6 @@ export const homeRoute = createRoute({ }); } - throw redirect({ to: '/login', replace: true }); + throw redirect({ to: '/auth/login', replace: true }); }, }); diff --git a/packages/ui/src/routes/index.tsx b/packages/ui/src/routes/index.tsx index ccfc3cc7..ac000609 100644 --- a/packages/ui/src/routes/index.tsx +++ b/packages/ui/src/routes/index.tsx @@ -1,17 +1,16 @@ import { createRouter } from '@tanstack/react-router'; +import { authRoute } from '@colanode/ui/routes/auth'; +import { loginRoute } from '@colanode/ui/routes/auth/login'; +import { registerRoute } from '@colanode/ui/routes/auth/register'; +import { resetRoute } from '@colanode/ui/routes/auth/reset'; import { workspaceCreateRoute } from '@colanode/ui/routes/create'; import { homeRoute } from '@colanode/ui/routes/home'; -import { loginRoute } from '@colanode/ui/routes/login'; import { rootRoute } from '@colanode/ui/routes/root'; import { workspaceRoute, workspaceMaskRoute, } from '@colanode/ui/routes/workspace'; -import { - accountLogoutMaskRoute, - accountLogoutRoute, -} from '@colanode/ui/routes/workspace/account-logout'; import { accountSettingsMaskRoute, accountSettingsRoute, @@ -28,6 +27,10 @@ import { workspaceHomeMaskRoute, workspaceHomeRoute, } from '@colanode/ui/routes/workspace/home'; +import { + logoutMaskRoute, + logoutRoute, +} from '@colanode/ui/routes/workspace/logout'; import { nodeMaskRoute, nodeRoute } from '@colanode/ui/routes/workspace/node'; import { workspaceRedirectMaskRoute, @@ -52,7 +55,7 @@ import { export const routeTree = rootRoute.addChildren([ homeRoute, - loginRoute, + authRoute.addChildren([loginRoute, registerRoute, resetRoute]), workspaceCreateRoute, workspaceRoute.addChildren([ workspaceRedirectRoute, @@ -64,7 +67,7 @@ export const routeTree = rootRoute.addChildren([ workspaceUsersRoute, workspaceSettingsRoute, accountSettingsRoute, - accountLogoutRoute, + logoutRoute, appAppearanceRoute, ]), workspaceMaskRoute.addChildren([ @@ -77,7 +80,7 @@ export const routeTree = rootRoute.addChildren([ workspaceUploadsMaskRoute, workspaceDownloadsMaskRoute, accountSettingsMaskRoute, - accountLogoutMaskRoute, + logoutMaskRoute, appAppearanceMaskRoute, ]), ]); diff --git a/packages/ui/src/routes/login.tsx b/packages/ui/src/routes/login.tsx deleted file mode 100644 index 5faefb48..00000000 --- a/packages/ui/src/routes/login.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createRoute } from '@tanstack/react-router'; - -import { Login } from '@colanode/ui/components/accounts/login'; -import { LoginTab } from '@colanode/ui/components/accounts/login-tab'; -import { rootRoute } from '@colanode/ui/routes/root'; - -export const loginRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/login', - component: Login, - context: () => { - return { - tab: , - }; - }, -}); diff --git a/packages/ui/src/routes/masks.tsx b/packages/ui/src/routes/masks.tsx index bccf7dd8..c3a3a354 100644 --- a/packages/ui/src/routes/masks.tsx +++ b/packages/ui/src/routes/masks.tsx @@ -113,8 +113,8 @@ export const accountSettingsRouteMask = createRouteMask({ export const accountLogoutRouteMask = createRouteMask({ routeTree: routeTree, - from: '/workspace/$userId/account/logout', - to: '/$workspaceId/account/logout', + from: '/workspace/$userId/logout', + to: '/$workspaceId/logout', params: (ctx) => { const workspace = collections.workspaces.get(ctx.userId); return { diff --git a/packages/ui/src/routes/workspace/account-logout.tsx b/packages/ui/src/routes/workspace/account-logout.tsx deleted file mode 100644 index 6bbeea42..00000000 --- a/packages/ui/src/routes/workspace/account-logout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { createRoute, redirect } from '@tanstack/react-router'; - -import { AccountLogoutContainer } from '@colanode/ui/components/accounts/account-logout-container'; -import { AccountLogoutHeader } from '@colanode/ui/components/accounts/account-logout-header'; -import { AccountLogoutTab } from '@colanode/ui/components/accounts/account-logout-tab'; -import { getWorkspaceUserId } from '@colanode/ui/routes/utils'; -import { - workspaceRoute, - workspaceMaskRoute, -} from '@colanode/ui/routes/workspace'; - -export const accountLogoutRoute = createRoute({ - getParentRoute: () => workspaceRoute, - path: '/account/logout', - component: AccountLogoutContainer, - context: () => { - return { - tab: , - header: , - }; - }, -}); - -export const accountLogoutMaskRoute = createRoute({ - getParentRoute: () => workspaceMaskRoute, - path: '/account/logout', - component: () => null, - beforeLoad: (ctx) => { - const userId = getWorkspaceUserId(ctx.params.workspaceId); - if (userId) { - throw redirect({ - to: '/workspace/$userId/account/logout', - params: { userId }, - replace: true, - }); - } - }, -}); diff --git a/packages/ui/src/routes/workspace/logout.tsx b/packages/ui/src/routes/workspace/logout.tsx new file mode 100644 index 00000000..6720df95 --- /dev/null +++ b/packages/ui/src/routes/workspace/logout.tsx @@ -0,0 +1,38 @@ +import { createRoute, redirect } from '@tanstack/react-router'; + +import { LogoutContainer } from '@colanode/ui/components/auth/logout-container'; +import { LogoutHeader } from '@colanode/ui/components/auth/logout-header'; +import { LogoutTab } from '@colanode/ui/components/auth/logout-tab'; +import { getWorkspaceUserId } from '@colanode/ui/routes/utils'; +import { + workspaceRoute, + workspaceMaskRoute, +} from '@colanode/ui/routes/workspace'; + +export const logoutRoute = createRoute({ + getParentRoute: () => workspaceRoute, + path: '/logout', + component: LogoutContainer, + context: () => { + return { + tab: , + header: , + }; + }, +}); + +export const logoutMaskRoute = createRoute({ + getParentRoute: () => workspaceMaskRoute, + path: '/logout', + component: () => null, + beforeLoad: (ctx) => { + const userId = getWorkspaceUserId(ctx.params.workspaceId); + if (userId) { + throw redirect({ + to: '/workspace/$userId/logout', + params: { userId }, + replace: true, + }); + } + }, +}); diff --git a/scripts/src/postinstall/README.md b/scripts/src/postinstall/README.md index 407d2b8b..5b8855b4 100644 --- a/scripts/src/postinstall/README.md +++ b/scripts/src/postinstall/README.md @@ -10,14 +10,14 @@ The script copies different types of assets to different Colanode apps based on - **Full databases**: `emojis.db` and `icons.db` (with SVG BLOBs) for offline capability - **SVG sprites**: `emojis.svg` and `icons.svg` for efficient rendering -- **Fonts**: Custom fonts like `neotrax.otf` +- **Fonts**: Custom fonts like `satoshi-variable.woff2` - **Images**: Application logos and icons in various formats (.png, .ico, .icns) **Web Application (`apps/web/public/assets/`)**: - **Minimal databases**: `emojis.min.db` and `icons.min.db` (renamed to `emojis.db` and `icons.db`) without SVG BLOBs for faster loading - **SVG sprites**: `emojis.svg` and `icons.svg` for rendering (since SVG data isn't in the minimal databases) -- **Fonts**: Custom fonts like `neotrax.otf` +- **Fonts**: Custom fonts like `satoshi-variable.woff2` - **Images**: Web-specific logo files in PNG format ## Why This Approach? diff --git a/scripts/src/postinstall/index.ts b/scripts/src/postinstall/index.ts index f2a0a39c..0ae16c6b 100644 --- a/scripts/src/postinstall/index.ts +++ b/scripts/src/postinstall/index.ts @@ -14,9 +14,16 @@ const ICONS_DB_PATH = path.resolve(ICONS_DIR, 'icons.db'); const ICONS_MIN_DB_PATH = path.resolve(ICONS_DIR, 'icons.min.db'); const ICONS_SVG_PATH = path.resolve(ICONS_DIR, 'icons.svg'); -const NEOTRAX_FONT_NAME = 'neotrax.otf'; +const SATOSHI_FONT_NAME = 'satoshi-variable.woff2'; +const SATOSHI_ITALIC_FONT_NAME = 'satoshi-variable-italic.woff2'; +const ANTONIO_FONT_NAME = 'antonio.ttf'; const FONTS_DIR = path.resolve(ASSETS_DIR, 'fonts'); -const FONTS_OTF_PATH = path.resolve(FONTS_DIR, NEOTRAX_FONT_NAME); +const FONTS_SATOSHI_PATH = path.resolve(FONTS_DIR, SATOSHI_FONT_NAME); +const FONTS_SATOSHI_ITALIC_PATH = path.resolve( + FONTS_DIR, + SATOSHI_ITALIC_FONT_NAME +); +const FONTS_ANTONIO_PATH = path.resolve(FONTS_DIR, ANTONIO_FONT_NAME); const DESKTOP_ASSETS_DIR = path.resolve('apps', 'desktop', 'assets'); const WEB_PUBLIC_DIR = path.resolve('apps', 'web', 'public'); @@ -51,10 +58,22 @@ const execute = () => { copyFile(ICONS_MIN_DB_PATH, path.resolve(WEB_ASSETS_DIR, 'icons.db')); copyFile(ICONS_SVG_PATH, path.resolve(WEB_ASSETS_DIR, 'icons.svg')); - copyFile(FONTS_OTF_PATH, [ - path.resolve(DESKTOP_ASSETS_DIR, 'fonts', NEOTRAX_FONT_NAME), - path.resolve(WEB_ASSETS_DIR, 'fonts', NEOTRAX_FONT_NAME), - path.resolve(MOBILE_ASSETS_DIR, 'fonts', NEOTRAX_FONT_NAME), + copyFile(FONTS_SATOSHI_PATH, [ + path.resolve(DESKTOP_ASSETS_DIR, 'fonts', SATOSHI_FONT_NAME), + path.resolve(WEB_ASSETS_DIR, 'fonts', SATOSHI_FONT_NAME), + path.resolve(MOBILE_ASSETS_DIR, 'fonts', SATOSHI_FONT_NAME), + ]); + + copyFile(FONTS_SATOSHI_ITALIC_PATH, [ + path.resolve(DESKTOP_ASSETS_DIR, 'fonts', SATOSHI_ITALIC_FONT_NAME), + path.resolve(WEB_ASSETS_DIR, 'fonts', SATOSHI_ITALIC_FONT_NAME), + path.resolve(MOBILE_ASSETS_DIR, 'fonts', SATOSHI_ITALIC_FONT_NAME), + ]); + + copyFile(FONTS_ANTONIO_PATH, [ + path.resolve(DESKTOP_ASSETS_DIR, 'fonts', ANTONIO_FONT_NAME), + path.resolve(WEB_ASSETS_DIR, 'fonts', ANTONIO_FONT_NAME), + path.resolve(MOBILE_ASSETS_DIR, 'fonts', ANTONIO_FONT_NAME), ]); copyFile(