mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 19:28:16 +00:00
Checkpoint weaving frontend and backend MFA
This commit is contained in:
@@ -4,6 +4,8 @@ const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
|
|||||||
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
|
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
|
||||||
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
||||||
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
|
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
|
||||||
|
const JWT_MFA_LIFETIME = process.env.JWT_MFA_LIFETIME! || '5m';
|
||||||
|
const JWT_MFA_SECRET = process.env.JWT_MFA_SECRET!;
|
||||||
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
|
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
|
||||||
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
|
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
|
||||||
const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!;
|
const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!;
|
||||||
@@ -54,6 +56,8 @@ export {
|
|||||||
SALT_ROUNDS,
|
SALT_ROUNDS,
|
||||||
JWT_AUTH_LIFETIME,
|
JWT_AUTH_LIFETIME,
|
||||||
JWT_AUTH_SECRET,
|
JWT_AUTH_SECRET,
|
||||||
|
JWT_MFA_LIFETIME,
|
||||||
|
JWT_MFA_SECRET,
|
||||||
JWT_REFRESH_LIFETIME,
|
JWT_REFRESH_LIFETIME,
|
||||||
JWT_REFRESH_SECRET,
|
JWT_REFRESH_SECRET,
|
||||||
JWT_SERVICE_SECRET,
|
JWT_SERVICE_SECRET,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node';
|
|||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import { User, LoginSRPDetail } from '../../models';
|
import { User, LoginSRPDetail } from '../../models';
|
||||||
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
|
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
|
||||||
import {
|
import {
|
||||||
ACTION_LOGIN,
|
ACTION_LOGIN,
|
||||||
ACTION_LOGOUT
|
ACTION_LOGOUT
|
||||||
@@ -111,7 +111,7 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueTokens({ userId: user._id.toString() });
|
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import * as Sentry from '@sentry/node';
|
|||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import { User } from '../../models';
|
import { User } from '../../models';
|
||||||
import { issueTokens } from '../../helpers/auth';
|
import { issueAuthTokens, createToken } from '../../helpers/auth';
|
||||||
import { sendMail } from '../../helpers/nodemailer';
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
import { TokenService } from '../../services';
|
import { TokenService } from '../../services';
|
||||||
import {
|
import {
|
||||||
NODE_ENV
|
NODE_ENV,
|
||||||
|
JWT_MFA_LIFETIME,
|
||||||
|
JWT_MFA_SECRET
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
import {
|
import {
|
||||||
TOKEN_EMAIL_MFA
|
TOKEN_EMAIL_MFA
|
||||||
@@ -103,6 +105,15 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
if (user.isMfaEnabled) {
|
if (user.isMfaEnabled) {
|
||||||
// case: user has MFA enabled
|
// case: user has MFA enabled
|
||||||
|
|
||||||
|
// generate temporary MFA token
|
||||||
|
const token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId: user._id.toString()
|
||||||
|
},
|
||||||
|
expiresIn: JWT_MFA_LIFETIME,
|
||||||
|
secret: JWT_MFA_SECRET
|
||||||
|
});
|
||||||
|
|
||||||
const code = await TokenService.createToken({
|
const code = await TokenService.createToken({
|
||||||
type: TOKEN_EMAIL_MFA,
|
type: TOKEN_EMAIL_MFA,
|
||||||
email
|
email
|
||||||
@@ -119,12 +130,13 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
mfaEnabled: true
|
mfaEnabled: true,
|
||||||
|
token
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueTokens({ userId: user._id.toString() });
|
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
@@ -136,18 +148,41 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// case: user does not have MFA enabled
|
// case: user does not have MFA enabled
|
||||||
// return (access) token in response
|
// return (access) token in response
|
||||||
return res.status(200).send({
|
|
||||||
|
interface ResponseData {
|
||||||
|
mfaEnabled: boolean;
|
||||||
|
encryptionVersion: any;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
token: string;
|
||||||
|
publicKey?: string;
|
||||||
|
encryptedPrivateKey?: string;
|
||||||
|
iv?: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ResponseData = {
|
||||||
mfaEnabled: false,
|
mfaEnabled: false,
|
||||||
encryptionVersion: user.encryptionVersion,
|
encryptionVersion: user.encryptionVersion,
|
||||||
protectedKey: user.protectedKey ?? null,
|
|
||||||
protectedKeyIV: user.protectedKeyIV ?? null,
|
|
||||||
protectedKeyTag: user.protectedKeyTag ?? null,
|
|
||||||
token: tokens.token,
|
token: tokens.token,
|
||||||
publicKey: user.publicKey,
|
publicKey: user.publicKey,
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
iv: user.iv,
|
iv: user.iv,
|
||||||
tag: user.tag
|
tag: user.tag
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user?.protectedKey &&
|
||||||
|
user?.protectedKeyIV &&
|
||||||
|
user?.protectedKeyTag
|
||||||
|
) {
|
||||||
|
response.protectedKey = user.protectedKey;
|
||||||
|
response.protectedKeyIV = user.protectedKeyIV
|
||||||
|
response.protectedKeyTag = user.protectedKeyTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
@@ -164,6 +199,43 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send MFA token to email [email]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const sendMfaToken = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
const code = await TokenService.createToken({
|
||||||
|
type: TOKEN_EMAIL_MFA,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
|
||||||
|
// send MFA code [code] to [email]
|
||||||
|
await sendMail({
|
||||||
|
template: 'emailMfa.handlebars',
|
||||||
|
subjectLine: 'Infisical MFA code',
|
||||||
|
recipients: [email],
|
||||||
|
substitutions: {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to send MFA code'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully sent new MFA code'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
|
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
|
||||||
* MFA token [mfaToken] is valid
|
* MFA token [mfaToken] is valid
|
||||||
@@ -187,7 +259,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueTokens({ userId: user._id.toString() });
|
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
@@ -218,3 +290,4 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import { completeAccount } from '../../helpers/user';
|
|||||||
import {
|
import {
|
||||||
initializeDefaultOrg
|
initializeDefaultOrg
|
||||||
} from '../../helpers/signup';
|
} from '../../helpers/signup';
|
||||||
import { issueTokens } from '../../helpers/auth';
|
import { issueAuthTokens } from '../../helpers/auth';
|
||||||
import { INVITED, ACCEPTED } from '../../variables';
|
import { INVITED, ACCEPTED } from '../../variables';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// TODO: finish
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete setting up user by adding their personal and auth information as part of the
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
* signup flow
|
* signup flow
|
||||||
@@ -102,7 +100,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueTokens({
|
const tokens = await issueAuthTokens({
|
||||||
userId: user._id.toString()
|
userId: user._id.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,7 +214,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueTokens({
|
const tokens = await issueAuthTokens({
|
||||||
userId: user._id.toString()
|
userId: user._id.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ const getAuthAPIKeyPayload = async ({
|
|||||||
* @return {String} obj.token - issued JWT token
|
* @return {String} obj.token - issued JWT token
|
||||||
* @return {String} obj.refreshToken - issued refresh token
|
* @return {String} obj.refreshToken - issued refresh token
|
||||||
*/
|
*/
|
||||||
const issueTokens = async ({ userId }: { userId: string }) => {
|
const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||||
let token: string;
|
let token: string;
|
||||||
let refreshToken: string;
|
let refreshToken: string;
|
||||||
try {
|
try {
|
||||||
@@ -298,6 +298,6 @@ export {
|
|||||||
getAuthSTDPayload,
|
getAuthSTDPayload,
|
||||||
getAuthAPIKeyPayload,
|
getAuthAPIKeyPayload,
|
||||||
createToken,
|
createToken,
|
||||||
issueTokens,
|
issueAuthTokens,
|
||||||
clearTokens
|
clearTokens
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import * as Sentry from '@sentry/node';
|
|||||||
import { IUser } from '../models';
|
import { IUser } from '../models';
|
||||||
import { createOrganization } from './organization';
|
import { createOrganization } from './organization';
|
||||||
import { addMembershipsOrg } from './membershipOrg';
|
import { addMembershipsOrg } from './membershipOrg';
|
||||||
import { createWorkspace } from './workspace';
|
import { OWNER, ACCEPTED } from '../variables';
|
||||||
import { addMemberships } from './membership';
|
|
||||||
import { OWNER, ADMIN, ACCEPTED } from '../variables';
|
|
||||||
import { sendMail } from '../helpers/nodemailer';
|
import { sendMail } from '../helpers/nodemailer';
|
||||||
import { TokenService } from '../services';
|
import { TokenService } from '../services';
|
||||||
import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
|
import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
|
||||||
@@ -95,18 +93,6 @@ const initializeDefaultOrg = async ({
|
|||||||
roles: [OWNER],
|
roles: [OWNER],
|
||||||
statuses: [ACCEPTED]
|
statuses: [ACCEPTED]
|
||||||
});
|
});
|
||||||
|
|
||||||
// initialize a default workspace inside the new organization
|
|
||||||
const workspace = await createWorkspace({
|
|
||||||
name: `Example Project`,
|
|
||||||
organizationId: organization._id.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
await addMemberships({
|
|
||||||
userIds: [user._id.toString()],
|
|
||||||
workspaceId: workspace._id.toString(),
|
|
||||||
roles: [ADMIN]
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
|
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import requireAuth from './requireAuth';
|
import requireAuth from './requireAuth';
|
||||||
|
import requireMfaAuth from './requireMfaAuth';
|
||||||
import requireBotAuth from './requireBotAuth';
|
import requireBotAuth from './requireBotAuth';
|
||||||
import requireSignupAuth from './requireSignupAuth';
|
import requireSignupAuth from './requireSignupAuth';
|
||||||
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||||
@@ -15,6 +16,7 @@ import validateRequest from './validateRequest';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
requireMfaAuth,
|
||||||
requireBotAuth,
|
requireBotAuth,
|
||||||
requireSignupAuth,
|
requireSignupAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { User } from '../models';
|
||||||
|
import { JWT_MFA_SECRET } from '../config';
|
||||||
|
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
|
declare module 'jsonwebtoken' {
|
||||||
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if (MFA) JWT temporary token on request is valid (e.g. not expired)
|
||||||
|
* and if there is an associated user.
|
||||||
|
*/
|
||||||
|
const requireMfaAuth = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
// JWT (temporary) authentication middleware for complete signup
|
||||||
|
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||||
|
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
|
||||||
|
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
|
||||||
|
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
|
||||||
|
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(AUTH_TOKEN_VALUE, JWT_MFA_SECRET)
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: decodedToken.userId
|
||||||
|
}).select('+publicKey');
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'}))
|
||||||
|
|
||||||
|
req.user = user;
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireMfaAuth;
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { body } from 'express-validator';
|
import { body } from 'express-validator';
|
||||||
import { validateRequest } from '../../middleware';
|
import { requireMfaAuth, validateRequest } from '../../middleware';
|
||||||
import { authController } from '../../controllers/v2';
|
import { authController } from '../../controllers/v2';
|
||||||
import { authLimiter } from '../../helpers/rateLimiter';
|
import { authLimiter } from '../../helpers/rateLimiter';
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/login1',
|
'/login1',
|
||||||
authLimiter,
|
authLimiter,
|
||||||
body('email').exists().trim().notEmpty(),
|
body('email').isString().trim().notEmpty(),
|
||||||
body('clientPublicKey').exists().trim().notEmpty(),
|
body('clientPublicKey').isString().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
authController.login1
|
authController.login1
|
||||||
);
|
);
|
||||||
@@ -17,19 +17,28 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
'/login2',
|
'/login2',
|
||||||
authLimiter,
|
authLimiter,
|
||||||
body('email').exists().trim().notEmpty(),
|
body('email').isString().trim().notEmpty(),
|
||||||
body('clientProof').exists().trim().notEmpty(),
|
body('clientProof').isString().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
authController.login2
|
authController.login2
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/mfa',
|
'/mfa/send',
|
||||||
authLimiter,
|
authLimiter,
|
||||||
body('email').exists().trim().notEmpty(),
|
body('email').isString().trim().notEmpty(),
|
||||||
body('mfaToken').exists().trim().notEmpty(),
|
validateRequest,
|
||||||
validateRequest,
|
authController.sendMfaToken
|
||||||
authController.verifyMfaToken
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/mfa/verify',
|
||||||
|
authLimiter,
|
||||||
|
requireMfaAuth,
|
||||||
|
body('email').isString().trim().notEmpty(),
|
||||||
|
body('mfaToken').isString().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
authController.verifyMfaToken
|
||||||
);
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { faWarning } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import Button from '@app/components/basic/buttons/Button';
|
||||||
|
import Error from '@app/components/basic/Error';
|
||||||
|
import InputField from '@app/components/basic/InputField';
|
||||||
|
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||||
|
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1st step of login - user enters their username and password
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.email - email of user
|
||||||
|
* @param {Function} obj.setEmail - function to set the email of user
|
||||||
|
* @param {String} obj.password - password of user
|
||||||
|
* @param {String} obj.setPassword - function to set the password of user
|
||||||
|
* @param {Function} obj.setStep - function to set the login flow step
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function LoginStep ({
|
||||||
|
email,
|
||||||
|
setEmail,
|
||||||
|
password,
|
||||||
|
setPassword,
|
||||||
|
setStep
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
setEmail: (email: string) => void;
|
||||||
|
password: string;
|
||||||
|
setPassword: (password: string) => void;
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [loginError, setLoginError] = useState(false);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
if (!email || !password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const isLoginSuccessful = await attemptLogin(email, password);
|
||||||
|
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||||
|
// case: login was successful
|
||||||
|
|
||||||
|
if (isLoginSuccessful.mfaEnabled) {
|
||||||
|
// case: login requires MFA step
|
||||||
|
setStep(2);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// case: login does not require MFA step
|
||||||
|
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setLoginError(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||||
|
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||||
|
{t('login:login')}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||||
|
<InputField
|
||||||
|
label={t('common:email')}
|
||||||
|
onChangeHandler={setEmail}
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
placeholder=""
|
||||||
|
isRequired
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||||
|
<InputField
|
||||||
|
label={t('common:password')}
|
||||||
|
onChangeHandler={setPassword}
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
placeholder=""
|
||||||
|
isRequired
|
||||||
|
autoComplete="current-password"
|
||||||
|
id="current-password"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||||
|
<Link href="/verify-email">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
||||||
|
>
|
||||||
|
{t('login:forgot-password')}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isLoading && loginError && <Error text={t('login:error-login') ?? ''} />}
|
||||||
|
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||||
|
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
text={t('login:login') ?? ''}
|
||||||
|
onButtonPressed={async () => handleLogin()}
|
||||||
|
loading={isLoading}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{false && (
|
||||||
|
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||||
|
{t('common:maintenance-alert')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||||
|
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||||
|
{t('login:need-account')}
|
||||||
|
</p>
|
||||||
|
<Link href="/signup">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
||||||
|
>
|
||||||
|
{t('login:create-account')}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
|
||||||
+62
-65
@@ -1,9 +1,11 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import ReactCodeInput from 'react-code-input';
|
import ReactCodeInput from 'react-code-input';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
|
import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa';
|
||||||
|
import resendMfaToken from '@app/pages/api/auth/resendMfaToken';
|
||||||
|
|
||||||
import Button from '../basic/buttons/Button';
|
import Button from '../basic/buttons/Button';
|
||||||
import Error from '../basic/Error';
|
import Error from '../basic/Error';
|
||||||
@@ -27,63 +29,65 @@ const props = {
|
|||||||
borderColor: '#2d2f33'
|
borderColor: '#2d2f33'
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
const propsPhone = {
|
|
||||||
inputStyle: {
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
margin: '4px',
|
|
||||||
MozAppearance: 'textfield',
|
|
||||||
width: '40px',
|
|
||||||
borderRadius: '5px',
|
|
||||||
fontSize: '24px',
|
|
||||||
height: '40px',
|
|
||||||
paddingLeft: '7',
|
|
||||||
backgroundColor: '#0d1117',
|
|
||||||
color: 'white',
|
|
||||||
border: '1px solid #2d2f33',
|
|
||||||
textAlign: 'center',
|
|
||||||
outlineColor: '#8ca542',
|
|
||||||
borderColor: '#2d2f33'
|
|
||||||
}
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
interface CodeInputStepProps {
|
|
||||||
email: string;
|
|
||||||
incrementStep: () => void;
|
|
||||||
setCode: (value: string) => void;
|
|
||||||
codeError: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the second step of sign up where users need to verify their email
|
* 2nd step of login - users enter their MFA code
|
||||||
* @param {object} obj
|
* @param {Object} obj
|
||||||
* @param {string} obj.email - user's email to which we just sent a verification email
|
* @param {String} obj.email - email of user
|
||||||
* @param {function} obj.incrementStep - goes to the next step of signup
|
* @param {String} obj.password - password of user
|
||||||
* @param {function} obj.setCode - state updating function that set the current value of the emai verification code
|
* @param {Function} obj.setStep - function to set the login flow step
|
||||||
* @param {boolean} obj.codeError - whether the code was inputted wrong or now
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function TwoFAStep({
|
export default function MFAStep({
|
||||||
email,
|
email,
|
||||||
incrementStep,
|
password
|
||||||
setCode,
|
}: {
|
||||||
codeError
|
email: string;
|
||||||
}: CodeInputStepProps): JSX.Element {
|
password: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
|
const [mfaCode, setMfaCode] = useState('');
|
||||||
|
const [codeError, setCodeError] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const resendVerificationEmail = async () => {
|
const handleLoginMfa = async () => {
|
||||||
setIsResendingVerificationEmail(true);
|
try {
|
||||||
setIsLoading(true);
|
if (mfaCode.length !== 6) {
|
||||||
sendVerificationEmail(email);
|
return;
|
||||||
setTimeout(() => {
|
}
|
||||||
setIsLoading(false);
|
|
||||||
setIsResendingVerificationEmail(false);
|
setIsLoading(true);
|
||||||
}, 2000);
|
const isLoginSuccessful = await attemptLoginMfa({
|
||||||
};
|
email,
|
||||||
|
password,
|
||||||
|
mfaToken: mfaCode
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoginSuccessful) {
|
||||||
|
setIsLoading(false);
|
||||||
|
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setCodeError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResendMfaCode = async () => {
|
||||||
|
try {
|
||||||
|
await resendMfaToken({
|
||||||
|
email
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
<form className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
||||||
<p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p>
|
<p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p>
|
||||||
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
|
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
@@ -92,38 +96,31 @@ export default function TwoFAStep({
|
|||||||
inputMode="tel"
|
inputMode="tel"
|
||||||
type="text"
|
type="text"
|
||||||
fields={6}
|
fields={6}
|
||||||
onChange={setCode}
|
onChange={setMfaCode}
|
||||||
{...props}
|
{...props}
|
||||||
className="mt-6 mb-2"
|
className="mt-6 mb-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="block md:hidden">
|
|
||||||
<ReactCodeInput
|
|
||||||
name=""
|
|
||||||
inputMode="tel"
|
|
||||||
type="text"
|
|
||||||
fields={6}
|
|
||||||
onChange={setCode}
|
|
||||||
{...propsPhone}
|
|
||||||
className="mt-2 mb-6"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{codeError && <Error text={t('signup:step2-code-error')} />}
|
{codeError && <Error text={t('signup:step2-code-error')} />}
|
||||||
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||||
<Button text={t('signup:verify') ?? ''} onButtonPressed={incrementStep} size="lg" />
|
<Button
|
||||||
|
text={t('signup:verify') ?? ''}
|
||||||
|
onButtonPressed={() => handleLoginMfa()}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
||||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||||
<span className="text-bunker-400">{t('signup:step2-resend-alert')}</span>
|
<span className="text-bunker-400">{t('signup:step2-resend-alert')}</span>
|
||||||
<u
|
<u
|
||||||
className={`font-normal ${
|
className={`font-normal ${
|
||||||
isResendingVerificationEmail
|
isLoading
|
||||||
? 'text-bunker-400'
|
? 'text-bunker-400'
|
||||||
: 'text-primary-700 hover:text-primary duration-200'
|
: 'text-primary-700 hover:text-primary duration-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
|
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
|
||||||
{isResendingVerificationEmail
|
{isLoading
|
||||||
? t('signup:step2-resend-progress')
|
? t('signup:step2-resend-progress')
|
||||||
: t('signup:step2-resend-submit')}
|
: t('signup:step2-resend-submit')}
|
||||||
</button>
|
</button>
|
||||||
@@ -131,6 +128,6 @@ export default function TwoFAStep({
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p>
|
<p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
@@ -10,20 +9,21 @@ import nacl from 'tweetnacl';
|
|||||||
import { encodeBase64 } from 'tweetnacl-util';
|
import { encodeBase64 } from 'tweetnacl-util';
|
||||||
|
|
||||||
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
|
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
|
||||||
|
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||||
|
import ProjectService from '@app/services/ProjectService';
|
||||||
|
|
||||||
import Button from '../basic/buttons/Button';
|
import Button from '../basic/buttons/Button';
|
||||||
import InputField from '../basic/InputField';
|
import InputField from '../basic/InputField';
|
||||||
import attemptLogin from '../utilities/attemptLogin';
|
|
||||||
import passwordCheck from '../utilities/checks/PasswordCheck';
|
import passwordCheck from '../utilities/checks/PasswordCheck';
|
||||||
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
||||||
import { deriveArgonKey } from '../utilities/cryptography/crypto';
|
import { deriveArgonKey } from '../utilities/cryptography/crypto';
|
||||||
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
|
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
|
||||||
|
import SecurityClient from '../utilities/SecurityClient';
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|
||||||
interface UserInfoStepProps {
|
interface UserInfoStepProps {
|
||||||
verificationToken: string;
|
|
||||||
incrementStep: () => void;
|
incrementStep: () => void;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -48,7 +48,6 @@ interface UserInfoStepProps {
|
|||||||
* @param {string} obj.setLastName - function managing the state of user's last name
|
* @param {string} obj.setLastName - function managing the state of user's last name
|
||||||
*/
|
*/
|
||||||
export default function UserInfoStep({
|
export default function UserInfoStep({
|
||||||
verificationToken,
|
|
||||||
incrementStep,
|
incrementStep,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
@@ -66,7 +65,6 @@ export default function UserInfoStep({
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Verifies if the information that the users entered (name, workspace)
|
// Verifies if the information that the users entered (name, workspace)
|
||||||
// is there, and if the password matches the criteria.
|
// is there, and if the password matches the criteria.
|
||||||
@@ -110,6 +108,8 @@ export default function UserInfoStep({
|
|||||||
async () => {
|
async () => {
|
||||||
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// TODO: moduralize into KeyService
|
||||||
const derivedKey = await deriveArgonKey({
|
const derivedKey = await deriveArgonKey({
|
||||||
password,
|
password,
|
||||||
salt: result.salt,
|
salt: result.salt,
|
||||||
@@ -158,28 +158,29 @@ export default function UserInfoStep({
|
|||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt: result.salt,
|
salt: result.salt,
|
||||||
verifier: result.verifier,
|
verifier: result.verifier,
|
||||||
token: verificationToken,
|
|
||||||
organizationName: `${firstName}'s organization`
|
organizationName: `${firstName}'s organization`
|
||||||
});
|
});
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
SecurityClient.setToken(response.token);
|
||||||
if (response.status === 200) {
|
|
||||||
// response = await response.json();
|
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
saveTokenToLocalStorage({
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
privateKey
|
privateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
await attemptLogin(email, password, () => {}, router, true, false);
|
incrementStep();
|
||||||
incrementStep();
|
|
||||||
}
|
const userOrgs = await getOrganizations();
|
||||||
|
await ProjectService.initProject({
|
||||||
|
organizationId: userOrgs[0]?._id,
|
||||||
|
projectName: 'Example Project'
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
import jsrp from 'jsrp';
|
import jsrp from 'jsrp';
|
||||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
|
||||||
|
|
||||||
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
|
||||||
import login1 from '@app/pages/api/auth/Login1';
|
import login1 from '@app/pages/api/auth/Login1';
|
||||||
import login2 from '@app/pages/api/auth/Login2';
|
import login2 from '@app/pages/api/auth/Login2';
|
||||||
import addSecrets from '@app/pages/api/files/AddSecrets';
|
|
||||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||||
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||||
import getUser from '@app/pages/api/user/getUser';
|
import KeyService from '@app/services/KeyService';
|
||||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
|
||||||
|
|
||||||
import { deriveArgonKey, encryptAssymmetric } from './cryptography/crypto';
|
|
||||||
import encryptSecrets from './secrets/encryptSecrets';
|
|
||||||
import Telemetry from './telemetry/Telemetry';
|
import Telemetry from './telemetry/Telemetry';
|
||||||
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
||||||
import SecurityClient from './SecurityClient';
|
import SecurityClient from './SecurityClient';
|
||||||
@@ -22,44 +14,39 @@ import SecurityClient from './SecurityClient';
|
|||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|
||||||
|
interface IsLoginSuccessful {
|
||||||
|
mfaEnabled: boolean;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function logs in the user (whether it's right after signup, or a normal login)
|
* Return whether or not login is successful for user with email [email]
|
||||||
* @param {string} email - email of the user logging in
|
* and password [password]
|
||||||
* @param {string} password - password of the user logging in
|
* @param {string} email - email of user to log in
|
||||||
* @param {function} setErrorLogin - function that visually dispay an error is something is wrong
|
* @param {string} password - password of user to log in
|
||||||
* @param {*} router
|
|
||||||
* @param {boolean} isSignUp - whether this log in is a part of signup
|
|
||||||
* @param {boolean} isLogin - ?
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
const attemptLogin = async (
|
const attemptLogin = async (
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string
|
||||||
setErrorLogin: (value: boolean) => void,
|
): Promise<IsLoginSuccessful> => {
|
||||||
router: any,
|
const telemetry = new Telemetry().getInstance();
|
||||||
isSignUp: boolean,
|
return new Promise((resolve, reject) => {
|
||||||
isLogin: boolean
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const telemetry = new Telemetry().getInstance();
|
|
||||||
|
|
||||||
client.init(
|
client.init(
|
||||||
{
|
{
|
||||||
username: email,
|
username: email,
|
||||||
password
|
password
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const clientPublicKey = client.getPublicKey();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const clientPublicKey = client.getPublicKey();
|
||||||
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
|
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
|
||||||
|
|
||||||
client.setSalt(salt);
|
client.setSalt(salt);
|
||||||
client.setServerPublicKey(serverPublicKey);
|
client.setServerPublicKey(serverPublicKey);
|
||||||
const clientProof = client.getProof(); // called M1
|
const clientProof = client.getProof(); // called M1
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
const {
|
||||||
const { // mfaEnabled
|
mfaEnabled,
|
||||||
encryptionVersion,
|
encryptionVersion,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
@@ -74,51 +61,39 @@ const attemptLogin = async (
|
|||||||
clientProof
|
clientProof
|
||||||
);
|
);
|
||||||
|
|
||||||
SecurityClient.setToken(token);
|
if (mfaEnabled) {
|
||||||
|
// case: MFA is enabled
|
||||||
|
|
||||||
let privateKey;
|
// set temporary (MFA) JWT token
|
||||||
if (encryptionVersion === 1) {
|
SecurityClient.setToken(token);
|
||||||
privateKey = Aes256Gcm.decrypt({
|
|
||||||
ciphertext: encryptedPrivateKey,
|
resolve({
|
||||||
iv,
|
mfaEnabled,
|
||||||
tag,
|
success: true
|
||||||
secret: password
|
|
||||||
.slice(0, 32)
|
|
||||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
|
||||||
});
|
});
|
||||||
|
} else if (
|
||||||
|
!mfaEnabled &&
|
||||||
|
encryptionVersion &&
|
||||||
|
encryptedPrivateKey &&
|
||||||
|
iv &&
|
||||||
|
tag &&
|
||||||
|
token
|
||||||
|
) {
|
||||||
|
// case: MFA is not enabled
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
// set JWT token
|
||||||
publicKey,
|
SecurityClient.setToken(token);
|
||||||
|
|
||||||
|
const privateKey = await KeyService.decryptPrivateKey({
|
||||||
|
encryptionVersion,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
|
|
||||||
const derivedKey = await deriveArgonKey({
|
|
||||||
password,
|
password,
|
||||||
salt,
|
salt,
|
||||||
mem: 65536,
|
protectedKey,
|
||||||
time: 3,
|
protectedKeyIV,
|
||||||
parallelism: 1,
|
protectedKeyTag
|
||||||
hashLen: 32
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!derivedKey) throw new Error('Failed to derive key');
|
|
||||||
|
|
||||||
const key = Aes256Gcm.decrypt({
|
|
||||||
ciphertext: protectedKey,
|
|
||||||
iv: protectedKeyIV,
|
|
||||||
tag: protectedKeyTag,
|
|
||||||
secret: Buffer.from(derivedKey.hash)
|
|
||||||
});
|
|
||||||
|
|
||||||
// decrypt back the private key
|
|
||||||
privateKey = Aes256Gcm.decrypt({
|
|
||||||
ciphertext: encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
secret: Buffer.from(key, 'hex')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
saveTokenToLocalStorage({
|
||||||
@@ -131,155 +106,160 @@ const attemptLogin = async (
|
|||||||
tag,
|
tag,
|
||||||
privateKey
|
privateKey
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!privateKey) throw new Error('Failed to decrypt private key');
|
// TODO: in the future - move this logic elsewhere
|
||||||
|
// because this function is about logging the user in
|
||||||
|
// and not initializing the login details
|
||||||
|
const userOrgs = await getOrganizations();
|
||||||
|
const orgId = userOrgs[0]._id;
|
||||||
|
localStorage.setItem('orgData.id', orgId);
|
||||||
|
|
||||||
const userOrgs = await getOrganizations();
|
const orgUserProjects = await getOrganizationUserProjects({
|
||||||
const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
|
orgId
|
||||||
|
|
||||||
let orgToLogin;
|
|
||||||
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
|
||||||
orgToLogin = localStorage.getItem('orgData.id');
|
|
||||||
} else {
|
|
||||||
orgToLogin = userOrgsData[0];
|
|
||||||
localStorage.setItem('orgData.id', orgToLogin);
|
|
||||||
}
|
|
||||||
|
|
||||||
let orgUserProjects = await getOrganizationUserProjects({
|
|
||||||
orgId: orgToLogin
|
|
||||||
});
|
|
||||||
|
|
||||||
orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
|
|
||||||
let projectToLogin;
|
|
||||||
if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
|
|
||||||
projectToLogin = localStorage.getItem('projectData.id');
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
projectToLogin = orgUserProjects[0];
|
|
||||||
localStorage.setItem('projectData.id', projectToLogin);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('ERROR: User likely has no projects. ', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (email) {
|
|
||||||
telemetry.identify(email);
|
|
||||||
telemetry.capture('User Logged In');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSignUp) {
|
|
||||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
|
||||||
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
|
|
||||||
|
|
||||||
const myUser = await getUser();
|
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: randomBytes,
|
|
||||||
publicKey: myUser.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
}) as { ciphertext: string; nonce: string };
|
|
||||||
|
|
||||||
await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
|
|
||||||
|
|
||||||
const secretsToBeAdded: SecretDataProps[] = [
|
|
||||||
{
|
|
||||||
pos: 0,
|
|
||||||
key: 'DATABASE_URL',
|
|
||||||
// eslint-disable-next-line no-template-curly-in-string
|
|
||||||
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
|
|
||||||
valueOverride: undefined,
|
|
||||||
comment: 'Secret referencing example',
|
|
||||||
id: '',
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pos: 1,
|
|
||||||
key: 'DB_USERNAME',
|
|
||||||
value: 'OVERRIDE_THIS',
|
|
||||||
valueOverride: undefined,
|
|
||||||
comment:
|
|
||||||
'Override secrets with personal value',
|
|
||||||
id: '',
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pos: 2,
|
|
||||||
key: 'DB_PASSWORD',
|
|
||||||
value: 'OVERRIDE_THIS',
|
|
||||||
valueOverride: undefined,
|
|
||||||
comment:
|
|
||||||
'Another secret override',
|
|
||||||
id: '',
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pos: 3,
|
|
||||||
key: 'DB_USERNAME',
|
|
||||||
value: 'user1234',
|
|
||||||
valueOverride: 'user1234',
|
|
||||||
comment: '',
|
|
||||||
id: '',
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pos: 4,
|
|
||||||
key: 'DB_PASSWORD',
|
|
||||||
value: 'example_password',
|
|
||||||
valueOverride: 'example_password',
|
|
||||||
comment: '',
|
|
||||||
id: '',
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pos: 5,
|
|
||||||
key: 'TWILIO_AUTH_TOKEN',
|
|
||||||
value: 'example_twillio_token',
|
|
||||||
valueOverride: undefined,
|
|
||||||
comment: '',
|
|
||||||
id: '',
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pos: 6,
|
|
||||||
key: 'WEBSITE_URL',
|
|
||||||
value: 'http://localhost:3000',
|
|
||||||
valueOverride: undefined,
|
|
||||||
comment: '',
|
|
||||||
id: '',
|
|
||||||
tags: []
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const secrets = await encryptSecrets({
|
|
||||||
secretsToEncrypt: secretsToBeAdded,
|
|
||||||
workspaceId: String(localStorage.getItem('projectData.id')),
|
|
||||||
env: 'dev'
|
|
||||||
});
|
});
|
||||||
await addSecrets({
|
localStorage.setItem('projectData.id', orgUserProjects[0]._id);
|
||||||
secrets: secrets ?? [],
|
|
||||||
env: 'dev',
|
// // TODO: this part definitely needs to be refactored
|
||||||
workspaceId: String(localStorage.getItem('projectData.id'))
|
// const userOrgs = await getOrganizations();
|
||||||
|
// const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
|
||||||
|
|
||||||
|
// let orgToLogin;
|
||||||
|
// if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
||||||
|
// orgToLogin = localStorage.getItem('orgData.id');
|
||||||
|
// } else {
|
||||||
|
// orgToLogin = userOrgsData[0];
|
||||||
|
// localStorage.setItem('orgData.id', orgToLogin);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let orgUserProjects = await getOrganizationUserProjects({
|
||||||
|
// orgId: orgToLogin
|
||||||
|
// });
|
||||||
|
|
||||||
|
// orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
|
||||||
|
// let projectToLogin;
|
||||||
|
// if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
|
||||||
|
// projectToLogin = localStorage.getItem('projectData.id');
|
||||||
|
// } else {
|
||||||
|
// try {
|
||||||
|
// projectToLogin = orgUserProjects[0];
|
||||||
|
// localStorage.setItem('projectData.id', projectToLogin);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log('ERROR: User likely has no projects. ', error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
telemetry.identify(email);
|
||||||
|
telemetry.capture('User Logged In');
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
mfaEnabled: false,
|
||||||
|
success: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
if (isLogin) {
|
reject(err);
|
||||||
if (localStorage.getItem('projectData.id') !== "undefined") {
|
|
||||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
|
||||||
} else {
|
|
||||||
router.push("/noprojects");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
setErrorLogin(true);
|
|
||||||
console.log('Login response not available');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
});
|
||||||
console.log('Something went wrong during authentication');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default attemptLogin;
|
export default attemptLogin;
|
||||||
|
|
||||||
|
// should be function: init first project
|
||||||
|
|
||||||
|
// if (isSignUp) {
|
||||||
|
// const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||||
|
// const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
|
||||||
|
|
||||||
|
// const myUser = await getUser();
|
||||||
|
|
||||||
|
// const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
// plaintext: randomBytes,
|
||||||
|
// publicKey: myUser.publicKey,
|
||||||
|
// privateKey: PRIVATE_KEY
|
||||||
|
// }) as { ciphertext: string; nonce: string };
|
||||||
|
|
||||||
|
// await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
|
||||||
|
|
||||||
|
// const secretsToBeAdded: SecretDataProps[] = [
|
||||||
|
// {
|
||||||
|
// pos: 0,
|
||||||
|
// key: 'DATABASE_URL',
|
||||||
|
// // eslint-disable-next-line no-template-curly-in-string
|
||||||
|
// value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
|
||||||
|
// valueOverride: undefined,
|
||||||
|
// comment: 'Secret referencing example',
|
||||||
|
// id: '',
|
||||||
|
// tags: []
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// pos: 1,
|
||||||
|
// key: 'DB_USERNAME',
|
||||||
|
// value: 'OVERRIDE_THIS',
|
||||||
|
// valueOverride: undefined,
|
||||||
|
// comment:
|
||||||
|
// 'Override secrets with personal value',
|
||||||
|
// id: '',
|
||||||
|
// tags: []
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// pos: 2,
|
||||||
|
// key: 'DB_PASSWORD',
|
||||||
|
// value: 'OVERRIDE_THIS',
|
||||||
|
// valueOverride: undefined,
|
||||||
|
// comment:
|
||||||
|
// 'Another secret override',
|
||||||
|
// id: '',
|
||||||
|
// tags: []
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// pos: 3,
|
||||||
|
// key: 'DB_USERNAME',
|
||||||
|
// value: 'user1234',
|
||||||
|
// valueOverride: 'user1234',
|
||||||
|
// comment: '',
|
||||||
|
// id: '',
|
||||||
|
// tags: []
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// pos: 4,
|
||||||
|
// key: 'DB_PASSWORD',
|
||||||
|
// value: 'example_password',
|
||||||
|
// valueOverride: 'example_password',
|
||||||
|
// comment: '',
|
||||||
|
// id: '',
|
||||||
|
// tags: []
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// pos: 5,
|
||||||
|
// key: 'TWILIO_AUTH_TOKEN',
|
||||||
|
// value: 'example_twillio_token',
|
||||||
|
// valueOverride: undefined,
|
||||||
|
// comment: '',
|
||||||
|
// id: '',
|
||||||
|
// tags: []
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// pos: 6,
|
||||||
|
// key: 'WEBSITE_URL',
|
||||||
|
// value: 'http://localhost:3000',
|
||||||
|
// valueOverride: undefined,
|
||||||
|
// comment: '',
|
||||||
|
// id: '',
|
||||||
|
// tags: []
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
// const secrets = await encryptSecrets({
|
||||||
|
// secretsToEncrypt: secretsToBeAdded,
|
||||||
|
// workspaceId: String(localStorage.getItem('projectData.id')),
|
||||||
|
// env: 'dev'
|
||||||
|
// });
|
||||||
|
// await addSecrets({
|
||||||
|
// secrets: secrets ?? [],
|
||||||
|
// env: 'dev',
|
||||||
|
// workspaceId: String(localStorage.getItem('projectData.id'))
|
||||||
|
// });
|
||||||
|
// }
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/* eslint-disable prefer-destructuring */
|
||||||
|
import jsrp from 'jsrp';
|
||||||
|
|
||||||
|
import login1 from '@app/pages/api/auth/Login1';
|
||||||
|
import verifyMfaToken from '@app/pages/api/auth/verifyMfaToken';
|
||||||
|
import KeyService from '@app/services/KeyService';
|
||||||
|
|
||||||
|
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
||||||
|
import SecurityClient from './SecurityClient';
|
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const client = new jsrp.client();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether or not MFA-login is successful for user with email [email]
|
||||||
|
* and MFA token [mfaToken]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.email - email of user
|
||||||
|
* @param {String} obj.mfaToken - MFA code/token
|
||||||
|
*/
|
||||||
|
const attemptLoginMfa = async ({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
mfaToken
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
mfaToken: string;
|
||||||
|
}): Promise<Boolean> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.init({
|
||||||
|
username: email,
|
||||||
|
password
|
||||||
|
}, async () => {
|
||||||
|
try {
|
||||||
|
const clientPublicKey = client.getPublicKey();
|
||||||
|
const { salt } = await login1(email, clientPublicKey);
|
||||||
|
|
||||||
|
const {
|
||||||
|
encryptionVersion,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
token,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
} = await verifyMfaToken({
|
||||||
|
email,
|
||||||
|
mfaToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// set JWT token
|
||||||
|
SecurityClient.setToken(token);
|
||||||
|
|
||||||
|
const privateKey = await KeyService.decryptPrivateKey({
|
||||||
|
encryptionVersion,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag
|
||||||
|
});
|
||||||
|
|
||||||
|
saveTokenToLocalStorage({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default attemptLoginMfa;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||||
|
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Number} obj.encryptionVersion
|
||||||
|
* @param {String} obj.encryptedPrivateKey
|
||||||
|
* @param {String} obj.iv
|
||||||
|
* @param {String} obj.tag
|
||||||
|
* @param {String} obj.password
|
||||||
|
* @param {String} obj.salt
|
||||||
|
* @param {String} obj.protectedKey
|
||||||
|
* @param {String} obj.protectedKeyIV
|
||||||
|
* @param {String} obj.protectedKeyTag
|
||||||
|
*/
|
||||||
|
const decryptPrivateKeyHelper = async ({
|
||||||
|
encryptionVersion,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
}: {
|
||||||
|
encryptionVersion: number;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
password: string;
|
||||||
|
salt: string;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
}) => {
|
||||||
|
let privateKey;
|
||||||
|
try {
|
||||||
|
if (encryptionVersion === 1) {
|
||||||
|
privateKey = Aes256Gcm.decrypt({
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
secret: password
|
||||||
|
.slice(0, 32)
|
||||||
|
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||||
|
});
|
||||||
|
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
|
||||||
|
const derivedKey = await deriveArgonKey({
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
mem: 65536,
|
||||||
|
time: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLen: 32
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!derivedKey) throw new Error('Failed to generate derived key');
|
||||||
|
|
||||||
|
const key = Aes256Gcm.decrypt({
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag,
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
// decrypt back the private key
|
||||||
|
privateKey = Aes256Gcm.decrypt({
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
secret: Buffer.from(key, 'hex')
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Insufficient details to decrypt private key');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new Error('Failed to decrypt private key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { decryptPrivateKeyHelper };
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
import { encryptAssymmetric } from '@app/components/utilities/cryptography/crypto';
|
||||||
|
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
|
||||||
|
import addSecrets from '@app/pages/api/files/AddSecrets';
|
||||||
|
import getUser from '@app/pages/api/user/getUser';
|
||||||
|
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
|
||||||
|
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||||
|
|
||||||
|
const secretsToBeAdded = [
|
||||||
|
{
|
||||||
|
pos: 0,
|
||||||
|
key: 'DATABASE_URL',
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
|
||||||
|
valueOverride: undefined,
|
||||||
|
comment: 'Secret referencing example',
|
||||||
|
id: '',
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: 1,
|
||||||
|
key: 'DB_USERNAME',
|
||||||
|
value: 'OVERRIDE_THIS',
|
||||||
|
valueOverride: undefined,
|
||||||
|
comment:
|
||||||
|
'Override secrets with personal value',
|
||||||
|
id: '',
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: 2,
|
||||||
|
key: 'DB_PASSWORD',
|
||||||
|
value: 'OVERRIDE_THIS',
|
||||||
|
valueOverride: undefined,
|
||||||
|
comment:
|
||||||
|
'Another secret override',
|
||||||
|
id: '',
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: 3,
|
||||||
|
key: 'DB_USERNAME',
|
||||||
|
value: 'user1234',
|
||||||
|
valueOverride: 'user1234',
|
||||||
|
comment: '',
|
||||||
|
id: '',
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: 4,
|
||||||
|
key: 'DB_PASSWORD',
|
||||||
|
value: 'example_password',
|
||||||
|
valueOverride: 'example_password',
|
||||||
|
comment: '',
|
||||||
|
id: '',
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: 5,
|
||||||
|
key: 'TWILIO_AUTH_TOKEN',
|
||||||
|
value: 'example_twillio_token',
|
||||||
|
valueOverride: undefined,
|
||||||
|
comment: '',
|
||||||
|
id: '',
|
||||||
|
tags: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: 6,
|
||||||
|
key: 'WEBSITE_URL',
|
||||||
|
value: 'http://localhost:3000',
|
||||||
|
valueOverride: undefined,
|
||||||
|
comment: '',
|
||||||
|
id: '',
|
||||||
|
tags: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and initialize a new project in organization with id [organizationId]
|
||||||
|
* Note: current user should be a member of the organization
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.organizationId - id of organization
|
||||||
|
* @param {String} obj.projectName - name of new project
|
||||||
|
* @returns {Project} project - new project
|
||||||
|
*/
|
||||||
|
const initProjectHelper = async ({
|
||||||
|
organizationId,
|
||||||
|
projectName
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
projectName: string;
|
||||||
|
}) => {
|
||||||
|
let project;
|
||||||
|
try {
|
||||||
|
// create new project
|
||||||
|
project = await createWorkspace({
|
||||||
|
workspaceName: projectName,
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
// create and upload new (encrypted) project key
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
|
||||||
|
if (!PRIVATE_KEY) throw new Error('Failed to find private key');
|
||||||
|
|
||||||
|
const user = await getUser();
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: randomBytes,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
await uploadKeys(project._id, user._id, ciphertext, nonce);
|
||||||
|
|
||||||
|
// encrypt and upload secrets to new project
|
||||||
|
const secrets = await encryptSecrets({
|
||||||
|
secretsToEncrypt: secretsToBeAdded,
|
||||||
|
workspaceId: project._id,
|
||||||
|
env: 'dev'
|
||||||
|
});
|
||||||
|
|
||||||
|
await addSecrets({
|
||||||
|
secrets: secrets ?? [],
|
||||||
|
env: 'dev',
|
||||||
|
workspaceId: project._id
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to init project in organization', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
initProjectHelper
|
||||||
|
}
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
import { UserWsKeyPair } from '../keys/types';
|
import { UserWsKeyPair } from '../keys/types';
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
seenIps: string[];
|
|
||||||
_id: string;
|
|
||||||
email: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
__v: number;
|
email?: string;
|
||||||
firstName: string;
|
firstName?: string;
|
||||||
lastName: string;
|
lastName?: string;
|
||||||
|
encryptionVersion?: number;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
encryptedPrivateKey?: string;
|
||||||
|
iv?: string;
|
||||||
|
tag?: string;
|
||||||
|
isMfaEnabled: boolean;
|
||||||
|
seenIps: string[];
|
||||||
|
_id: string;
|
||||||
|
__v: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OrgUser = {
|
export type OrgUser = {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -12,7 +14,6 @@ interface Props {
|
|||||||
organizationName: string;
|
organizationName: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
token: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +33,6 @@ interface Props {
|
|||||||
* @param {string} obj.tag
|
* @param {string} obj.tag
|
||||||
* @param {string} obj.salt
|
* @param {string} obj.salt
|
||||||
* @param {string} obj.verifier
|
* @param {string} obj.verifier
|
||||||
* @param {string} obj.token - token that confirms a user's identity
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const completeAccountInformationSignup = ({
|
const completeAccountInformationSignup = ({
|
||||||
@@ -48,13 +48,11 @@ const completeAccountInformationSignup = ({
|
|||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
token,
|
|
||||||
organizationName
|
organizationName
|
||||||
}: Props) => fetch('/api/v2/signup/complete-account/signup', {
|
}: Props) => SecurityClient.fetchCall('/api/v2/signup/complete-account/signup', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json'
|
||||||
Authorization: `Bearer ${ token}`
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email,
|
email,
|
||||||
@@ -71,6 +69,12 @@ const completeAccountInformationSignup = ({
|
|||||||
verifier,
|
verifier,
|
||||||
organizationName
|
organizationName
|
||||||
})
|
})
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res?.status === 200) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
console.log('Failed to verify MFA code');
|
||||||
|
throw new Error('Something went wrong during MFA code verification');
|
||||||
});
|
});
|
||||||
|
|
||||||
export default completeAccountInformationSignup;
|
export default completeAccountInformationSignup;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
interface Login2Response {
|
interface Login2Response {
|
||||||
mfaEnabled: boolean;
|
mfaEnabled: boolean;
|
||||||
encryptionVersion: number;
|
token: string;
|
||||||
|
encryptionVersion?: number;
|
||||||
protectedKey?: string;
|
protectedKey?: string;
|
||||||
protectedKeyIV?: string;
|
protectedKeyIV?: string;
|
||||||
protectedKeyTag?: string;
|
protectedKeyTag?: string;
|
||||||
token: string;
|
publicKey?: string;
|
||||||
publicKey: string;
|
encryptedPrivateKey?: string;
|
||||||
encryptedPrivateKey: string;
|
iv?: string;
|
||||||
iv: string;
|
tag?: string;
|
||||||
tag: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send new MFA token to user with email [email]
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} obj.email - email of user
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const resendMfaToken = async ({
|
||||||
|
email,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
}) => SecurityClient.fetchCall('/api/v2/auth/mfa/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email
|
||||||
|
})
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res?.status === 200) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
console.log('Failed to send new MFA code');
|
||||||
|
throw new Error('Something went wrong while sending new MFA code');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default resendMfaToken;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify MFA token [mfaToken] for user with email [email]
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} obj.email - email of user
|
||||||
|
* @param {string} obj.mfaToken - MFA cod/token to verify
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const verifyMfaToken = async ({
|
||||||
|
email,
|
||||||
|
mfaToken
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
mfaToken: string;
|
||||||
|
}) => SecurityClient.fetchCall('/api/v2/auth/mfa/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
mfaToken
|
||||||
|
})
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res?.status === 200) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
console.log('Failed to verify MFA code');
|
||||||
|
throw new Error('Something went wrong during MFA code verification');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default verifyMfaToken;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isMfaEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's MFA-enabled status to [isMfaEnabled]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Boolean} obj.isMfaEnabled - whether or not MFA status should be set to enabled or not
|
||||||
|
* @returns {User} user - user with updated MFA-enabled status
|
||||||
|
*/
|
||||||
|
const updateMyMfaEnabled = async ({
|
||||||
|
isMfaEnabled
|
||||||
|
}: Props) =>
|
||||||
|
SecurityClient.fetchCall(`/api/v2/users/me/mfa`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
isMfaEnabled,
|
||||||
|
})
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
return (await res.json()).user;
|
||||||
|
}
|
||||||
|
console.log('Failed to update MFA status');
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updateMyMfaEnabled;
|
||||||
+30
-100
@@ -4,29 +4,25 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { faWarning } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
|
|
||||||
import Button from '@app/components/basic/buttons/Button';
|
|
||||||
import Error from '@app/components/basic/Error';
|
|
||||||
import InputField from '@app/components/basic/InputField';
|
|
||||||
import ListBox from '@app/components/basic/Listbox';
|
import ListBox from '@app/components/basic/Listbox';
|
||||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
import LoginStep from '@app/components/login/LoginStep';
|
||||||
|
import MFAStep from '@app/components/login/MFAStep';
|
||||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||||
import { isLoggedIn } from '@app/reactQuery';
|
import { isLoggedIn } from '@app/reactQuery';
|
||||||
|
|
||||||
import getWorkspaces from './api/workspace/getWorkspaces';
|
import getWorkspaces from './api/workspace/getWorkspaces';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
|
const router = useRouter();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [errorLogin, setErrorLogin] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
|
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
|
||||||
const router = useRouter();
|
const [step, setStep] = useState(1);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const lang = router.locale ?? 'en';
|
const lang = router.locale ?? 'en';
|
||||||
|
|
||||||
|
|
||||||
const setLanguage = async (to: string) => {
|
const setLanguage = async (to: string) => {
|
||||||
router.push('/login', '/login', { locale: to });
|
router.push('/login', '/login', { locale: to });
|
||||||
localStorage.setItem('lang', to);
|
localStorage.setItem('lang', to);
|
||||||
@@ -50,24 +46,31 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
const renderStep = (loginStep: number) => {
|
||||||
* This function check if the user entered the correct credentials and should be allowed to log in.
|
// TODO: add MFA step
|
||||||
*/
|
switch (loginStep) {
|
||||||
const loginCheck = async () => {
|
case 1:
|
||||||
|
return (
|
||||||
// #TODO: IF 2FA IS ENABLED REDIRECT TO <2FASTEP /> AND 'return;'
|
<LoginStep
|
||||||
|
email={email}
|
||||||
if (!email || !password) {
|
setEmail={setEmail}
|
||||||
return;
|
password={password}
|
||||||
|
setPassword={setPassword}
|
||||||
|
setStep={setStep}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
// TODO: add MFA step
|
||||||
|
return (
|
||||||
|
<MFAStep
|
||||||
|
email={email}
|
||||||
|
password={password}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />
|
||||||
}
|
}
|
||||||
|
}
|
||||||
setIsLoading(true);
|
|
||||||
await attemptLogin(email, password, setErrorLogin, router, false, true).then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isAlreadyLoggedIn) {
|
if (isAlreadyLoggedIn) {
|
||||||
return null
|
return null
|
||||||
@@ -87,80 +90,7 @@ export default function Login() {
|
|||||||
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
|
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<form onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()}>
|
{renderStep(step)}
|
||||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
|
||||||
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
|
||||||
{t('login:login')}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
|
||||||
<InputField
|
|
||||||
label={t('common:email')}
|
|
||||||
onChangeHandler={setEmail}
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
autoComplete="username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
|
||||||
<InputField
|
|
||||||
label={t('common:password')}
|
|
||||||
onChangeHandler={setPassword}
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
autoComplete="current-password"
|
|
||||||
id="current-password"
|
|
||||||
/>
|
|
||||||
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
|
||||||
<Link href="/verify-email">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
|
||||||
>
|
|
||||||
{t('login:forgot-password')}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isLoading && errorLogin && <Error text={t('login:error-login') ?? ''} />}
|
|
||||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
|
||||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
text={t('login:login') ?? ''}
|
|
||||||
onButtonPressed={loginCheck}
|
|
||||||
loading={isLoading}
|
|
||||||
size="lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
{false && (
|
|
||||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
|
||||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
|
||||||
{t('common:maintenance-alert')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
|
||||||
<p className="text-sm flex justify-center text-gray-400 w-max">
|
|
||||||
{t('login:need-account')}
|
|
||||||
</p>
|
|
||||||
<Link href="/signup">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
|
||||||
>
|
|
||||||
{t('login:create-account')}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
|
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
|
||||||
<div className="w-48 mx-auto">
|
<div className="w-48 mx-auto">
|
||||||
<ListBox
|
<ListBox
|
||||||
|
|||||||
@@ -101,10 +101,7 @@ export default function PersonalSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SecuritySection
|
<SecuritySection />
|
||||||
isTwoFAEnabled
|
|
||||||
onIsTwoFAEnabledChange={(value: boolean) => console.log(value)}
|
|
||||||
/>
|
|
||||||
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-2 mb-8 pt-2">
|
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-2 mb-8 pt-2">
|
||||||
<div className="flex flex-row justify-between w-full">
|
<div className="flex flex-row justify-between w-full">
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import DownloadBackupPDF from '@app/components/signup/DonwloadBackupPDFStep';
|
|||||||
import EnterEmailStep from '@app/components/signup/EnterEmailStep';
|
import EnterEmailStep from '@app/components/signup/EnterEmailStep';
|
||||||
import TeamInviteStep from '@app/components/signup/TeamInviteStep';
|
import TeamInviteStep from '@app/components/signup/TeamInviteStep';
|
||||||
import UserInfoStep from '@app/components/signup/UserInfoStep';
|
import UserInfoStep from '@app/components/signup/UserInfoStep';
|
||||||
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||||
|
|
||||||
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
|
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
|
||||||
@@ -28,7 +29,6 @@ export default function SignUp() {
|
|||||||
const [codeError, setCodeError] = useState(false);
|
const [codeError, setCodeError] = useState(false);
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [verificationToken, setVerificationToken] = useState('');
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export default function SignUp() {
|
|||||||
// Checking if the code matches the email.
|
// Checking if the code matches the email.
|
||||||
const response = await checkEmailVerificationCode({ email, code });
|
const response = await checkEmailVerificationCode({ email, code });
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setVerificationToken((await response.json()).token);
|
SecurityClient.setToken((await response.json()).token);
|
||||||
setStep(3);
|
setStep(3);
|
||||||
} else {
|
} else {
|
||||||
setCodeError(true);
|
setCodeError(true);
|
||||||
@@ -94,7 +94,6 @@ export default function SignUp() {
|
|||||||
/>
|
/>
|
||||||
) : step === 3 ? (
|
) : step === 3 ? (
|
||||||
<UserInfoStep
|
<UserInfoStep
|
||||||
verificationToken={verificationToken}
|
|
||||||
incrementStep={incrementStep}
|
incrementStep={incrementStep}
|
||||||
email={email}
|
email={email}
|
||||||
password={password}
|
password={password}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import verifySignupInvite from './api/auth/VerifySignupInvite';
|
|||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|
||||||
export default function SignupInvite() {
|
export default function SignupInvite() {
|
||||||
|
console.log('SignupInvite');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
const [lastName, setLastName] = useState('');
|
const [lastName, setLastName] = useState('');
|
||||||
@@ -133,6 +134,7 @@ export default function SignupInvite() {
|
|||||||
secret: Buffer.from(derivedKey.hash)
|
secret: Buffer.from(derivedKey.hash)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('SignupInvite A');
|
||||||
let response = await completeAccountInformationSignupInvite({
|
let response = await completeAccountInformationSignupInvite({
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
@@ -148,17 +150,19 @@ export default function SignupInvite() {
|
|||||||
verifier: result.verifier,
|
verifier: result.verifier,
|
||||||
token: verificationToken
|
token: verificationToken
|
||||||
});
|
});
|
||||||
|
console.log('SignupInvite B');
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
// if everything works, go the main dashboard page.
|
||||||
if (!errorCheck && response.status === 200) {
|
if (!errorCheck && response.status === 200) {
|
||||||
response = await response.json();
|
response = await response.json();
|
||||||
|
|
||||||
|
console.log('SignupInvite C');
|
||||||
localStorage.setItem('publicKey', publicKey);
|
localStorage.setItem('publicKey', publicKey);
|
||||||
localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey);
|
localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey);
|
||||||
localStorage.setItem('iv', encryptedPrivateKeyIV);
|
localStorage.setItem('iv', encryptedPrivateKeyIV);
|
||||||
localStorage.setItem('tag', encryptedPrivateKeyTag);
|
localStorage.setItem('tag', encryptedPrivateKeyTag);
|
||||||
|
console.log('SignupInvite D');
|
||||||
|
|
||||||
await attemptLogin(email, password, setErrorLogin, router, false, false);
|
|
||||||
setStep(3);
|
setStep(3);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { decryptPrivateKeyHelper } from '@app/helpers/key';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle key actions
|
||||||
|
*/
|
||||||
|
class KeyService {
|
||||||
|
|
||||||
|
/** Return the user's decrypted private key
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Number} obj.encryptionVersion
|
||||||
|
* @param {String} obj.encryptedPrivateKey
|
||||||
|
* @param {String} obj.iv
|
||||||
|
* @param {String} obj.tag
|
||||||
|
* @param {String} obj.password
|
||||||
|
* @param {String} obj.salt
|
||||||
|
* @param {String} obj.protectedKey
|
||||||
|
* @param {String} obj.protectedKeyIV
|
||||||
|
* @param {String} obj.protectedKeyTag
|
||||||
|
* @returns {String} privateKey - decrypted private key
|
||||||
|
*/
|
||||||
|
static async decryptPrivateKey({
|
||||||
|
encryptionVersion,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
}: {
|
||||||
|
encryptionVersion: number;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
password: string;
|
||||||
|
salt: string;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
}) {
|
||||||
|
return decryptPrivateKeyHelper({
|
||||||
|
encryptionVersion,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyService;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { initProjectHelper } from '@app/helpers/project';
|
||||||
|
|
||||||
|
class ProjectService {
|
||||||
|
/**
|
||||||
|
* Create and initialize a new project in organization with id [organizationId]
|
||||||
|
* Note: current user should be a member of the organization
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.organizationId - id of organization
|
||||||
|
* @param {String} obj.projectName - name of new project
|
||||||
|
* @returns {Project} project - new project
|
||||||
|
*/
|
||||||
|
static async initProject({
|
||||||
|
organizationId,
|
||||||
|
projectName
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
projectName: string
|
||||||
|
}) {
|
||||||
|
return initProjectHelper({
|
||||||
|
organizationId,
|
||||||
|
projectName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectService;
|
||||||
+31
-10
@@ -1,14 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Checkbox } from '@app/components/v2';
|
import { Checkbox } from '@app/components/v2';
|
||||||
|
|
||||||
type Props = {
|
import { useGetUser } from '../../../../hooks/api';
|
||||||
isTwoFAEnabled?: boolean;
|
import { User } from '../../../../hooks/api/types';
|
||||||
onIsTwoFAEnabledChange: (state: boolean) => void;
|
import updateMyMfaEnabled from '../../../../pages/api/user/updateMyMfaEnabled';
|
||||||
};
|
|
||||||
|
export const SecuritySection = () => {
|
||||||
|
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
|
||||||
|
const { data: user } = useGetUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && typeof user.isMfaEnabled !== 'undefined') {
|
||||||
|
setIsMfaEnabled(user.isMfaEnabled);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const toggleMfa = async (state: boolean) => {
|
||||||
|
try {
|
||||||
|
const newUser: User = await updateMyMfaEnabled({
|
||||||
|
isMfaEnabled: state
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newUser) {
|
||||||
|
setIsMfaEnabled(newUser.isMfaEnabled);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const SecuritySection = ({
|
|
||||||
isTwoFAEnabled,
|
|
||||||
onIsTwoFAEnabledChange
|
|
||||||
}: Props) => {
|
|
||||||
return (
|
return (
|
||||||
<form>
|
<form>
|
||||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
|
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
|
||||||
@@ -18,9 +39,9 @@ export const SecuritySection = ({
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
className="data-[state=checked]:bg-primary"
|
className="data-[state=checked]:bg-primary"
|
||||||
id="isTwoFAEnabled"
|
id="isTwoFAEnabled"
|
||||||
isChecked={isTwoFAEnabled}
|
isChecked={isMfaEnabled}
|
||||||
onCheckedChange={(state) => {
|
onCheckedChange={(state) => {
|
||||||
onIsTwoFAEnabledChange(state as boolean);
|
toggleMfa(state as boolean);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Enable 2-factor authentication via your personal email.
|
Enable 2-factor authentication via your personal email.
|
||||||
|
|||||||
Reference in New Issue
Block a user