From cacdccd2bee38a26273b03ce37e8b94f9d00dcab Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 18 Mar 2024 13:26:09 +0200 Subject: [PATCH 01/16] [nan-578] workos implementtion --- package-lock.json | 17 +++++ .../server/lib/controllers/auth.controller.ts | 74 +++++++++++++++++++ packages/server/lib/server.ts | 12 +-- packages/server/package.json | 1 + packages/webapp/src/pages/Signup.tsx | 56 ++++++++++++++ 5 files changed, 155 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 778c9a8720..4d19ad7d99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8643,6 +8643,14 @@ } } }, + "node_modules/@workos-inc/node": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@workos-inc/node/-/node-6.2.0.tgz", + "integrity": "sha512-nrfhsEsFUNhYzS4BW0WnuCgU+668ixANaDfGuCfz7bcSb1lrgmfMmTnqDlllsLjXmMYPT4Z4KerQjOq2+fRDoQ==", + "dependencies": { + "pluralize": "8.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "license": "BSD-3-Clause" @@ -20490,6 +20498,14 @@ "node": ">=4" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", @@ -27376,6 +27392,7 @@ "dependencies": { "@hapi/boom": "^10.0.1", "@nangohq/shared": "^0.39.5", + "@workos-inc/node": "^6.2.0", "axios": "^1.3.4", "connect-session-knex": "^3.0.1", "cookie-parser": "^1.4.6", diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 83d25ccb4c..20847f467e 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -1,4 +1,5 @@ import type { Request, Response, NextFunction } from 'express'; +import { WorkOS } from '@workos-inc/node'; import crypto from 'crypto'; import util from 'util'; import { resetPasswordSecret, getUserAccountAndEnvironmentFromSession } from '../utils/utils.js'; @@ -26,6 +27,11 @@ export interface WebUser { name: string; } +let workos: WorkOS | null = null; +if (process.env['WORKOS_API_KEY']) { + workos = new WorkOS(process.env['WORKOS_API_KEY']); +} + class AuthController { async signin(req: Request, res: Response, next: NextFunction) { try { @@ -260,6 +266,74 @@ class AuthController { next(error); } } + + async getSocialLogin(req: Request, res: Response, next: NextFunction) { + try { + const provider = req.query['provider'] as string; + + if (!workos) { + errorManager.errRes(res, 'workos_not_configured'); + return; + } + + const oAuthUrl = workos?.userManagement.getAuthorizationUrl({ + clientId: process.env['WORKOS_CLIENT_ID'] || '', + provider, + redirectUri: `${getBaseUrl()}/api/v1/social/callback` + }); + console.log(oAuthUrl); + + res.send({ url: oAuthUrl }); + } catch (err) { + next(err); + } + } + + async socialLoginCallback(req: Request, res: Response, next: NextFunction) { + try { + const { code } = req.query; + + if (!workos) { + errorManager.errRes(res, 'workos_not_configured'); + return; + } + + const { user: authorizedUser } = await workos.userManagement.authenticateWithCode({ + clientId: process.env['WORKOS_CLIENT_ID'] || '', + code: code as string + }); + + let user: User | null = null; + const existingUser = await userService.getUserByEmail(authorizedUser.email); + + if (existingUser) { + user = existingUser; + } else { + const name = + authorizedUser.firstName || authorizedUser.lastName + ? `${authorizedUser.firstName || ''} ${authorizedUser.lastName || ''}` + : authorizedUser.email.split('@')[0]; + const account = await environmentService.createAccount(`${name}'s Organization`); + if (!account) { + throw new NangoError('account_creation_failure'); + } + const createdUser = await userService.createUser(authorizedUser.email, name as string, '', '', account.id); + if (!createdUser) { + throw new NangoError('user_creation_failure'); + } + user = createdUser; + } + + req.login(user, function (err) { + if (err) { + return next(err); + } + res.redirect(`${getBaseUrl()}/dev`); + }); + } catch (err) { + next(err); + } + } } export default new AuthController(); diff --git a/packages/server/lib/server.ts b/packages/server/lib/server.ts index b0f0f2dbc9..392c3b9350 100644 --- a/packages/server/lib/server.ts +++ b/packages/server/lib/server.ts @@ -55,14 +55,14 @@ const app = express(); // Auth AuthClient.setup(app); -const apiAuth = [authMiddleware.secretKeyAuth, rateLimiterMiddleware]; -const apiPublicAuth = [authMiddleware.publicKeyAuth, rateLimiterMiddleware]; +const apiAuth = [authMiddleware.secretKeyAuth.bind(authMiddleware), rateLimiterMiddleware]; +const apiPublicAuth = [authMiddleware.publicKeyAuth.bind(authMiddleware), rateLimiterMiddleware]; const webAuth = isCloud() || isEnterprise() - ? [passport.authenticate('session'), authMiddleware.sessionAuth, rateLimiterMiddleware] + ? [passport.authenticate('session'), authMiddleware.sessionAuth.bind(authMiddleware), rateLimiterMiddleware] : isBasicAuthEnabled() - ? [passport.authenticate('basic', { session: false }), authMiddleware.basicAuth, rateLimiterMiddleware] - : [authMiddleware.noAuth, rateLimiterMiddleware]; + ? [passport.authenticate('basic', { session: false }), authMiddleware.basicAuth.bind(authMiddleware), rateLimiterMiddleware] + : [authMiddleware.noAuth.bind(authMiddleware), rateLimiterMiddleware]; app.use( express.json({ @@ -149,6 +149,8 @@ if (AUTH_ENABLED) { app.route('/api/v1/signin').post(rateLimiterMiddleware, passport.authenticate('local'), authController.signin.bind(authController)); app.route('/api/v1/forgot-password').put(rateLimiterMiddleware, authController.forgotPassword.bind(authController)); app.route('/api/v1/reset-password').put(rateLimiterMiddleware, authController.resetPassword.bind(authController)); + app.route('/api/v1/social/signup').post(rateLimiterMiddleware, authController.getSocialLogin.bind(authController)); + app.route('/api/v1/social/callback').get(rateLimiterMiddleware, authController.socialLoginCallback.bind(authController)); } // Webapp routes (session auth). diff --git a/packages/server/package.json b/packages/server/package.json index 548c9840cc..69a1eabd01 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -24,6 +24,7 @@ "dependencies": { "@hapi/boom": "^10.0.1", "@nangohq/shared": "^0.39.5", + "@workos-inc/node": "^6.2.0", "axios": "^1.3.4", "connect-session-knex": "^3.0.1", "cookie-parser": "^1.4.6", diff --git a/packages/webapp/src/pages/Signup.tsx b/packages/webapp/src/pages/Signup.tsx index ef3a3a3bf0..fe60149ddc 100644 --- a/packages/webapp/src/pages/Signup.tsx +++ b/packages/webapp/src/pages/Signup.tsx @@ -42,6 +42,25 @@ export default function Signup() { } }; + const googleLogin = async () => { + analyticsTrack('web:account_signup_google'); + const res = await fetch('/api/v1/social/signup?provider=GoogleOAuth', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (res?.status === 200) { + const data = await res.json(); + const { url } = data; + window.location = url; + } else if (res != null) { + const errorMessage = (await res.json()).error; + setServerErrorMessage(errorMessage); + } + }; + return ( <> @@ -111,6 +130,43 @@ export default function Signup() { {serverErrorMessage &&

{serverErrorMessage}

} +

or sign up with

