implemented comments

This commit is contained in:
Sheen Capadngan
2023-05-03 18:58:32 +08:00
parent dfb84e9932
commit f703ee29e5
16 changed files with 247 additions and 151 deletions

View File

@@ -70,6 +70,6 @@ STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
CLIENT_ID_GOOGLE=
CLIENT_SECRET_GOOGLE=
SESSION_SECRET=

View File

@@ -29,12 +29,14 @@ export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_
export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue;
export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue;
export const getClientIdGoogle = async () => (await client.getSecret('CLIENT_ID_GOOGLE')).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret('CLIENT_SECRET_AZURE')).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret('CLIENT_SECRET_HEROKU')).secretValue;
export const getClientSecretVercel = async () => (await client.getSecret('CLIENT_SECRET_VERCEL')).secretValue;
export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue;
export const getClientSecretGitHub = async () => (await client.getSecret('CLIENT_SECRET_GITHUB')).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret('CLIENT_SECRET_GITLAB')).secretValue;
export const getClientSecretGoogle = async () => (await client.getSecret('CLIENT_SECRET_GOOGLE')).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue;
export const getPostHogHost = async () => (await client.getSecret('POSTHOG_HOST')).secretValue || 'https://app.posthog.com';
export const getPostHogProjectApiKey = async () => (await client.getSecret('POSTHOG_PROJECT_API_KEY')).secretValue || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';

View File

@@ -267,7 +267,3 @@ export const getNewToken = async (req: Request, res: Response) => {
});
}
};
export const handleGoogleCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}

View File

@@ -5,6 +5,7 @@ import * as integrationController from './integrationController';
import * as keyController from './keyController';
import * as membershipController from './membershipController';
import * as membershipOrgController from './membershipOrgController';
import * as oauthController from './oauthController';
import * as organizationController from './organizationController';
import * as passwordController from './passwordController';
import * as secretController from './secretController';
@@ -23,6 +24,7 @@ export {
keyController,
membershipController,
membershipOrgController,
oauthController,
organizationController,
passwordController,
secretController,

View File

@@ -0,0 +1,5 @@
import { Request, Response } from 'express';
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}

View File

@@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import { issueAuthTokens, createToken } from '../../helpers/auth';
import { issueAuthTokens, createToken, validateProviderAuthToken } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
@@ -20,12 +20,14 @@ import {
getJwtMfaLifetime,
getJwtMfaSecret,
getHttpsEnabled,
getJwtProviderAuthSecret
} from '../../config';
import { AuthProvider } from '../../models/user';
declare module 'jsonwebtoken' {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthProvider;
}
}
@@ -42,30 +44,25 @@ export const login1 = async (req: Request, res: Response) => {
providerAuthToken,
clientPublicKey
}: {
email?: string;
email: string;
clientPublicKey: string,
providerAuthToken?: string;
} = req.body;
let userId = '';
if (providerAuthToken) {
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
);
userId = decodedToken.userId;
}
const filter = userId ? {
_id: userId,
} : {
const user = await User.findOne({
email,
}
const user = await User.findOne(filter).select('+salt +verifier');
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
if (user.authProvider) {
await validateProviderAuthToken({
email,
user,
providerAuthToken,
})
}
const server = new jsrp.server();
server.init(
{
@@ -75,14 +72,10 @@ export const login1 = async (req: Request, res: Response) => {
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
const identifier = userId ? {
userId,
} : {
email,
}
await LoginSRPDetail.findOneAndReplace(filter, {
...identifier,
await LoginSRPDetail.findOneAndReplace({
userId: user.id,
}, {
userId: user.id,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
@@ -115,32 +108,22 @@ export const login2 = async (req: Request, res: Response) => {
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
const { email, clientProof, providerAuthToken } = req.body;
let userId = '';
if (providerAuthToken) {
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
);
userId = decodedToken.userId;
}
const filter = userId ? {
_id: userId,
} : {
const user = await User.findOne({
email,
}
const user = await User.findOne(filter).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user');
const identifier = userId ? {
userId,
} : {
email,
if (user.authProvider) {
await validateProviderAuthToken({
email,
user,
providerAuthToken,
})
}
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ ...identifier });
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ userId: user.id });
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))

View File

@@ -20,6 +20,7 @@ import {
import {
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtProviderAuthSecret,
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
@@ -319,8 +320,34 @@ const createToken = ({
});
};
const validateProviderAuthToken = async ({
email,
user,
providerAuthToken,
}: {
email: string;
user: IUser,
providerAuthToken?: string;
}) => {
if (!providerAuthToken) {
throw new Error('Invalid authentication request.');
}
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
);
if (
decodedToken.authProvider !== user.authProvider ||
decodedToken.email !== email
) {
throw new Error('Invalid authentication credentials.')
}
}
export {
validateAuthMode,
validateProviderAuthToken,
getAuthUserPayload,
getAuthSTDPayload,
getAuthSAAKPayload,

View File

@@ -37,6 +37,7 @@ import {
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
oauth as v1OAuth,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
@@ -78,6 +79,7 @@ import {
getSiteURL,
getSessionSecret,
} from './config';
import { initializePassport } from './utils/auth';
const main = async () => {
TelemetryService.logTelemetryMessage();
@@ -95,6 +97,9 @@ const main = async () => {
patchRouterParam();
const app = express();
await initializePassport();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
@@ -129,6 +134,7 @@ const main = async () => {
// v1 routes (default)
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/oauth', v1OAuth);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);

View File

@@ -16,7 +16,7 @@ import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; //
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from './serviceAccountOrganizationPermission'; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from './serviceAccountWorkspacePermission'; // new
import TokenData, { ITokenData } from './tokenData';
import User, { IUser } from './user';
import User,{ AuthProvider, IUser } from './user';
import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
@@ -24,6 +24,7 @@ import APIKeyData, { IAPIKeyData } from './apiKeyData';
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
export {
AuthProvider,
BackupPrivateKey,
IBackupPrivateKey,
Bot,

View File

@@ -1,9 +1,13 @@
import { Schema, model, Types, Document } from 'mongoose';
export enum AuthProvider {
GOOGLE = 'google',
}
export interface IUser extends Document {
_id: Types.ObjectId;
authId?: string;
authProvider?: string;
authProvider?: AuthProvider;
email: string;
firstName?: string;
lastName?: string;
@@ -33,6 +37,7 @@ const userSchema = new Schema<IUser>(
},
authProvider: {
type: String,
enum: AuthProvider,
},
email: {
type: String,

View File

@@ -1,50 +1,10 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import passport from 'passport';
import { requireAuth, validateRequest } from '../../middleware';
import { authController } from '../../controllers/v1';
import { authLimiter } from '../../helpers/rateLimiter';
import { AUTH_MODE_JWT } from '../../variables';
import { User } from '../../models';
import { createToken } from '../../helpers/auth';
import { getJwtProviderAuthLifetime, getJwtProviderAuthSecret } from '../../config';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GoogleStrategy = require('passport-google-oidc');
passport.use(new GoogleStrategy({
passReqToCallback: true,
clientID: process.env['GOOGLE_CLIENT_ID'],
clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
callbackURL: '/api/v1/auth/google/callback',
}, async (req: express.Request, issuer: any, profile: any, cb: any) => {
const email = profile.emails[0].value;
let user = await User.findOne({
authProvider: issuer,
authId: profile.id,
})
if (!user) {
user = await new User({
email,
authProvider: issuer,
authId: profile.id,
}).save();
}
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.providerAuthToken = providerAuthToken;
cb(null, profile);
}));
router.post('/token', validateRequest, authController.getNewToken);
@@ -83,18 +43,5 @@ router.post(
authController.checkAuth
);
router.get(
'/login/google',
authLimiter,
passport.authenticate('google', {
scope: ['profile', 'email'],
}),
)
router.get(
'/google/callback',
passport.authenticate('google', { failureRedirect: '/error', session: false }),
authController.handleGoogleCallback,
)
export default router;

View File

@@ -15,7 +15,8 @@ import password from './password';
import stripe from './stripe';
import integration from './integration';
import integrationAuth from './integrationAuth';
import secretsFolder from './secretsFolder'
import secretsFolder from './secretsFolder';
import oauth from './oauth';
export {
signup,
@@ -29,6 +30,7 @@ export {
membership,
key,
inviteOrg,
oauth,
secret,
serviceToken,
password,

View File

@@ -0,0 +1,21 @@
import express from 'express';
const router = express.Router();
import passport from 'passport';
import { oauthController } from '../../controllers/v1';
import { authLimiter } from '../../helpers/rateLimiter';
router.get(
'/redirect/google',
authLimiter,
passport.authenticate('google', {
scope: ['profile', 'email'],
}),
)
router.get(
'/callback/google',
passport.authenticate('google', { failureRedirect: '/error', session: false }),
oauthController.handleAuthProviderCallback,
)
export default router;

View File

@@ -1,10 +1,22 @@
import express from 'express';
import passport from 'passport';
import { AuthData } from '../interfaces/middleware';
import {
AuthProvider,
User,
ServiceAccount,
ServiceTokenData,
ServiceToken
} from '../models';
import { createToken } from '../helpers/auth';
import {
getClientIdGoogle,
getClientSecretGoogle,
getJwtProviderAuthLifetime,
getJwtProviderAuthSecret
} from '../config';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GoogleStrategy = require('passport-google-oidc');
// TODO: find a more optimal folder structure to store these types of functions
@@ -48,7 +60,47 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
}
}
const initializePassport = async () => {
const googleClientSecret = await getClientSecretGoogle();
const googleClientId = await getClientIdGoogle();
passport.use(new GoogleStrategy({
passReqToCallback: true,
clientID: googleClientId,
clientSecret: googleClientSecret,
callbackURL: '/api/v1/oauth/callback/google',
}, async (req: express.Request, issuer: any, profile: any, cb: any) => {
const email = profile.emails[0].value;
let user = await User.findOne({
authProvider: AuthProvider.GOOGLE,
authId: profile.id,
})
if (!user) {
user = await new User({
email,
authProvider: AuthProvider.GOOGLE,
authId: profile.id,
}).save();
}
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
authProvider: user.authProvider,
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.providerAuthToken = providerAuthToken;
cb(null, profile);
}));
}
export {
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj
}
getAuthDataPayloadUserObj,
initializePassport,
}

View File

@@ -24,12 +24,12 @@ export default function Login() {
const lang = router.locale ?? 'en';
const [isLoginWithEmail, setIsLoginWithEmail] = useState(false);
const {
providerAuthToken,
userId,
email: providerEmail,
setProviderAuthToken
providerAuthToken,
userId,
email: providerEmail,
setProviderAuthToken
} = useProviderAuth();
const setLanguage = async (to: string) => {
router.push('/login', '/login', { locale: to });
localStorage.setItem('lang', to);
@@ -51,7 +51,7 @@ export default function Login() {
redirectToDashboard();
}
}, []);
const renderView = (loginStep: number) => {
if (providerAuthToken && step === 1) {
@@ -83,12 +83,12 @@ export default function Login() {
return (
<>
<button type='button' className='text-white' onClick={() => {
window.open('/api/v1/auth/login/google')
window.open('/api/v1/oauth/redirect/google')
}}>
Continue with Google
</button>
<button type='button' className='text-white' onClick={() => {
setIsLoginWithEmail(true);
setIsLoginWithEmail(true);
}}>
Continue with Email
</button>

View File

@@ -14,10 +14,12 @@ import UserInfoStep from '@app/components/signup/UserInfoStep';
import SecurityClient from '@app/components/utilities/SecurityClient';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { useProviderAuth } from '@app/hooks/useProviderAuth';
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
import getWorkspaces from './api/workspace/getWorkspaces';
/**
* @returns the signup page
*/
@@ -31,7 +33,13 @@ export default function SignUp() {
const [step, setStep] = useState(1);
const router = useRouter();
const { data: serverDetails } = useFetchServerStatus();
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
const { t } = useTranslation();
const { providerAuthToken } = useProviderAuth();
if (providerAuthToken && step < 3) {
setStep(3);
}
useEffect(() => {
const tryAuth = async () => {
@@ -60,7 +68,7 @@ export default function SignUp() {
// Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200) {
const {token} = await response.json();
const { token } = await response.json();
SecurityClient.setSignupToken(token);
setStep(3);
} else {
@@ -71,17 +79,83 @@ export default function SignUp() {
// when email service is not configured, skip step 2 and 5
useEffect(() => {
if (!serverDetails?.emailConfigured && step === 2){
if (!serverDetails?.emailConfigured && step === 2) {
incrementStep()
}
if (!serverDetails?.emailConfigured && step === 5){
getWorkspaces().then((userWorkspaces)=>{
if (!serverDetails?.emailConfigured && step === 5) {
getWorkspaces().then((userWorkspaces) => {
router.push(`/dashboard/${userWorkspaces[0]._id}`);
});
}
}, [step]);
const renderView = (registerStep: number) => {
if (isSignupWithEmail && registerStep === 1) {
return <EnterEmailStep email={email} setEmail={setEmail} incrementStep={incrementStep} />
}
if (!isSignupWithEmail && registerStep === 1) {
return (
<>
<button type='button' className='text-white' onClick={() => {
window.open('/api/v1/auth/login/google')
}}>
Continue with Google
</button>
<button type='button' className='text-white' onClick={() => {
setIsSignupWithEmail(true);
}}>
Continue with Email
</button>
</>
)
}
if (registerStep === 2) {
return (
<CodeInputStep
email={email}
incrementStep={incrementStep}
setCode={setCode}
codeError={codeError}
/>
)
}
if (registerStep === 3) {
return (
<UserInfoStep
incrementStep={incrementStep}
email={email}
password={password}
setPassword={setPassword}
firstName={firstName}
setFirstName={setFirstName}
lastName={lastName}
setLastName={setLastName}
/>
)
}
if (registerStep === 4) {
return (
<DownloadBackupPDF
incrementStep={incrementStep}
email={email}
password={password}
name={`${firstName} ${lastName}`}
/>
)
}
if (serverDetails?.emailConfigured) {
return <TeamInviteStep />
}
return ""
}
return (
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
<Head>
@@ -98,34 +172,7 @@ export default function SignUp() {
</div>
</Link>
<form onSubmit={(e) => e.preventDefault()}>
{step === 1 ? (
<EnterEmailStep email={email} setEmail={setEmail} incrementStep={incrementStep} />
) : step === 2 ? (
<CodeInputStep
email={email}
incrementStep={incrementStep}
setCode={setCode}
codeError={codeError}
/>
) : step === 3 ? (
<UserInfoStep
incrementStep={incrementStep}
email={email}
password={password}
setPassword={setPassword}
firstName={firstName}
setFirstName={setFirstName}
lastName={lastName}
setLastName={setLastName}
/>
) : step === 4 ? (
<DownloadBackupPDF
incrementStep={incrementStep}
email={email}
password={password}
name={`${firstName} ${lastName}`}
/>
) : (serverDetails?.emailConfigured ? <TeamInviteStep /> : "")}
{renderView(step)}
</form>
</div>
</div>