+
+ +
From 8642139242f2cdb1b3279f166253e3717444c96c Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 18 Mar 2024 15:59:38 +0200 Subject: [PATCH 02/16] [nan-578] workos implementation --- .../server/lib/controllers/auth.controller.ts | 41 ++++++++++--- packages/webapp/src/pages/InviteSignup.tsx | 59 +++++++++++++++++++ 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 20847f467e..f2968838e8 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -276,12 +276,14 @@ class AuthController { return; } + const body = req.body; + const oAuthUrl = workos?.userManagement.getAuthorizationUrl({ clientId: process.env['WORKOS_CLIENT_ID'] || '', provider, - redirectUri: `${getBaseUrl()}/api/v1/social/callback` + redirectUri: `${getBaseUrl()}/api/v1/social/callback`, + state: body ? Buffer.from(JSON.stringify(body)).toString('base64') : '' }); - console.log(oAuthUrl); res.send({ url: oAuthUrl }); } catch (err) { @@ -289,9 +291,33 @@ class AuthController { } } + async createAccountIfNotInvited(state: string, name: string) { + const account: { accountId: number; token: string } | null = (state ? JSON.parse(Buffer.from(state, 'base64').toString('ascii')) : null) as { + accountId: number; + token: string; + } | null; + const accountId = typeof account?.accountId !== 'undefined' ? account.accountId : null; + + if (accountId === null) { + const account = await environmentService.createAccount(`${name}'s Organization`); + if (!account) { + throw new NangoError('account_creation_failure'); + } + return account.id; + } else { + const token = account?.token; + const validToken = await userService.getInvitedUserByToken(token as string); + if (validToken) { + await userService.markAcceptedInvite(token as string); + } + } + + return accountId; + } + async socialLoginCallback(req: Request, res: Response, next: NextFunction) { try { - const { code } = req.query; + const { code, state } = req.query; if (!workos) { errorManager.errRes(res, 'workos_not_configured'); @@ -313,11 +339,10 @@ class AuthController { authorizedUser.firstName || authorizedUser.lastName ? `${authorizedUser.firstName || ''} ${authorizedUser.lastName || ''}` : authorizedUser.email.split('@')[0]; - const account = await environmentService.createAccount(`${name}'s Organization`); - if (!account) { - throw new NangoError('account_creation_failure'); - } - const createdUser = await userService.createUser(authorizedUser.email, name as string, '', '', account.id); + + const accountId = await this.createAccountIfNotInvited(state as string, name as string); + + const createdUser = await userService.createUser(authorizedUser.email, name as string, '', '', accountId); if (!createdUser) { throw new NangoError('user_creation_failure'); } diff --git a/packages/webapp/src/pages/InviteSignup.tsx b/packages/webapp/src/pages/InviteSignup.tsx index f0182f8c8c..7a76cae24e 100644 --- a/packages/webapp/src/pages/InviteSignup.tsx +++ b/packages/webapp/src/pages/InviteSignup.tsx @@ -63,6 +63,29 @@ export default function InviteSignup() { } }; + const googleLogin = async () => { + const body = { + accountId: invitedAccountID, + token + }; + const res = await fetch('/api/v1/social/signup?provider=GoogleOAuth', { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json' + } + }); + + if (res?.status === 200) { + const data = await res.json(); + const { url } = data; + window.location = url; + } else if (res != null) { + const errorMessage = (await res.json()).error; + setServerErrorMessage(errorMessage); + } + }; + return ( <> @@ -135,6 +158,42 @@ export default function InviteSignup() { {serverErrorMessage &&

{serverErrorMessage}

}
+
+ +
From 68fec0b2f3518c139548adf7f2c7c78aafe955de Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 18 Mar 2024 20:54:20 +0200 Subject: [PATCH 03/16] [nan-578] refactor --- .../server/lib/controllers/auth.controller.ts | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index f2968838e8..d6b6afb2c4 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -27,6 +27,11 @@ export interface WebUser { name: string; } +interface InviteAccountState { + accountId: number; + token: string; +} + let workos: WorkOS | null = null; if (process.env['WORKOS_API_KEY']) { workos = new WorkOS(process.env['WORKOS_API_KEY']); @@ -291,28 +296,24 @@ class AuthController { } } - async createAccountIfNotInvited(state: string, name: string) { - const account: { accountId: number; token: string } | null = (state ? JSON.parse(Buffer.from(state, 'base64').toString('ascii')) : null) as { - accountId: number; - token: string; - } | null; - const accountId = typeof account?.accountId !== 'undefined' ? account.accountId : null; + async createAccountIfNotInvited(state: string, name: string): Promise { + const parsedAccount = (state ? JSON.parse(Buffer.from(state, 'base64').toString('ascii')) : null) as InviteAccountState | null; + const accountId = typeof parsedAccount?.accountId !== 'undefined' ? parsedAccount.accountId : null; - if (accountId === null) { - const account = await environmentService.createAccount(`${name}'s Organization`); - if (!account) { - throw new NangoError('account_creation_failure'); - } - return account.id; - } else { - const token = account?.token; + if (accountId !== null) { + const token = parsedAccount?.token; const validToken = await userService.getInvitedUserByToken(token as string); if (validToken) { await userService.markAcceptedInvite(token as string); } - } - return accountId; + return accountId; + } + const account = await environmentService.createAccount(`${name}'s Organization`); + if (!account) { + throw new NangoError('account_creation_failure'); + } + return account.id; } async socialLoginCallback(req: Request, res: Response, next: NextFunction) { @@ -342,6 +343,10 @@ class AuthController { const accountId = await this.createAccountIfNotInvited(state as string, name as string); + if (!accountId) { + throw new NangoError('account_creation_failure'); + } + const createdUser = await userService.createUser(authorizedUser.email, name as string, '', '', accountId); if (!createdUser) { throw new NangoError('user_creation_failure'); From 9bd4f073d5343e326d15584ecb447ac5c9981af8 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 19 Mar 2024 09:39:54 +0200 Subject: [PATCH 04/16] [nan-578] route to home page --- packages/server/lib/controllers/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index d6b6afb2c4..61cc1399cc 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -358,7 +358,7 @@ class AuthController { if (err) { return next(err); } - res.redirect(`${getBaseUrl()}/dev`); + res.redirect(`${getBaseUrl()}/`); }); } catch (err) { next(err); From 51538b26d8ce64ed08d7971b90720a35524ca55e Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 19 Mar 2024 10:08:10 +0200 Subject: [PATCH 05/16] [nan-578] env varible for redirect --- packages/server/lib/controllers/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 61cc1399cc..c135ae00fa 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -286,7 +286,7 @@ class AuthController { const oAuthUrl = workos?.userManagement.getAuthorizationUrl({ clientId: process.env['WORKOS_CLIENT_ID'] || '', provider, - redirectUri: `${getBaseUrl()}/api/v1/social/callback`, + redirectUri: process.env['GOOGLE_OAUTH_REDIRECT'] || `${getBaseUrl()}/api/v1/social/callback`, state: body ? Buffer.from(JSON.stringify(body)).toString('base64') : '' }); From 818204e47e268a56d1bec258b74291b47ad3d6e9 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 19 Mar 2024 10:14:10 +0200 Subject: [PATCH 06/16] [nan-578] remove env var --- packages/server/lib/controllers/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index c135ae00fa..61cc1399cc 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -286,7 +286,7 @@ class AuthController { const oAuthUrl = workos?.userManagement.getAuthorizationUrl({ clientId: process.env['WORKOS_CLIENT_ID'] || '', provider, - redirectUri: process.env['GOOGLE_OAUTH_REDIRECT'] || `${getBaseUrl()}/api/v1/social/callback`, + redirectUri: `${getBaseUrl()}/api/v1/social/callback`, state: body ? Buffer.from(JSON.stringify(body)).toString('base64') : '' }); From 386b6d0e2924c09bd694fcc1a0aa741566e9507b Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 19 Mar 2024 10:22:51 +0200 Subject: [PATCH 07/16] [nan-578] cross purpose hosted login routes --- packages/server/lib/controllers/auth.controller.ts | 6 +++--- packages/server/lib/server.ts | 4 ++-- packages/webapp/src/pages/InviteSignup.tsx | 2 +- packages/webapp/src/pages/Signup.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 61cc1399cc..d141262425 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -272,7 +272,7 @@ class AuthController { } } - async getSocialLogin(req: Request, res: Response, next: NextFunction) { + async getHostedLogin(req: Request, res: Response, next: NextFunction) { try { const provider = req.query['provider'] as string; @@ -286,7 +286,7 @@ class AuthController { const oAuthUrl = workos?.userManagement.getAuthorizationUrl({ clientId: process.env['WORKOS_CLIENT_ID'] || '', provider, - redirectUri: `${getBaseUrl()}/api/v1/social/callback`, + redirectUri: `${getBaseUrl()}/api/v1/login/callback`, state: body ? Buffer.from(JSON.stringify(body)).toString('base64') : '' }); @@ -316,7 +316,7 @@ class AuthController { return account.id; } - async socialLoginCallback(req: Request, res: Response, next: NextFunction) { + async loginCallback(req: Request, res: Response, next: NextFunction) { try { const { code, state } = req.query; diff --git a/packages/server/lib/server.ts b/packages/server/lib/server.ts index 392c3b9350..a045eb0d7d 100644 --- a/packages/server/lib/server.ts +++ b/packages/server/lib/server.ts @@ -149,8 +149,8 @@ if (AUTH_ENABLED) { app.route('/api/v1/signin').post(rateLimiterMiddleware, passport.authenticate('local'), authController.signin.bind(authController)); app.route('/api/v1/forgot-password').put(rateLimiterMiddleware, authController.forgotPassword.bind(authController)); app.route('/api/v1/reset-password').put(rateLimiterMiddleware, authController.resetPassword.bind(authController)); - app.route('/api/v1/social/signup').post(rateLimiterMiddleware, authController.getSocialLogin.bind(authController)); - app.route('/api/v1/social/callback').get(rateLimiterMiddleware, authController.socialLoginCallback.bind(authController)); + app.route('/api/v1/hosted/signup').post(rateLimiterMiddleware, authController.getHostedLogin.bind(authController)); + app.route('/api/v1/login/callback').get(rateLimiterMiddleware, authController.loginCallback.bind(authController)); } // Webapp routes (session auth). diff --git a/packages/webapp/src/pages/InviteSignup.tsx b/packages/webapp/src/pages/InviteSignup.tsx index 7a76cae24e..1f292d3ded 100644 --- a/packages/webapp/src/pages/InviteSignup.tsx +++ b/packages/webapp/src/pages/InviteSignup.tsx @@ -68,7 +68,7 @@ export default function InviteSignup() { accountId: invitedAccountID, token }; - const res = await fetch('/api/v1/social/signup?provider=GoogleOAuth', { + const res = await fetch('/api/v1/hosted/signup?provider=GoogleOAuth', { method: 'POST', body: JSON.stringify(body), headers: { diff --git a/packages/webapp/src/pages/Signup.tsx b/packages/webapp/src/pages/Signup.tsx index fe60149ddc..1b8c2c4b3d 100644 --- a/packages/webapp/src/pages/Signup.tsx +++ b/packages/webapp/src/pages/Signup.tsx @@ -44,7 +44,7 @@ export default function Signup() { const googleLogin = async () => { analyticsTrack('web:account_signup_google'); - const res = await fetch('/api/v1/social/signup?provider=GoogleOAuth', { + const res = await fetch('/api/v1/hosted/signup?provider=GoogleOAuth', { method: 'POST', headers: { 'Content-Type': 'application/json' From fbd3c44f75ffb2e11a28c4702feea8d7d4e534c9 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 19 Mar 2024 10:30:59 +0200 Subject: [PATCH 08/16] [nan-578] fix base url --- packages/server/lib/controllers/auth.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index d141262425..0fb2cb0726 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -16,6 +16,7 @@ import { AnalyticsTypes, isCloud, getBaseUrl, + getBasePublicUrl, NangoError, createOnboardingProvider } from '@nangohq/shared'; @@ -358,7 +359,7 @@ class AuthController { if (err) { return next(err); } - res.redirect(`${getBaseUrl()}/`); + res.redirect(`${getBasePublicUrl()}/`); }); } catch (err) { next(err); From 93ac937d8f5443e962fed30b1ac057baf592e76a Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 19 Mar 2024 14:56:13 +0200 Subject: [PATCH 09/16] [nan-578] add social buttons --- .../src/components/ui/button/Auth/Google.tsx | 82 +++++++++++ packages/webapp/src/index.css | 2 +- packages/webapp/src/layout/DefaultLayout.tsx | 10 +- packages/webapp/src/pages/ForgotPassword.tsx | 16 +-- packages/webapp/src/pages/InviteSignup.tsx | 132 +++++------------- packages/webapp/src/pages/ResetPassword.tsx | 16 +-- packages/webapp/src/pages/Signin.tsx | 82 +++++------ packages/webapp/src/pages/Signup.tsx | 126 +++++------------ packages/webapp/tailwind.config.js | 11 +- 9 files changed, 219 insertions(+), 258 deletions(-) create mode 100644 packages/webapp/src/components/ui/button/Auth/Google.tsx diff --git a/packages/webapp/src/components/ui/button/Auth/Google.tsx b/packages/webapp/src/components/ui/button/Auth/Google.tsx new file mode 100644 index 0000000000..2d4d89f418 --- /dev/null +++ b/packages/webapp/src/components/ui/button/Auth/Google.tsx @@ -0,0 +1,82 @@ +interface Props { + text: string; + setServerErrorMessage: (message: string) => void; + invitedAccountID?: number; + token?: string; +} + +interface PostBody { + method: string; + headers: { + 'Content-Type': string; + }; + body?: string; +} + +export default function GoogleButton({ text, setServerErrorMessage, invitedAccountID, token }: Props) { + const googleLogin = async () => { + const postBody: PostBody = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }; + + if (invitedAccountID && token) { + postBody.body = JSON.stringify({ + accountId: invitedAccountID, + token + }); + } + const res = await fetch('/api/v1/hosted/signup?provider=GoogleOAuth', postBody); + + if (res?.status === 200) { + const data = await res.json(); + const { url } = data; + window.location = url; + } else if (res != null) { + const errorMessage = (await res.json()).error; + setServerErrorMessage(errorMessage); + } + }; + return ( + + ); +} diff --git a/packages/webapp/src/index.css b/packages/webapp/src/index.css index f13a54f3b5..214884e0d1 100644 --- a/packages/webapp/src/index.css +++ b/packages/webapp/src/index.css @@ -3,7 +3,7 @@ body, #root { width: 100%; height: 100%; - background-color: #0e1014; + background-color: #010101; } @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@300;400;500;600;700&display=swap'); diff --git a/packages/webapp/src/layout/DefaultLayout.tsx b/packages/webapp/src/layout/DefaultLayout.tsx index 3e96f71774..da2bf7cc68 100644 --- a/packages/webapp/src/layout/DefaultLayout.tsx +++ b/packages/webapp/src/layout/DefaultLayout.tsx @@ -4,11 +4,13 @@ interface DefaultLayoutI { export default function DefaultLayout({ children }: DefaultLayoutI) { return ( -
-
- Your Company +
+
+
+ Nango +
+ {children}
- {children}
); } diff --git a/packages/webapp/src/pages/ForgotPassword.tsx b/packages/webapp/src/pages/ForgotPassword.tsx index ef20f724d5..168e8363ee 100644 --- a/packages/webapp/src/pages/ForgotPassword.tsx +++ b/packages/webapp/src/pages/ForgotPassword.tsx @@ -30,23 +30,21 @@ export default function Signin() { return ( <> -
-
-

Request Password Reset

+
+
+

Request password reset

-
@@ -55,9 +53,9 @@ export default function Signin() {
{serverErrorMessage &&

{serverErrorMessage}

}
diff --git a/packages/webapp/src/pages/InviteSignup.tsx b/packages/webapp/src/pages/InviteSignup.tsx index 1f292d3ded..45af91e4a2 100644 --- a/packages/webapp/src/pages/InviteSignup.tsx +++ b/packages/webapp/src/pages/InviteSignup.tsx @@ -1,10 +1,11 @@ import { useState, useEffect } from 'react'; -import { Link, useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useInviteSignupAPI, useSignupAPI } from '../utils/api'; import { isEnterprise } from '../utils/utils'; import { useSignin, User } from '../utils/user'; import DefaultLayout from '../layout/DefaultLayout'; +import GoogleButton from '../components/ui/button/Auth/Google'; export default function InviteSignup() { const [serverErrorMessage, setServerErrorMessage] = useState(''); @@ -63,40 +64,14 @@ export default function InviteSignup() { } }; - const googleLogin = async () => { - const body = { - accountId: invitedAccountID, - token - }; - const res = await fetch('/api/v1/hosted/signup?provider=GoogleOAuth', { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'Content-Type': 'application/json' - } - }); - - if (res?.status === 200) { - const data = await res.json(); - const { url } = data; - window.location = url; - } else if (res != null) { - const errorMessage = (await res.json()).error; - setServerErrorMessage(errorMessage); - } - }; - return ( <> -
-
+
+

Sign up

-
-
-
@@ -151,74 +123,38 @@ export default function InviteSignup() {
{serverErrorMessage &&

{serverErrorMessage}

}
-
- -
-
-
-
-

Already have an account?

- - Sign in - +
+
+ or continue with +
+
-
-
-

By signing up, you agree to our

- - Terms of Service - -

and

- - Privacy Policy - -

.

+
+
+

+ By signing in, you agree to our + + Terms of Service + + and + + Privacy Policy + + . +

diff --git a/packages/webapp/src/pages/ResetPassword.tsx b/packages/webapp/src/pages/ResetPassword.tsx index 769369d38b..9a2b54cfa1 100644 --- a/packages/webapp/src/pages/ResetPassword.tsx +++ b/packages/webapp/src/pages/ResetPassword.tsx @@ -39,23 +39,17 @@ export default function Signin() { return ( <> -
-
-

Reset Password

+
+
+

Reset password

-
-
- -
-
diff --git a/packages/webapp/src/pages/Signin.tsx b/packages/webapp/src/pages/Signin.tsx index 3b21ac2047..46ab5f69d6 100644 --- a/packages/webapp/src/pages/Signin.tsx +++ b/packages/webapp/src/pages/Signin.tsx @@ -4,6 +4,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { useSigninAPI } from '../utils/api'; import { useSignin, User } from '../utils/user'; import DefaultLayout from '../layout/DefaultLayout'; +import GoogleButton from '../components/ui/button/Auth/Google'; export default function Signin() { const [serverErrorMessage, setServerErrorMessage] = useState(''); @@ -35,47 +36,41 @@ export default function Signin() { return ( <> -
-
-

Sign in

- +
+
+

Log in to Nango

+
-
-
-
- -
-
- + -
+
@@ -83,38 +78,43 @@ export default function Signin() {
{serverErrorMessage &&

{serverErrorMessage}

}
+ +
+
+ or continue with +
+
+ +
-
-
-

Need an account?

- - Sign up +
+
+

Don't have an account?

+ + Sign up.
-
-
-

By signing up, you agree to our

- - Terms of Service - -

and

- - Privacy Policy - -

.

+
+
+

+ By signing in, you agree to our + + Terms of Service + + and + + Privacy Policy + + . +

diff --git a/packages/webapp/src/pages/Signup.tsx b/packages/webapp/src/pages/Signup.tsx index 1b8c2c4b3d..fa96e651c8 100644 --- a/packages/webapp/src/pages/Signup.tsx +++ b/packages/webapp/src/pages/Signup.tsx @@ -5,6 +5,7 @@ import { useAnalyticsTrack } from '../utils/analytics'; import { useSignupAPI } from '../utils/api'; import { useSignin, User } from '../utils/user'; import DefaultLayout from '../layout/DefaultLayout'; +import GoogleButton from '../components/ui/button/Auth/Google'; export default function Signup() { const [serverErrorMessage, setServerErrorMessage] = useState(''); @@ -42,36 +43,14 @@ export default function Signup() { } }; - const googleLogin = async () => { - analyticsTrack('web:account_signup_google'); - const res = await fetch('/api/v1/hosted/signup?provider=GoogleOAuth', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (res?.status === 200) { - const data = await res.json(); - const { url } = data; - window.location = url; - } else if (res != null) { - const errorMessage = (await res.json()).error; - setServerErrorMessage(errorMessage); - } - }; - return ( <> -
-
-

Sign up

+
+
+

Sign up to Nango

-
-
-
@@ -123,75 +99,41 @@ export default function Signup() {
{serverErrorMessage &&

{serverErrorMessage}

}
-

or sign up with

-
- +
+
+ or continue with +
+
-
-
-

Already have an account?

- - Sign in +
+
+

Already have an account?

+ + Sign in.
-
-
-

By signing up, you agree to our

- - Terms of Service - -

and

- - Privacy Policy - -

.

+
+
+

+ By signing in, you agree to our + + Terms of Service + + and + + Privacy Policy + + . +

diff --git a/packages/webapp/tailwind.config.js b/packages/webapp/tailwind.config.js index 32f6b2e7de..0573c44dab 100644 --- a/packages/webapp/tailwind.config.js +++ b/packages/webapp/tailwind.config.js @@ -10,18 +10,25 @@ module.exports = { colors: { 'bg-black': '#0E1014', 'pure-black': '#05050a', - 'bg-dark-gray': '#181B20', 'active-gray': '#161720', 'hover-gray': '#1D1F28', 'text-light-gray': '#A9A9A9', 'off-black': '#05050a', - 'text-dark-gray': '#5F5F5F', 'bg-cta-green': '#75E270', 'text-cta-green1': '#224421', 'border-gray': '#333333', 'border-blue': '#1489DF', 'text-blue': '#1489DF', 'text-light-blue': '#76C5FF', + 'dark-0': '#FFFFFF', + 'dark-100': '#F4F4F5', + 'dark-200': '#E4E4E7', + 'dark-300': '#D4D4D8', + 'dark-400': '#A1A1AA', + 'dark-500': '#71717A', + 'dark-600': '#27272A', + 'dark-700': '#18181B', + 'dark-800': '#09090B', 'bg-dark-blue': '#182633', white: '#FFFFFF' }, From c010b0b761c87bf583308acd4d1e6908274ba270 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 19 Mar 2024 19:56:43 +0200 Subject: [PATCH 10/16] [nan-578] feedback updates --- .env.example | 4 ++ .../server/lib/controllers/auth.controller.ts | 44 +++++++++---------- packages/webapp/src/pages/ResetPassword.tsx | 2 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 0d0e8def04..6853af187d 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,7 @@ NANGO_TELEMETRY_SDK=false # Getting Started configuration DEFAULT_GITHUB_CLIENT_ID= DEFAULT_GITHUB_CLIENT_SECRET= + +# Hosted Auth Configuration +WORKOS_API_KEY= +WORKOS_CLIENT_ID= diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 0fb2cb0726..d75884c6fd 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -38,6 +38,26 @@ if (process.env['WORKOS_API_KEY']) { workos = new WorkOS(process.env['WORKOS_API_KEY']); } +const createAccountIfNotInvited = async (state: string, name: string): Promise => { + const parsedAccount = (state ? JSON.parse(Buffer.from(state, 'base64').toString('ascii')) : null) as InviteAccountState | null; + const accountId = typeof parsedAccount?.accountId !== 'undefined' ? parsedAccount.accountId : null; + + if (accountId !== null) { + const token = parsedAccount?.token; + const validToken = await userService.getInvitedUserByToken(token as string); + if (validToken) { + await userService.markAcceptedInvite(token as string); + } + + return accountId; + } + const account = await environmentService.createAccount(`${name}'s Organization`); + if (!account) { + throw new NangoError('account_creation_failure'); + } + return account.id; +}; + class AuthController { async signin(req: Request, res: Response, next: NextFunction) { try { @@ -273,7 +293,7 @@ class AuthController { } } - async getHostedLogin(req: Request, res: Response, next: NextFunction) { + getHostedLogin(req: Request, res: Response, next: NextFunction) { try { const provider = req.query['provider'] as string; @@ -297,26 +317,6 @@ class AuthController { } } - async createAccountIfNotInvited(state: string, name: string): Promise { - const parsedAccount = (state ? JSON.parse(Buffer.from(state, 'base64').toString('ascii')) : null) as InviteAccountState | null; - const accountId = typeof parsedAccount?.accountId !== 'undefined' ? parsedAccount.accountId : null; - - if (accountId !== null) { - const token = parsedAccount?.token; - const validToken = await userService.getInvitedUserByToken(token as string); - if (validToken) { - await userService.markAcceptedInvite(token as string); - } - - return accountId; - } - const account = await environmentService.createAccount(`${name}'s Organization`); - if (!account) { - throw new NangoError('account_creation_failure'); - } - return account.id; - } - async loginCallback(req: Request, res: Response, next: NextFunction) { try { const { code, state } = req.query; @@ -342,7 +342,7 @@ class AuthController { ? `${authorizedUser.firstName || ''} ${authorizedUser.lastName || ''}` : authorizedUser.email.split('@')[0]; - const accountId = await this.createAccountIfNotInvited(state as string, name as string); + const accountId = await createAccountIfNotInvited(state as string, name as string); if (!accountId) { throw new NangoError('account_creation_failure'); diff --git a/packages/webapp/src/pages/ResetPassword.tsx b/packages/webapp/src/pages/ResetPassword.tsx index 9a2b54cfa1..844e979dc9 100644 --- a/packages/webapp/src/pages/ResetPassword.tsx +++ b/packages/webapp/src/pages/ResetPassword.tsx @@ -50,7 +50,7 @@ export default function Signin() { name="password" type="password" placeholder="Password" - autoComplete="current-password" + autoComplete="new-password" required className="border-border-gray bg-bg-black text-text-light-gray block h-11 w-full appearance-none rounded-md border px-3 py-2 text-base placeholder-gray-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500" /> From 6d7cf1afb499c3b65a7f250275e75e5d563e9010 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 20 Mar 2024 08:47:24 +0200 Subject: [PATCH 11/16] [nan-578] only show for cloud --- packages/webapp/src/pages/InviteSignup.tsx | 28 ++++++++++++---------- packages/webapp/src/pages/Signin.tsx | 17 ++++++++----- packages/webapp/src/pages/Signup.tsx | 17 ++++++++----- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/packages/webapp/src/pages/InviteSignup.tsx b/packages/webapp/src/pages/InviteSignup.tsx index 45af91e4a2..300035dbd0 100644 --- a/packages/webapp/src/pages/InviteSignup.tsx +++ b/packages/webapp/src/pages/InviteSignup.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useInviteSignupAPI, useSignupAPI } from '../utils/api'; -import { isEnterprise } from '../utils/utils'; +import { isCloud, isEnterprise } from '../utils/utils'; import { useSignin, User } from '../utils/user'; import DefaultLayout from '../layout/DefaultLayout'; import GoogleButton from '../components/ui/button/Auth/Google'; @@ -130,17 +130,21 @@ export default function InviteSignup() { {serverErrorMessage &&

{serverErrorMessage}

}
-
-
- or continue with -
-
- + {isCloud() && ( + <> +
+
+ or continue with +
+
+ + + )}
diff --git a/packages/webapp/src/pages/Signin.tsx b/packages/webapp/src/pages/Signin.tsx index 46ab5f69d6..fad771d764 100644 --- a/packages/webapp/src/pages/Signin.tsx +++ b/packages/webapp/src/pages/Signin.tsx @@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { useSigninAPI } from '../utils/api'; import { useSignin, User } from '../utils/user'; +import { isCloud } from '../utils/utils'; import DefaultLayout from '../layout/DefaultLayout'; import GoogleButton from '../components/ui/button/Auth/Google'; @@ -85,13 +86,17 @@ export default function Signin() { {serverErrorMessage &&

{serverErrorMessage}

}
-
-
- or continue with -
-
+ {isCloud() && ( + <> +
+
+ or continue with +
+
- + + + )}
diff --git a/packages/webapp/src/pages/Signup.tsx b/packages/webapp/src/pages/Signup.tsx index fa96e651c8..180a2da9b0 100644 --- a/packages/webapp/src/pages/Signup.tsx +++ b/packages/webapp/src/pages/Signup.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAnalyticsTrack } from '../utils/analytics'; +import { isCloud } from '../utils/utils'; import { useSignupAPI } from '../utils/api'; import { useSignin, User } from '../utils/user'; import DefaultLayout from '../layout/DefaultLayout'; @@ -106,12 +107,16 @@ export default function Signup() { {serverErrorMessage &&

{serverErrorMessage}

}
-
-
- or continue with -
-
- + {isCloud() && ( + <> +
+
+ or continue with +
+
+ + + )}
From a4606041801e87414eb9a6ea36bfbc9ecdcc9ace Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 20 Mar 2024 09:15:55 +0200 Subject: [PATCH 12/16] [nan-578] handle organization being returned --- .../server/lib/controllers/auth.controller.ts | 48 +++++++++++++------ .../shared/lib/services/account.service.ts | 16 +++++++ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index d75884c6fd..61e1a9000f 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -326,33 +326,53 @@ class AuthController { return; } - const { user: authorizedUser } = await workos.userManagement.authenticateWithCode({ + const { user: authorizedUser, organizationId } = await workos.userManagement.authenticateWithCode({ clientId: process.env['WORKOS_CLIENT_ID'] || '', code: code as string }); - let user: User | null = null; const existingUser = await userService.getUserByEmail(authorizedUser.email); if (existingUser) { - user = existingUser; - } else { - const name = - authorizedUser.firstName || authorizedUser.lastName - ? `${authorizedUser.firstName || ''} ${authorizedUser.lastName || ''}` - : authorizedUser.email.split('@')[0]; + req.login(existingUser, function (err) { + if (err) { + return next(err); + } + res.redirect(`${getBasePublicUrl()}/`); + }); + + return; + } - const accountId = await createAccountIfNotInvited(state as string, name as string); + const name = + authorizedUser.firstName || authorizedUser.lastName + ? `${authorizedUser.firstName || ''} ${authorizedUser.lastName || ''}` + : authorizedUser.email.split('@')[0]; - if (!accountId) { + let accountId: number | null = null; + + if (organizationId) { + // in this case we have a pre registered organization with workos + // let's make sure it exists in our system + const organization = await workos.organizations.getOrganization(organizationId); + + const account = await accountService.getOrCreateAccount(organization.name); + + if (!account) { throw new NangoError('account_creation_failure'); } + accountId = account.id; + } else { + accountId = await createAccountIfNotInvited(state as string, name as string); - const createdUser = await userService.createUser(authorizedUser.email, name as string, '', '', accountId); - if (!createdUser) { - throw new NangoError('user_creation_failure'); + if (!accountId) { + throw new NangoError('account_creation_failure'); } - user = createdUser; + } + + const user = await userService.createUser(authorizedUser.email, name as string, '', '', accountId); + if (!user) { + throw new NangoError('user_creation_failure'); } req.login(user, function (err) { diff --git a/packages/shared/lib/services/account.service.ts b/packages/shared/lib/services/account.service.ts index 2b8ee4cfc2..756e7f3c65 100644 --- a/packages/shared/lib/services/account.service.ts +++ b/packages/shared/lib/services/account.service.ts @@ -67,6 +67,22 @@ class AccountService { return account[0].uuid; } + + async getOrCreateAccount(name: string): Promise { + const account: Account[] = await db.knex.select('id').from(`_nango_accounts`).where({ name }); + + if (account == null || account.length == 0 || !account[0]) { + const newAccount: Account[] = await db.knex.insert({ name, created_at: new Date() }).into(`_nango_accounts`).returning('*'); + + if (!newAccount || newAccount.length == 0 || !newAccount[0]) { + throw new Error('Failed to create account'); + } + + return newAccount[0]; + } + + return account[0]; + } } export default new AccountService(); From 0c5f6d5ce2256abf6d4c95954467f466fb8dc7c7 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 20 Mar 2024 11:44:12 +0200 Subject: [PATCH 13/16] [nan-578] feedback and organization handling --- .../lib/controllers/account.controller.ts | 3 +- .../server/lib/controllers/auth.controller.ts | 114 +++++++++++++++--- packages/server/lib/server.ts | 8 +- .../shared/lib/services/account.service.ts | 18 +++ .../lib/services/environment.service.ts | 18 +-- packages/shared/lib/utils/error.ts | 20 +++ packages/shared/lib/utils/utils.ts | 3 + .../src/components/ui/button/Auth/Google.tsx | 9 +- packages/webapp/src/pages/InviteSignup.tsx | 4 +- packages/webapp/src/pages/Signin.tsx | 4 +- packages/webapp/src/pages/Signup.tsx | 4 +- packages/webapp/src/utils/utils.tsx | 1 + 12 files changed, 162 insertions(+), 44 deletions(-) diff --git a/packages/server/lib/controllers/account.controller.ts b/packages/server/lib/controllers/account.controller.ts index f61bfee48a..28b8407bfe 100644 --- a/packages/server/lib/controllers/account.controller.ts +++ b/packages/server/lib/controllers/account.controller.ts @@ -1,8 +1,7 @@ import type { Request, Response, NextFunction } from 'express'; -import { accountService, userService, errorManager, LogLevel, LogActionEnum, createActivityLogAndLogMessage, isEnterprise, isCloud } from '@nangohq/shared'; +import { accountService, userService, errorManager, LogLevel, LogActionEnum, createActivityLogAndLogMessage, isCloud } from '@nangohq/shared'; import { getUserAccountAndEnvironmentFromSession } from '../utils/utils.js'; -export const AUTH_ENABLED = isCloud() || isEnterprise(); export const NANGO_ADMIN_UUID = process.env['NANGO_ADMIN_UUID']; export const AUTH_ADMIN_SWITCH_ENABLED = NANGO_ADMIN_UUID && isCloud(); export const AUTH_ADMIN_SWITCH_MS = 600 * 1000; diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 61e1a9000f..b69816fd39 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -10,6 +10,10 @@ import { userService, accountService, errorManager, + Result, + isOk, + resultOk, + resultErr, ErrorSourceEnum, environmentService, analytics, @@ -28,8 +32,10 @@ export interface WebUser { name: string; } -interface InviteAccountState { +interface InviteAccountBody { accountId: number; +} +interface InviteAccountState extends InviteAccountBody { token: string; } @@ -38,24 +44,39 @@ if (process.env['WORKOS_API_KEY']) { workos = new WorkOS(process.env['WORKOS_API_KEY']); } -const createAccountIfNotInvited = async (state: string, name: string): Promise => { - const parsedAccount = (state ? JSON.parse(Buffer.from(state, 'base64').toString('ascii')) : null) as InviteAccountState | null; - const accountId = typeof parsedAccount?.accountId !== 'undefined' ? parsedAccount.accountId : null; +const allowedProviders = ['GoogleOAuth']; - if (accountId !== null) { - const token = parsedAccount?.token; - const validToken = await userService.getInvitedUserByToken(token as string); - if (validToken) { - await userService.markAcceptedInvite(token as string); +const parseState = (state: string): Result => { + try { + const parsed = JSON.parse(Buffer.from(state, 'base64').toString('ascii')) as InviteAccountState; + return resultOk(parsed); + } catch { + const error = new Error('Invalid state'); + return resultErr(error); + } +}; + +const createAccountIfNotInvited = async (name: string, state?: string): Promise => { + if (!state) { + const account = await environmentService.createAccount(`${name}'s Organization`); + if (!account) { + throw new NangoError('account_creation_failure'); } + return account.id; + } + + const parsedState: Result = parseState(state); + if (isOk(parsedState)) { + const { accountId, token } = parsedState.res; + const validToken = await userService.getInvitedUserByToken(token); + if (validToken) { + await userService.markAcceptedInvite(token); + } return accountId; } - const account = await environmentService.createAccount(`${name}'s Organization`); - if (!account) { - throw new NangoError('account_creation_failure'); - } - return account.id; + + return null; }; class AuthController { @@ -297,18 +318,68 @@ class AuthController { try { const provider = req.query['provider'] as string; + if (!provider || !allowedProviders.includes(provider)) { + errorManager.errRes(res, 'invalid_provider'); + return; + } + if (!workos) { errorManager.errRes(res, 'workos_not_configured'); return; } - const body = req.body; + const oAuthUrl = workos?.userManagement.getAuthorizationUrl({ + clientId: process.env['WORKOS_CLIENT_ID'] || '', + provider, + redirectUri: `${getBaseUrl()}/api/v1/login/callback` + }); + + res.send({ url: oAuthUrl }); + } catch (err) { + next(err); + } + } + + getHostedLoginWithInvite(req: Request, res: Response, next: NextFunction) { + try { + const provider = req.query['provider'] as string; + + if (!provider || !allowedProviders.includes(provider)) { + errorManager.errRes(res, 'invalid_provider'); + return; + } + + const token = req.params['token'] as string; + + const body: InviteAccountBody = req.body as InviteAccountBody; + + if (!body || body.accountId !== undefined) { + errorManager.errRes(res, 'missing_params'); + return; + } + + if (!provider || !token) { + errorManager.errRes(res, 'missing_params'); + return; + } + + if (!workos) { + errorManager.errRes(res, 'workos_not_configured'); + return; + } + + const accountId = body.accountId; + + const inviteParams: InviteAccountState = { + accountId, + token + }; const oAuthUrl = workos?.userManagement.getAuthorizationUrl({ clientId: process.env['WORKOS_CLIENT_ID'] || '', provider, redirectUri: `${getBaseUrl()}/api/v1/login/callback`, - state: body ? Buffer.from(JSON.stringify(body)).toString('base64') : '' + state: JSON.stringify(inviteParams) }); res.send({ url: oAuthUrl }); @@ -326,6 +397,11 @@ class AuthController { return; } + if (!code) { + errorManager.errRes(res, 'missing_hosted_login_callback_code'); + return; + } + const { user: authorizedUser, organizationId } = await workos.userManagement.authenticateWithCode({ clientId: process.env['WORKOS_CLIENT_ID'] || '', code: code as string @@ -363,7 +439,11 @@ class AuthController { } accountId = account.id; } else { - accountId = await createAccountIfNotInvited(state as string, name as string); + if (!name) { + throw new NangoError('missing_name_for_account_creation'); + } + + accountId = await createAccountIfNotInvited(name, state as string); if (!accountId) { throw new NangoError('account_creation_failure'); diff --git a/packages/server/lib/server.ts b/packages/server/lib/server.ts index a045eb0d7d..33b54a82f2 100644 --- a/packages/server/lib/server.ts +++ b/packages/server/lib/server.ts @@ -30,13 +30,15 @@ import { AuthClient } from './clients/auth.client.js'; import publisher from './clients/publisher.client.js'; import passport from 'passport'; import environmentController from './controllers/environment.controller.js'; -import accountController, { AUTH_ENABLED } from './controllers/account.controller.js'; +import accountController from './controllers/account.controller.js'; import type { Response, Request } from 'express'; import Logger from './utils/logger.js'; import { getGlobalOAuthCallbackUrl, environmentService, getPort, + AUTH_ENABLED, + HOSTED_AUTH_ENABLED, isCloud, isEnterprise, isBasicAuthEnabled, @@ -149,7 +151,11 @@ if (AUTH_ENABLED) { app.route('/api/v1/signin').post(rateLimiterMiddleware, passport.authenticate('local'), authController.signin.bind(authController)); app.route('/api/v1/forgot-password').put(rateLimiterMiddleware, authController.forgotPassword.bind(authController)); app.route('/api/v1/reset-password').put(rateLimiterMiddleware, authController.resetPassword.bind(authController)); +} + +if (HOSTED_AUTH_ENABLED) { app.route('/api/v1/hosted/signup').post(rateLimiterMiddleware, authController.getHostedLogin.bind(authController)); + app.route('/api/v1/hosted/signup/:token').post(rateLimiterMiddleware, authController.getHostedLoginWithInvite.bind(authController)); app.route('/api/v1/login/callback').get(rateLimiterMiddleware, authController.loginCallback.bind(authController)); } diff --git a/packages/shared/lib/services/account.service.ts b/packages/shared/lib/services/account.service.ts index 756e7f3c65..7bbac74452 100644 --- a/packages/shared/lib/services/account.service.ts +++ b/packages/shared/lib/services/account.service.ts @@ -2,6 +2,7 @@ import db from '../db/database.js'; import type { Account } from '../models/Admin'; import type { Environment } from '../models/Environment'; import { LogActionEnum } from '../models/Activity.js'; +import environmentService from './environment.service.js'; import errorManager, { ErrorSourceEnum } from '../utils/error.manager.js'; class AccountService { @@ -77,12 +78,29 @@ class AccountService { if (!newAccount || newAccount.length == 0 || !newAccount[0]) { throw new Error('Failed to create account'); } + await environmentService.createDefaultEnvironments(newAccount[0]['id']); return newAccount[0]; } return account[0]; } + + /** + * Create Account + * @desc create a new account and assign to the default environmenets + */ + async createAccount(name: string): Promise { + const result: void | Pick = await db.knex.from(`_nango_accounts`).insert({ name: name }, ['id']); + + if (Array.isArray(result) && result.length === 1 && result[0] != null && 'id' in result[0]) { + await environmentService.createDefaultEnvironments(result[0]['id']); + + return result[0]; + } + + return null; + } } export default new AccountService(); diff --git a/packages/shared/lib/services/environment.service.ts b/packages/shared/lib/services/environment.service.ts index ffea46a797..116830a013 100644 --- a/packages/shared/lib/services/environment.service.ts +++ b/packages/shared/lib/services/environment.service.ts @@ -295,22 +295,10 @@ class EnvironmentService { return null; } - /** - * Create Account - * @desc create a new account and assign to the default environmenets - */ - async createAccount(name: string): Promise { - const result: void | Pick = await db.knex.from(`_nango_accounts`).insert({ name: name }, ['id']); - - if (Array.isArray(result) && result.length === 1 && result[0] != null && 'id' in result[0]) { - for (const defaultEnvironment of defaultEnvironments) { - await this.createEnvironment(result[0]['id'], defaultEnvironment); - } - - return result[0]; + async createDefaultEnvironments(accountId: number): Promise { + for (const environment of defaultEnvironments) { + await this.createEnvironment(accountId, environment); } - - return null; } async getEnvironmentName(id: number): Promise { diff --git a/packages/shared/lib/utils/error.ts b/packages/shared/lib/utils/error.ts index a17f5e4962..42d4b63386 100644 --- a/packages/shared/lib/utils/error.ts +++ b/packages/shared/lib/utils/error.ts @@ -532,6 +532,26 @@ export class NangoError extends Error { this.message = `The parameter ${this.payload['incorrect']} is invalid. Did you mean ${this.payload['correct']}?`; break; + case 'invalid_provider': + this.status = 400; + this.message = `The provider is not allowed. Please try again with a valid provider`; + break; + + case 'workos_not_configured': + this.status = 400; + this.message = `WorkOS is not configured. Please reach out to support to obtain valid WorkOS credentials.`; + break; + + case 'missing_hosted_login_callback_code': + this.status = 400; + this.message = `Missing param 'code' for the hosted login callback.`; + break; + + case 'missing_name_for_account_creation': + this.status = 400; + this.message = `Missing an account name for account login/signup.`; + break; + default: this.status = 500; this.type = 'unhandled_' + type; diff --git a/packages/shared/lib/utils/utils.ts b/packages/shared/lib/utils/utils.ts index a543fbcb91..b581362bec 100644 --- a/packages/shared/lib/utils/utils.ts +++ b/packages/shared/lib/utils/utils.ts @@ -34,6 +34,9 @@ export enum NodeEnv { Prod = 'production' } +export const AUTH_ENABLED = isCloud() || isEnterprise(); +export const HOSTED_AUTH_ENABLED = isCloud() || isLocal(); + export const JAVASCRIPT_AND_TYPESCRIPT_TYPES = { primitives: ['string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'null'], aliases: ['String', 'Number', 'Boolean', 'BigInt', 'Symbol', 'Undefined', 'Null', 'bool', 'char', 'integer', 'int', 'date', 'object'], diff --git a/packages/webapp/src/components/ui/button/Auth/Google.tsx b/packages/webapp/src/components/ui/button/Auth/Google.tsx index 2d4d89f418..c0792d6718 100644 --- a/packages/webapp/src/components/ui/button/Auth/Google.tsx +++ b/packages/webapp/src/components/ui/button/Auth/Google.tsx @@ -24,11 +24,13 @@ export default function GoogleButton({ text, setServerErrorMessage, invitedAccou if (invitedAccountID && token) { postBody.body = JSON.stringify({ - accountId: invitedAccountID, - token + accountId: invitedAccountID }); } - const res = await fetch('/api/v1/hosted/signup?provider=GoogleOAuth', postBody); + console.log(invitedAccountID, token); + const endpoint = token ? `/api/v1/hosted/signup/${token}?provider=GoogleOAuth` : '/api/v1/hosted/signup?provider=GoogleOAuth'; + + const res = await fetch(endpoint, postBody); if (res?.status === 200) { const data = await res.json(); @@ -42,6 +44,7 @@ export default function GoogleButton({ text, setServerErrorMessage, invitedAccou return (
- {isCloud() && ( + {HOSTED_AUTH_ENABLED && ( <>
diff --git a/packages/webapp/src/pages/Signin.tsx b/packages/webapp/src/pages/Signin.tsx index fad771d764..6c4e33a242 100644 --- a/packages/webapp/src/pages/Signin.tsx +++ b/packages/webapp/src/pages/Signin.tsx @@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { useSigninAPI } from '../utils/api'; import { useSignin, User } from '../utils/user'; -import { isCloud } from '../utils/utils'; +import { HOSTED_AUTH_ENABLED } from '../utils/utils'; import DefaultLayout from '../layout/DefaultLayout'; import GoogleButton from '../components/ui/button/Auth/Google'; @@ -86,7 +86,7 @@ export default function Signin() { {serverErrorMessage &&

{serverErrorMessage}

}
- {isCloud() && ( + {HOSTED_AUTH_ENABLED && ( <>
diff --git a/packages/webapp/src/pages/Signup.tsx b/packages/webapp/src/pages/Signup.tsx index 180a2da9b0..9f87f10556 100644 --- a/packages/webapp/src/pages/Signup.tsx +++ b/packages/webapp/src/pages/Signup.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAnalyticsTrack } from '../utils/analytics'; -import { isCloud } from '../utils/utils'; +import { HOSTED_AUTH_ENABLED } from '../utils/utils'; import { useSignupAPI } from '../utils/api'; import { useSignin, User } from '../utils/user'; import DefaultLayout from '../layout/DefaultLayout'; @@ -107,7 +107,7 @@ export default function Signup() { {serverErrorMessage &&

{serverErrorMessage}

}
- {isCloud() && ( + {HOSTED_AUTH_ENABLED && ( <>
diff --git a/packages/webapp/src/utils/utils.tsx b/packages/webapp/src/utils/utils.tsx index 3f6ba66833..873fc94646 100644 --- a/packages/webapp/src/utils/utils.tsx +++ b/packages/webapp/src/utils/utils.tsx @@ -10,6 +10,7 @@ export const prodUrl: string = 'https://api.nango.dev'; export const syncDocs = 'https://docs.nango.dev/integrate/guides/sync-data-from-an-api'; export const AUTH_ENABLED = isCloud() || isEnterprise() || isLocal(); +export const HOSTED_AUTH_ENABLED = isCloud() || isLocal(); export function isHosted() { return process.env.REACT_APP_ENV === 'hosted'; From f155434b111a1477e55d08139d7e70a14f4e2cd6 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 20 Mar 2024 11:54:21 +0200 Subject: [PATCH 14/16] [nan-578] fix type separation and service usage --- packages/server/lib/controllers/auth.controller.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 86faa32641..002e988309 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -5,12 +5,11 @@ import util from 'util'; import { resetPasswordSecret, getUserAccountAndEnvironmentFromSession } from '../utils/utils.js'; import jwt from 'jsonwebtoken'; import EmailClient from '../clients/email.client.js'; -import type { User } from '@nangohq/shared'; +import type { User, Result } from '@nangohq/shared'; import { userService, accountService, errorManager, - Result, isOk, resultOk, resultErr, @@ -58,7 +57,7 @@ const parseState = (state: string): Result => { const createAccountIfNotInvited = async (name: string, state?: string): Promise => { if (!state) { - const account = await environmentService.createAccount(`${name}'s Organization`); + const account = await accountService.createAccount(`${name}'s Organization`); if (!account) { throw new NangoError('account_creation_failure'); } @@ -158,7 +157,7 @@ class AuthController { account = await accountService.getAccountById(Number(req.body['account_id'])); joinedWithToken = true; } else { - account = await environmentService.createAccount(`${name}'s Organization`); + account = await accountService.createAccount(`${name}'s Organization`); } if (account == null) { From 9659dc44730aa0532c7c90156fc234ee6aaa781c Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 20 Mar 2024 12:54:29 +0200 Subject: [PATCH 15/16] [nan-578] naming update --- packages/server/lib/controllers/auth.controller.ts | 6 +++--- packages/server/lib/server.ts | 8 ++++---- packages/shared/lib/utils/error.ts | 4 ++-- packages/shared/lib/utils/utils.ts | 2 +- packages/webapp/src/components/ui/button/Auth/Google.tsx | 3 +-- packages/webapp/src/pages/InviteSignup.tsx | 7 ++++--- packages/webapp/src/pages/Signin.tsx | 7 ++++--- packages/webapp/src/pages/Signup.tsx | 7 ++++--- packages/webapp/src/utils/utils.tsx | 2 +- 9 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 002e988309..70868d2fc1 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -313,7 +313,7 @@ class AuthController { } } - getHostedLogin(req: Request, res: Response, next: NextFunction) { + getManagedLogin(req: Request, res: Response, next: NextFunction) { try { const provider = req.query['provider'] as string; @@ -339,7 +339,7 @@ class AuthController { } } - getHostedLoginWithInvite(req: Request, res: Response, next: NextFunction) { + getManagedLoginWithInvite(req: Request, res: Response, next: NextFunction) { try { const provider = req.query['provider'] as string; @@ -397,7 +397,7 @@ class AuthController { } if (!code) { - errorManager.errRes(res, 'missing_hosted_login_callback_code'); + errorManager.errRes(res, 'missing_managed_login_callback_code'); return; } diff --git a/packages/server/lib/server.ts b/packages/server/lib/server.ts index bef626ac52..43287c1110 100644 --- a/packages/server/lib/server.ts +++ b/packages/server/lib/server.ts @@ -39,7 +39,7 @@ import { environmentService, getPort, AUTH_ENABLED, - HOSTED_AUTH_ENABLED, + MANAGED_AUTH_ENABLED, isCloud, isEnterprise, isBasicAuthEnabled, @@ -154,9 +154,9 @@ if (AUTH_ENABLED) { app.route('/api/v1/reset-password').put(rateLimiterMiddleware, authController.resetPassword.bind(authController)); } -if (HOSTED_AUTH_ENABLED) { - app.route('/api/v1/hosted/signup').post(rateLimiterMiddleware, authController.getHostedLogin.bind(authController)); - app.route('/api/v1/hosted/signup/:token').post(rateLimiterMiddleware, authController.getHostedLoginWithInvite.bind(authController)); +if (MANAGED_AUTH_ENABLED) { + app.route('/api/v1/managed/signup').post(rateLimiterMiddleware, authController.getManagedLogin.bind(authController)); + app.route('/api/v1/managed/signup/:token').post(rateLimiterMiddleware, authController.getManagedLoginWithInvite.bind(authController)); app.route('/api/v1/login/callback').get(rateLimiterMiddleware, authController.loginCallback.bind(authController)); } diff --git a/packages/shared/lib/utils/error.ts b/packages/shared/lib/utils/error.ts index 42d4b63386..894fa3729a 100644 --- a/packages/shared/lib/utils/error.ts +++ b/packages/shared/lib/utils/error.ts @@ -542,9 +542,9 @@ export class NangoError extends Error { this.message = `WorkOS is not configured. Please reach out to support to obtain valid WorkOS credentials.`; break; - case 'missing_hosted_login_callback_code': + case 'missing_managed_login_callback_code': this.status = 400; - this.message = `Missing param 'code' for the hosted login callback.`; + this.message = `Missing param 'code' for the managed login callback.`; break; case 'missing_name_for_account_creation': diff --git a/packages/shared/lib/utils/utils.ts b/packages/shared/lib/utils/utils.ts index 4dba602360..5aa13d5fe2 100644 --- a/packages/shared/lib/utils/utils.ts +++ b/packages/shared/lib/utils/utils.ts @@ -35,7 +35,7 @@ export enum NodeEnv { } export const AUTH_ENABLED = isCloud() || isEnterprise(); -export const HOSTED_AUTH_ENABLED = isCloud() || isLocal(); +export const MANAGED_AUTH_ENABLED = isCloud() || isLocal(); export const JAVASCRIPT_AND_TYPESCRIPT_TYPES = { primitives: ['string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'null'], diff --git a/packages/webapp/src/components/ui/button/Auth/Google.tsx b/packages/webapp/src/components/ui/button/Auth/Google.tsx index c0792d6718..7a87f40576 100644 --- a/packages/webapp/src/components/ui/button/Auth/Google.tsx +++ b/packages/webapp/src/components/ui/button/Auth/Google.tsx @@ -27,8 +27,7 @@ export default function GoogleButton({ text, setServerErrorMessage, invitedAccou accountId: invitedAccountID }); } - console.log(invitedAccountID, token); - const endpoint = token ? `/api/v1/hosted/signup/${token}?provider=GoogleOAuth` : '/api/v1/hosted/signup?provider=GoogleOAuth'; + const endpoint = token ? `/api/v1/managed/signup/${token}?provider=GoogleOAuth` : '/api/v1/managed/signup?provider=GoogleOAuth'; const res = await fetch(endpoint, postBody); diff --git a/packages/webapp/src/pages/InviteSignup.tsx b/packages/webapp/src/pages/InviteSignup.tsx index 6eadb1b891..ace9dd7285 100644 --- a/packages/webapp/src/pages/InviteSignup.tsx +++ b/packages/webapp/src/pages/InviteSignup.tsx @@ -2,8 +2,9 @@ import { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useInviteSignupAPI, useSignupAPI } from '../utils/api'; -import { HOSTED_AUTH_ENABLED, isEnterprise } from '../utils/utils'; -import { useSignin, User } from '../utils/user'; +import { MANAGED_AUTH_ENABLED, isEnterprise } from '../utils/utils'; +import { useSignin } from '../utils/user'; +import type { User } from '../utils/user'; import DefaultLayout from '../layout/DefaultLayout'; import GoogleButton from '../components/ui/button/Auth/Google'; @@ -130,7 +131,7 @@ export default function InviteSignup() { {serverErrorMessage &&

{serverErrorMessage}

}
- {HOSTED_AUTH_ENABLED && ( + {MANAGED_AUTH_ENABLED && ( <>
diff --git a/packages/webapp/src/pages/Signin.tsx b/packages/webapp/src/pages/Signin.tsx index 6c4e33a242..b41b343e81 100644 --- a/packages/webapp/src/pages/Signin.tsx +++ b/packages/webapp/src/pages/Signin.tsx @@ -2,8 +2,9 @@ import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useSigninAPI } from '../utils/api'; -import { useSignin, User } from '../utils/user'; -import { HOSTED_AUTH_ENABLED } from '../utils/utils'; +import { useSignin } from '../utils/user'; +import type { User } from '../utils/user'; +import { MANAGED_AUTH_ENABLED } from '../utils/utils'; import DefaultLayout from '../layout/DefaultLayout'; import GoogleButton from '../components/ui/button/Auth/Google'; @@ -86,7 +87,7 @@ export default function Signin() { {serverErrorMessage &&

{serverErrorMessage}

}
- {HOSTED_AUTH_ENABLED && ( + {MANAGED_AUTH_ENABLED && ( <>
diff --git a/packages/webapp/src/pages/Signup.tsx b/packages/webapp/src/pages/Signup.tsx index 9f87f10556..6e04cc8554 100644 --- a/packages/webapp/src/pages/Signup.tsx +++ b/packages/webapp/src/pages/Signup.tsx @@ -2,9 +2,10 @@ import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAnalyticsTrack } from '../utils/analytics'; -import { HOSTED_AUTH_ENABLED } from '../utils/utils'; +import { MANAGED_AUTH_ENABLED } from '../utils/utils'; import { useSignupAPI } from '../utils/api'; -import { useSignin, User } from '../utils/user'; +import type { User } from '../utils/user'; +import { useSignin } from '../utils/user'; import DefaultLayout from '../layout/DefaultLayout'; import GoogleButton from '../components/ui/button/Auth/Google'; @@ -107,7 +108,7 @@ export default function Signup() { {serverErrorMessage &&

{serverErrorMessage}

}
- {HOSTED_AUTH_ENABLED && ( + {MANAGED_AUTH_ENABLED && ( <>
diff --git a/packages/webapp/src/utils/utils.tsx b/packages/webapp/src/utils/utils.tsx index 873fc94646..d7afdc9aa7 100644 --- a/packages/webapp/src/utils/utils.tsx +++ b/packages/webapp/src/utils/utils.tsx @@ -10,7 +10,7 @@ export const prodUrl: string = 'https://api.nango.dev'; export const syncDocs = 'https://docs.nango.dev/integrate/guides/sync-data-from-an-api'; export const AUTH_ENABLED = isCloud() || isEnterprise() || isLocal(); -export const HOSTED_AUTH_ENABLED = isCloud() || isLocal(); +export const MANAGED_AUTH_ENABLED = isCloud() || isLocal(); export function isHosted() { return process.env.REACT_APP_ENV === 'hosted'; From 3dfd105b4c050698dfb7f12084a87b2b3fe3a85f Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 20 Mar 2024 19:49:05 +0200 Subject: [PATCH 16/16] [nan-578] throw error if workos not configured and is cloud --- packages/server/lib/controllers/auth.controller.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server/lib/controllers/auth.controller.ts b/packages/server/lib/controllers/auth.controller.ts index 70868d2fc1..fc07f8a945 100644 --- a/packages/server/lib/controllers/auth.controller.ts +++ b/packages/server/lib/controllers/auth.controller.ts @@ -39,8 +39,12 @@ interface InviteAccountState extends InviteAccountBody { } let workos: WorkOS | null = null; -if (process.env['WORKOS_API_KEY']) { +if (process.env['WORKOS_API_KEY'] && process.env['WORKOS_CLIENT_ID']) { workos = new WorkOS(process.env['WORKOS_API_KEY']); +} else { + if (isCloud()) { + throw new NangoError('workos_not_configured'); + } } const allowedProviders = ['GoogleOAuth'];