Merge pull request #535 from sheensantoscapadngan/feature/google-signin-signup-integration

Feature/google signin signup integration
This commit is contained in:
vmatsiiako
2023-06-02 23:44:45 -07:00
committed by GitHub
67 changed files with 7542 additions and 7797 deletions
+5
View File
@@ -9,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string
@@ -16,6 +17,7 @@ JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME=
JWT_PROVIDER_AUTH_LIFETIME=
# MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
@@ -66,3 +68,6 @@ STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
CLIENT_ID_GOOGLE=
CLIENT_SECRET_GOOGLE=
+2719 -4480
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -34,6 +34,8 @@
"mongoose": "^6.10.5",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
@@ -88,6 +90,7 @@
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
+4
View File
@@ -23,6 +23,8 @@ export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_RE
export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue;
export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue;
export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m';
export const getJwtProviderAuthSecret = async () => (await client.getSecret('JWT_PROVIDER_AUTH_SECRET')).secretValue;
export const getJwtProviderAuthLifetime = async () => (await client.getSecret('JWT_PROVIDER_AUTH_LIFETIME')).secretValue || '15m';
export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue;
export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue;
export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production';
@@ -34,12 +36,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';
@@ -267,3 +267,7 @@ export const getNewToken = async (req: Request, res: Response) => {
});
}
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}
@@ -288,6 +288,8 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
if (!user) throw new Error('Failed to find user');
await LoginSRPDetail.deleteOne({ userId: user.id })
await checkUserDevice({
user,
ip: req.ip,
@@ -0,0 +1,260 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import { issueAuthTokens, createToken, validateProviderAuthToken } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { EELogService } from '../../ee/services';
import { BadRequestError, InternalServerError } from '../../utils/errors';
import {
TOKEN_EMAIL_MFA,
ACTION_LOGIN
} from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getJwtMfaLifetime,
getJwtMfaSecret,
getHttpsEnabled,
} from '../../config';
import { AuthProvider } from '../../models/user';
declare module 'jsonwebtoken' {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthProvider;
isUserCompleted: boolean,
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
providerAuthToken,
clientPublicKey
}: {
email: string;
clientPublicKey: string,
providerAuthToken?: string;
} = req.body;
const user = await User.findOne({
email,
}).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(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({
userId: user.id,
}, {
email,
userId: user.id,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
try {
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
const { email, clientProof, providerAuthToken } = req.body;
const user = await User.findOne({
email,
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user');
if (user.authProvider) {
await validateProviderAuthToken({
email,
user,
providerAuthToken,
})
}
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ userId: user.id });
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret()
});
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: [user.email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// return (access) token in response
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,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(response);
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
};
+6 -2
View File
@@ -1,7 +1,11 @@
import * as secretsController from './secretsController';
import * as workspacesController from './workspacesController';
import * as authController from './authController';
import * as signupController from './signupController';
export {
authController,
secretsController,
workspacesController
}
signupController,
workspacesController,
}
@@ -0,0 +1,192 @@
import jwt from 'jsonwebtoken';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import {
initializeDefaultOrg
} from '../../helpers/signup';
import { issueAuthTokens, validateProviderAuthToken } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import { standardRequest } from '../../config/request';
import { getLoopsApiKey, getHttpsEnabled, getJwtSignupSecret } from '../../config';
import { BadRequestError } from '../../utils/errors';
import { TelemetryService } from '../../services';
/**
* Complete setting up user by adding their personal and auth information as part of the
* signup flow
* @param req
* @param res
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken;
try {
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName,
providerAuthToken,
attributionSource,
}: {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName: string;
providerAuthToken?: string;
attributionSource?: string;
} = req.body;
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken,
user,
});
} else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if (AUTH_TOKEN_TYPE === null) {
throw BadRequestError({ message: `Missing Authorization Header in the request header.` });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') {
throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` })
}
if (AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: 'Missing Authorization Body in the request header',
})
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret())
);
if (decodedToken.userId !== user.id) {
throw BadRequestError();
}
}
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user)
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
});
token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey())
},
});
}
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'User Signed Up',
distinctId: req.user.email,
properties: {
email,
attributionSource
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({
message: 'Successfully set up account',
user,
token
});
};
+27
View File
@@ -19,6 +19,7 @@ import {
import {
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtProviderAuthSecret,
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
@@ -317,8 +318,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,
+140 -135
View File
@@ -1,4 +1,4 @@
import dotenv from 'dotenv';
import dotenv from "dotenv";
dotenv.config();
import express from 'express';
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -16,160 +16,165 @@ const swaggerFile = require('../spec.json');
const requestIp = require('request-ip');
import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter,
organizations as eeOrganizationsRouter,
cloudProducts as eeCloudProductsRouter
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter,
organizations as eeOrganizationsRouter,
cloudProducts as eeCloudProductsRouter
} from './ee/routes/v1';
import {
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter,
secretsFolder as v1SecretsFolder
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter,
secretsFolder as v1SecretsFolder
} from './routes/v1';
import {
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import {
secrets as v3SecretsRouter,
workspaces as v3WorkspacesRouter
auth as v3AuthRouter,
secrets as v3SecretsRouter,
signup as v3SignupRouter,
workspaces as v3WorkspacesRouter
} from './routes/v3';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
import {
getNodeEnv,
getPort,
getSiteURL
getNodeEnv,
getPort,
getSiteURL
} from './config';
import { setup } from './utils/setup';
const main = async () => {
await setup();
await setup();
await EELicenseService.initGlobalFeatureSet();
await EELicenseService.initGlobalFeatureSet();
const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: await getSiteURL()
})
const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: await getSiteURL()
})
);
app.use(requestIp.mw());
if ((await getNodeEnv()) === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
app.use('/api/v1/organizations', eeOrganizationsRouter);
app.use('/api/v1/cloud-products', eeCloudProductsRouter);
// v1 routes (default)
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter); // deprecate
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v1/folder', v1SecretsFolder)
// v2 routes (improvements)
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecate
app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// v3 routes (experimental)
app.use("/api/v3/auth", v3AuthRouter);
app.use('/api/v3/secrets', v3SecretsRouter);
app.use('/api/v3/workspaces', v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
app.use(requestErrorHandler);
const server = app.listen(await getPort(), async () => {
(await getLogger("backend-main")).info(
`Server started listening at port ${await getPort()}`
);
});
app.use(requestIp.mw());
setUpHealthEndpoint(server);
if ((await getNodeEnv()) === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
server.on("close", async () => {
await DatabaseService.closeDatabase();
});
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
app.use('/api/v1/organizations', eeOrganizationsRouter);
app.use('/api/v1/cloud-products', eeCloudProductsRouter);
return server;
};
// v1 routes (default)
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter); // deprecate
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v1/folder', v1SecretsFolder)
// v2 routes (improvements)
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecate
app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// v3 routes (experimental)
app.use('/api/v3/secrets', v3SecretsRouter);
app.use('/api/v3/workspaces', v3WorkspacesRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
app.use(requestErrorHandler)
const server = app.listen(await getPort(), async () => {
(await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`)
});
// await createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on('close', async () => {
await DatabaseService.closeDatabase();
})
return server;
}
export default main();
export default main();
@@ -16,7 +16,7 @@ export const requestErrorHandler: ErrorRequestHandler = async (error: RequestErr
//* Set Sentry user identification if req.user is populated
if (req.user !== undefined && req.user !== null) {
Sentry.setUser({ email: req.user.email })
Sentry.setUser({ email: (req.user as any).email })
}
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
+4 -5
View File
@@ -42,10 +42,9 @@ const requireAuth = ({
acceptedAuthModes: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authMode, authTokenValue } = validateAuthMode({
headers: req.headers,
acceptedAuthModes
@@ -78,13 +77,13 @@ const requireAuth = ({
req.user = authPayload;
break;
}
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
@@ -92,7 +91,7 @@ const requireAuth = ({
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
}
return next();
}
}
+2 -1
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,
+5 -1
View File
@@ -5,6 +5,7 @@ export interface ILoginSRPDetail {
clientPublicKey: string;
email: string;
serverBInt: mongoose.Schema.Types.Buffer;
userId: string;
expireAt: Date;
}
@@ -16,10 +17,13 @@ const loginSRPDetailSchema = new Schema<ILoginSRPDetail>(
},
email: {
type: String,
required: true,
unique: true
},
serverBInt: { type: mongoose.Schema.Types.Buffer },
userId: {
type: String,
unique: true,
},
expireAt: { type: Date }
}
);
+15 -1
View File
@@ -1,7 +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?: AuthProvider;
email: string;
firstName?: string;
lastName?: string;
@@ -26,9 +32,17 @@ export interface IUser extends Document {
const userSchema = new Schema<IUser>(
{
authId: {
type: String,
},
authProvider: {
type: String,
enum: AuthProvider,
},
email: {
type: String,
required: true
required: true,
unique: true,
},
firstName: {
type: String
+22 -4
View File
@@ -1,6 +1,7 @@
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';
@@ -27,20 +28,37 @@ router.post( // deprecated (moved to api/v2/auth/login2)
);
router.post(
'/logout',
'/logout',
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
}),
authController.logout
);
router.post(
'/checkAuth',
'/checkAuth',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
}),
authController.checkAuth
);
router.get(
'/redirect/google',
authLimiter,
passport.authenticate('google', {
scope: ['profile', 'email'],
session: false,
}),
)
router.get(
'/callback/google',
passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }),
authController.handleAuthProviderCallback,
)
export default router;
+1 -1
View File
@@ -15,7 +15,7 @@ import password from './password';
import stripe from './stripe';
import integration from './integration';
import integrationAuth from './integrationAuth';
import secretsFolder from './secretsFolder'
import secretsFolder from './secretsFolder';
export {
signup,
+29
View File
@@ -0,0 +1,29 @@
import express from 'express';
import { body } from 'express-validator';
import { validateRequest } from '../../middleware';
import { authController } from '../../controllers/v3';
import { authLimiter } from '../../helpers/rateLimiter';
const router = express.Router();
router.post(
'/login1',
authLimiter,
body('email').isString().trim(),
body('providerAuthToken').isString().trim().optional({nullable: true}),
body('clientPublicKey').isString().trim().notEmpty(),
validateRequest,
authController.login1
);
router.post(
'/login2',
authLimiter,
body('email').isString().trim(),
body('providerAuthToken').isString().trim().optional({nullable: true}),
body('clientProof').isString().trim().notEmpty(),
validateRequest,
authController.login2
);
export default router;
+6 -2
View File
@@ -1,7 +1,11 @@
import auth from './auth';
import secrets from './secrets';
import workspaces from './workspaces';
import signup from './signup';
export {
auth,
secrets,
workspaces
}
signup,
workspaces,
}
+29
View File
@@ -0,0 +1,29 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { signupController } from '../../controllers/v3';
import { authLimiter } from '../../helpers/rateLimiter';
import { validateRequest } from '../../middleware';
router.post(
'/complete-account/signup',
authLimiter,
body('email').exists().isString().trim().notEmpty().isEmail(),
body('firstName').exists().isString().trim().notEmpty(),
body('lastName').exists().isString().trim().optional({nullable: true}),
body('protectedKey').exists().isString().trim().notEmpty(),
body('protectedKeyIV').exists().isString().trim().notEmpty(),
body('protectedKeyTag').exists().isString().trim().notEmpty(),
body('publicKey').exists().isString().trim().notEmpty(),
body('encryptedPrivateKey').exists().isString().trim().notEmpty(),
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(),
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(),
body('salt').exists().isString().trim().notEmpty(),
body('verifier').exists().isString().trim().notEmpty(),
body('organizationName').exists().isString().trim().notEmpty(),
body('providerAuthToken').isString().trim().optional({nullable: true}),
validateRequest,
signupController.completeAccountSignup,
);
export default router;
+7
View File
@@ -9,6 +9,12 @@ import {
AuthData
} from '../../interfaces/middleware';
declare module 'express' {
interface Request {
user?: any;
}
}
// TODO: fix (any) types
declare global {
namespace Express {
@@ -18,6 +24,7 @@ declare global {
workspace: any;
membership: any;
targetMembership: any;
providerAuthToken: any;
organization: any;
membershipOrg: any;
integration: any;
+88 -24
View File
@@ -1,10 +1,22 @@
import express from 'express';
import passport from 'passport';
import { AuthData } from '../interfaces/middleware';
import {
User,
ServiceAccount,
ServiceTokenData,
ServiceToken
AuthProvider,
User,
ServiceAccount,
ServiceTokenData,
} 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-oauth20').Strategy;
// TODO: find a more optimal folder structure to store these types of functions
@@ -14,17 +26,17 @@ import {
* @returns
*/
const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
}
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { serviceAccountId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { serviceAccountId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
@@ -35,20 +47,72 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
*/
const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceAccount) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
}
}
const initializePassport = async () => {
const googleClientSecret = await getClientSecretGoogle();
const googleClientId = await getClientIdGoogle();
passport.use(new GoogleStrategy({
passReqToCallback: true,
clientID: googleClientId,
clientSecret: googleClientSecret,
callbackURL: '/api/v1/auth/callback/google',
scope: ['profile', ' email'],
}, async (
req: express.Request,
accessToken: string,
refreshToken: string,
profile: any,
cb: any
) => {
try {
const email = profile.emails[0].value;
let user = await User.findOne({
authProvider: AuthProvider.GOOGLE,
authId: profile.id,
}).select('+publicKey')
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,
isUserCompleted: !!user.publicKey
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.providerAuthToken = providerAuthToken;
cb(null, profile);
} catch (err) {
cb(null, false);
}
}));
}
export {
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj
}
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj,
initializePassport,
}
+10 -8
View File
@@ -7,21 +7,22 @@ import { createTestUserForDevelopment } from '../addDevelopmentUser'
import {
validateEncryptionKeysConfig
} from './validateConfig';
import {
backfillSecretVersions,
import {
backfillSecretVersions,
backfillBots,
backfillSecretBlindIndexData,
backfillSecretBlindIndexData,
backfillEncryptionMetadata
} from './backfillData';
import {
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts
} from './reencryptData';
import {
import {
getNodeEnv,
getMongoURL,
getSentryDSN
} from '../../config';
import { initializePassport } from '../auth';
/**
* Prepare Infisical upon startup. This includes tasks like:
@@ -39,13 +40,14 @@ export const setup = async () => {
// initializing SMTP configuration
setTransporter(await initSmtp());
// initializing global feature set
await EELicenseService.initGlobalFeatureSet();
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
await initializePassport();
/**
* NOTE: the order in this setup function is critical.
* It is important to backfill data before performing any re-encryption functionality.
@@ -56,7 +58,7 @@ export const setup = async () => {
await backfillBots();
await backfillSecretBlindIndexData();
await backfillEncryptionMetadata();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
// await reencryptBotPrivateKeys();
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+6
View File
@@ -121,6 +121,12 @@
{
"group": "Self-host Infisical",
"pages": [
{
"group": "Authentication",
"pages": [
"self-hosting/authentication/google"
]
},
{
"group": "Deployment options",
"pages": [
@@ -0,0 +1,18 @@
---
title: "Google"
description: "Learn to configure Google signin/signup when self-hosting Infisical."
---
To enable Google Auth for your Infisical project, you first need to set up a Google OAuth application.
Follow Google's Setting up OAuth 2.0 documentation [here](https://support.google.com/googleapi/answer/6158849). After the setup, copy the Client ID and Client secret. You will need them below.
## Configure Redirect URI
Add the following URI to the Authorized redirect URIs section in the OAuth Client credentials page - `BASE_URL/api/v1/auth/callback/google`. Replace BASE_URL with the URL of your hosted Infisical instance.
![google redirect](../../images/authentication-google-redirect.png)
## General Configuration
You wil need to configure the following [environment variables](https://infisical.com/docs/self-hosting/configuration/envars):
- `CLIENT_ID_GOOGLE`: OAuth 2.0 Client ID obtained from the credentials page
- `CLIENT_SECRET_GOOGLE`: OAuth 2.0 Client Secret obtained from the credentials page
- `JWT_PROVIDER_AUTH_SECRET`: Secret Key for signing OAuth 2.0 access token
- `JWT_PROVIDER_AUTH_LIFETIME`: Lifetime for OAuth 2.0 access token
+17 -1
View File
@@ -69,7 +69,7 @@ Other environment variables are listed below to increase the functionality of yo
Name label to be used in From field (e.g. Team)
</ParamField>
</Tab>
<Tab title="Integrations">
<Tab title="Secret Integrations">
To sync secret to third party services, provide value for the related services
<ParamField query="CLIENT_ID_HEROKU" type="string" default="none" optional>
@@ -108,6 +108,18 @@ Other environment variables are listed below to increase the functionality of yo
OAuth2 slug for Netlify integration
</ParamField>
</Tab>
<Tab title="Auth Integrations">
To integrate with external auth providers, provide value for the related keys
<ParamField query="JWT_PROVIDER_AUTH_SECRET" type="string" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
</ParamField>
<ParamField query="CLIENT_ID_GOOGLE" type="string" default="none" optional>
OAuth2 client ID for Google auth integration
</ParamField>
<ParamField query="CLIENT_SECRET_GOOGLE" type="string" default="none" optional>
OAuth2 client secret for Google auth integration
</ParamField>
</Tab>
<Tab title="Others">
#### JWT
<ParamField query="JWT_SIGNUP_LIFETIME" type="string" default="15m" optional>
@@ -126,6 +138,10 @@ Other environment variables are listed below to increase the functionality of yo
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="JWT_PROVIDER_AUTH_LIFETIME" type="string" default="5m" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="MONGO_USERNAME" type="string" default="none" optional></ParamField>
<ParamField query="MONGO_PASSWORD" type="string" default="none" optional></ParamField>
+2832 -2685
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -62,6 +62,7 @@
"jspdf": "^2.5.1",
"jsrp": "^0.2.4",
"lottie-react": "^2.4.0",
"jwt-decode": "^3.1.2",
"markdown-it": "^13.0.1",
"next": "^12.3.4",
"posthog-js": "^1.58.0",
+9
View File
@@ -0,0 +1,9 @@
<svg width="91" height="43" viewBox="0 0 91 43" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.9734 0C24.8526 0 27.4793 0.412021 29.8535 1.23606C32.2528 2.06011 34.4123 3.13386 36.3318 4.45732C38.2766 5.75581 39.9814 7.10424 41.4463 8.50261C42.2545 9.25174 42.987 9.98839 43.6436 10.7125C44.3003 11.4367 44.9317 12.1609 45.5379 12.885C46.0935 12.1609 46.6618 11.4617 47.2427 10.7875C47.8489 10.0883 48.6066 9.32665 49.5158 8.50261C51.7132 6.38008 54.4409 4.43235 57.699 2.65941C60.9824 0.886471 64.7835 0 69.1024 0C73.1435 0 76.8183 0.97387 80.127 2.92161C83.4609 4.84437 86.1002 7.42886 88.045 10.6751C90.015 13.9213 91 17.5171 91 21.4625C91 24.4591 90.4317 27.2683 89.2952 29.8902C88.1586 32.4872 86.5927 34.7721 84.5974 36.7448C82.6021 38.6925 80.2785 40.2282 77.6266 41.3519C74.9746 42.4506 72.1332 43 69.1024 43C66.2231 43 63.5712 42.6005 61.1465 41.8014C58.7472 40.9774 56.5751 39.9286 54.6303 38.6551C52.7108 37.3815 51.0186 36.0706 49.5537 34.7221C48.7202 33.8981 47.9752 33.124 47.3185 32.3998C46.6871 31.6507 46.0935 30.9141 45.5379 30.1899C44.9065 30.9141 44.2624 31.6507 43.6057 32.3998C42.9491 33.149 42.2166 33.9231 41.4084 34.7221C39.9688 36.0706 38.2766 37.3815 36.3318 38.6551C34.4123 39.9036 32.2528 40.9399 29.8535 41.7639C27.4793 42.588 24.8526 43 21.9734 43C17.8818 43 14.1817 42.0386 10.873 40.1159C7.56439 38.1931 4.92506 35.6086 2.95504 32.3624C0.985013 29.0912 0 25.4579 0 21.4625C0 18.491 0.555648 15.7192 1.66694 13.1472C2.8035 10.5502 4.36941 8.26539 6.3647 6.29269C8.38523 4.31998 10.7215 2.78427 13.3734 1.68554C16.0507 0.561848 18.9173 0 21.9734 0ZM10.4563 21.4625C10.4563 23.5351 10.974 25.4204 12.0096 27.1185C13.0451 28.8165 14.4342 30.1649 16.1769 31.1638C17.9197 32.1626 19.8518 32.662 21.9734 32.662C24.3475 32.662 26.5322 32.1376 28.5275 31.0889C30.5228 30.0401 32.3791 28.7166 34.0966 27.1185C35.1826 26.0947 36.1171 25.1083 36.9001 24.1594C37.683 23.2105 38.365 22.3116 38.9459 21.4625C38.3397 20.6635 37.6199 19.777 36.7864 18.8031C35.9782 17.8043 35.0816 16.8554 34.0966 15.9564C32.4802 14.3833 30.649 13.0598 28.6032 11.9861C26.5575 10.8873 24.3475 10.338 21.9734 10.338C19.8518 10.338 17.9197 10.8499 16.1769 11.8737C14.4342 12.8725 13.0451 14.221 12.0096 15.919C10.974 17.592 10.4563 19.4399 10.4563 21.4625ZM80.4679 21.4625C80.4679 19.4399 79.9502 17.592 78.9146 15.919C77.9044 14.221 76.5405 12.8725 74.8231 11.8737C73.1056 10.8499 71.1987 10.338 69.1024 10.338C67.486 10.338 65.9453 10.5877 64.4804 11.0871C63.0155 11.5865 61.639 12.2607 60.351 13.1098C59.0881 13.9588 57.9263 14.9077 56.8655 15.9564C55.729 17.0052 54.7313 18.079 53.8726 19.1777C53.0139 20.2515 52.3951 21.0131 52.0162 21.4625C52.6477 22.3365 53.3549 23.248 54.1378 24.1969C54.9208 25.1208 55.83 26.0947 56.8655 27.1185C58.5577 28.7166 60.4015 30.0401 62.3968 31.0889C64.4173 32.1376 66.6525 32.662 69.1024 32.662C71.1987 32.662 73.1056 32.1626 74.8231 31.1638C76.5405 30.1649 77.9044 28.8165 78.9146 27.1185C79.9502 25.4204 80.4679 23.5351 80.4679 21.4625Z" fill="url(#paint0_linear_491_1466)"/>
<defs>
<linearGradient id="paint0_linear_491_1466" x1="46" y1="-1.40259e-06" x2="66" y2="80.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#D9E8FF"/>
<stop offset="1" stop-color="white" stop-opacity="0.36"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+8 -1
View File
@@ -131,7 +131,10 @@
"need-account": "Need an Infisical account?",
"create-account": "Create an account",
"forgot-password": "Forgot your password?",
"error-login": "Wrong credentials."
"error-login": "Wrong credentials.",
"continue-with-google": "Continue with Google",
"continue-with-email": "Continue with Email",
"other-option": "Log in with another option"
},
"mfa": {
"title": "Sign Up",
@@ -295,6 +298,10 @@
"signup": "Sign Up",
"already-have-account": "Have an account? Log in",
"forgot-password": "Forgot your password?",
"initial-title": "Create your Infisical account",
"continue-with-google": "Continue with Google",
"continue-with-email": "Continue with Email",
"create-policy": "By signing up, you agree to our Terms of Service and Privacy Policy.",
"verify": "Verify",
"step1-start": "Let's get started",
"step1-privacy": "By creating an account, you agree to our Terms and have read and acknowledged the Privacy Policy.",
+1 -17
View File
@@ -61,22 +61,6 @@ const InputField = ({
<div className="flex-col w-full">
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
<p className="text-sm font-semibold mr-1">{label}</p>
{/* {label === "Password" && router.asPath !== "/login" && (
<div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200">
<FontAwesomeIcon
icon={faCircleExclamation}
className={`text-sm peer ${
error && "text-red"
}`}
/>
<span className="absolute hidden peer-hover:block duration-200 w-60 -left-28 -top-2 -translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-200 text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
The password should contain at least 8
characters including at least 1 lowercase
character, uppercase character, number, and a
special character.
</span>
</div>
)} */}
</div>
<div
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
@@ -95,7 +79,7 @@ const InputField = ({
: ''
} ${
error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
} relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
} relative peer bg-mineshaft-900 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
name={name}
spellCheck="false"
autoComplete={autoComplete}
@@ -0,0 +1,64 @@
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import { useRouter } from 'next/router';
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button } from '../v2';
export default function InitialLoginStep({
setIsLoginWithEmail,
}: {
setIsLoginWithEmail: (value: boolean) => void;
}) {
const router = useRouter();
const { t } = useTranslation();
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open('/api/v1/auth/redirect/google')
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-14 w-full mx-0"
>
{t('login.continue-with-google')}
</Button>
</div> */}
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
setIsLoginWithEmail(true);
}}
isFullWidth
className="h-14 w-full mx-0"
>
{t('login.continue-with-email')}
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
isFullWidth
className="h-14 w-full mx-0"
>
Continue with SAML SSO
</Button>
</div>
<div className="mt-4 text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.create-account')}</span>
</Link>
</div>
</div>
}
+47 -55
View File
@@ -5,11 +5,11 @@ import { useRouter } from 'next/router';
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 { Button, Input } from '../v2';
/**
* 1st step of login - user enters their username and password
* @param {Object} obj
@@ -46,7 +46,10 @@ export default function LoginStep({
}
setIsLoading(true);
const isLoginSuccessful = await attemptLogin(email, password);
const isLoginSuccessful = await attemptLogin({
email,
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
@@ -60,82 +63,71 @@ export default function LoginStep({
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
};
}
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl">
<p className="mx-auto mb-6 flex w-max justify-center text-3xl font-semibold text-bunker-100">
<div className="w-full mx-auto h-full px-6">
<p className="text-xl w-max mx-auto flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 mb-6">
{t('login.login')}
</p>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<InputField
label={t('common.email')}
onChangeHandler={setEmail}
type="email"
<div className="flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<Input
value={email}
placeholder=""
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-12"
/>
</div>
<div className="relative mt-6 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-2 md:max-h-28 md:p-2">
<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 cursor-pointer text-sm text-primary-700 duration-200 hover:text-primary">
<Link href="/verify-email">
<button
type="button"
className="ml-1.5 text-sm font-normal text-primary-700 underline-offset-4 duration-200 hover:text-primary"
>
{t('login.forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && loginError && <Error text={t('login.error-login') ?? ''} />}
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button
type="submit"
text={t('login.login') ?? ''}
onButtonPressed={async () => handleLogin()}
loading={isLoading}
size="lg"
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12"
/>
</div>
</div>
{!isLoading && loginError && <Error text={t('login.error-login') ?? ''} />}
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] px-2 mt-4 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
onClick={async () => handleLogin()}
size="sm"
isFullWidth
className='h-14'
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
> {String(t('login.login'))} </Button>
</div>
</div>
</div>
<div className="text-bunker-400 text-sm flex flex-row w-max mx-auto">
<Link href="/verify-email">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.forgot-password')}</span>
</Link>
</div>
{false && (
<div className="mx-auto mt-4 flex w-full max-w-md flex-row items-center rounded-md bg-white/10 p-2 text-gray-300">
<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="mt-4 flex flex-row items-center justify-center md:pb-4">
<p className="flex w-max justify-center text-sm text-gray-400">{t('login.need-account')}</p>
<Link href="/signup">
<button
type="button"
className="ml-1.5 text-sm font-normal text-primary-700 underline-offset-4 duration-200 hover:text-primary"
>
{t('login.create-account')}
</button>
</Link>
</div>
</form>
);
}
+44 -23
View File
@@ -7,8 +7,8 @@ import { useRouter } from 'next/router';
import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa';
import { useSendMfaToken } from '@app/hooks/api/auth';
import Button from '../basic/buttons/Button';
import Error from '../basic/Error';
import { Button } from '../v2';
// The style for the verification code input
const props = {
@@ -52,10 +52,12 @@ interface VerifyMfaTokenError {
*/
export default function MFAStep({
email,
password
password,
providerAuthToken,
}: {
email: string;
password: string;
providerAuthToken?: string;
}): JSX.Element {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
@@ -77,6 +79,7 @@ export default function MFAStep({
const isLoginSuccessful = await attemptLoginMfa({
email,
password,
providerAuthToken,
mfaToken: mfaCode
});
@@ -112,10 +115,10 @@ export default function MFAStep({
};
return (
<form className="h-7/12 mx-auto mb-64 w-max rounded-xl bg-bunker px-8 pt-10 pb-4 drop-shadow-xl md:mb-16">
<form className="mx-auto w-max px-8 pb-4 pt-4 md:mb-16">
<p className="text-l flex justify-center text-bunker-300">{t('mfa.step2-message')}</p>
<p className="text-l my-2 flex justify-center font-semibold text-bunker-300">{email} </p>
<div className="hidden md:block">
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email} </p>
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
<ReactCodeInput
name=""
inputMode="tel"
@@ -126,28 +129,46 @@ export default function MFAStep({
className="mt-6 mb-2"
/>
</div>
<div className="block md:hidden w-max mt-4 mx-auto">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setMfaCode}
{...props}
className="mt-2 mb-2"
/>
</div>
{typeof triesLeft === 'number' && (
<Error text={`${t('mfa.step2-code-error')} ${triesLeft}`} />
)}
<div className="min-w-28 mx-auto mt-4 mb-2 flex max-h-24 max-w-max flex-col items-center justify-center px-4 text-lg md:p-2">
<Button text={t('mfa.verify') ?? ''} onButtonPressed={() => handleLoginMfa()} size="lg" />
</div>
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">{t('mfa.step2-resend-alert')}</span>
<u
className={`font-normal ${
isLoadingResend
? 'text-bunker-400'
: 'text-primary-700 duration-200 hover:text-primary'
}`}
>
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
{isLoadingResend ? t('mfa.step2-resend-progress') : t('mfa.step2-resend-submit')}
</button>
</u>
<div className="flex flex-col mt-6 items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
onClick={() => handleLoginMfa()}
size="sm"
isFullWidth
className='h-14'
colorSchema="primary"
variant="outline_bg"
> {String(t('mfa.verify'))} </Button>
</div>
<p className="pb-2 text-sm text-bunker-400">{t('mfa.step2-spam-alert')}</p>
</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-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">{t('signup.step2-resend-alert')}</span>
<div className="mt-2 text-bunker-400 text-md flex flex-row">
<button disabled={isLoading} onClick={handleResendMfaCode} type="button">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>
{isLoadingResend
? t('signup.step2-resend-progress')
: t('signup.step2-resend-submit')}
</span>
</button>
</div>
</div>
<p className="text-sm text-bunker-400 pb-2">{t('signup.step2-spam-alert')}</p>
</div>
</form>
);
@@ -0,0 +1,119 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import { useRouter } from 'next/router';
import Error from '@app/components/basic/Error';
import attemptLogin from '@app/components/utilities/attemptLogin';
import SecurityClient from '../utilities/SecurityClient';
import { Button, Input } from '../v2';
export default function PasswordInputStep({
email,
password,
providerAuthToken,
setPassword,
setProviderAuthToken,
setStep
}: {
email: string;
password: string;
providerAuthToken: string;
setPassword: (password: string) => void;
setProviderAuthToken: (value: 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 {
setIsLoading(true);
const loginAttempt = await attemptLogin({
email,
password,
providerAuthToken,
});
if (loginAttempt && loginAttempt.success) {
// case: login was successful
if (loginAttempt.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) {
setLoginError(true);
}
setIsLoading(false);
};
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="h-full mx-auto w-full max-w-md px-6 pt-8">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What’s your Infisical Password?
</p>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12"
/>
</div>
</div>
{!isLoading && loginError && <Error text={t('login.error-login') ?? ''} />}
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={async () => handleLogin()}
isFullWidth
isLoading={isLoading}
className="h-14"
>
{t('login.login')}
</Button>
</div>
<div className="text-bunker-400 text-xs flex flex-col items-center w-max mx-auto mt-4">
<span className='duration-200 max-w-sm text-center px-4'>
Infisical Master Password serves as a decryption mechanism so that even Google is not able to access your secrets.
</span>
<Link href="/verify-email">
<span className='hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.forgot-password')}</span>
</Link>
</div>
<div className="flex flex-row items-center justify-center">
<button
onClick={(e) => {
e.preventDefault();
SecurityClient.setProviderAuthToken('');
setProviderAuthToken('');
}}
type="button"
className="text-bunker-400 text-xs hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t('login.other-option')}
</button>
</div>
</div>
</form>
);
}
@@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next';
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
import Button from '../basic/buttons/Button';
import Error from '../basic/Error';
import { Button } from '../v2';
// The style for the verification code input
const props = {
@@ -51,6 +51,7 @@ interface CodeInputStepProps {
incrementStep: () => void;
setCode: (value: string) => void;
codeError: boolean;
isCodeInputCheckLoading: boolean;
}
/**
@@ -66,7 +67,8 @@ export default function CodeInputStep({
email,
incrementStep,
setCode,
codeError
codeError,
isCodeInputCheckLoading
}: CodeInputStepProps): JSX.Element {
const [isLoading, setIsLoading] = useState(false);
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
@@ -83,10 +85,10 @@ export default function CodeInputStep({
};
return (
<div className="h-7/12 mx-auto mb-64 w-max rounded-xl bg-bunker px-8 pt-10 pb-4 drop-shadow-xl md:mb-16">
<p className="text-l flex justify-center text-bunker-300">{t('signup.step2-message')}</p>
<p className="text-l my-2 flex justify-center font-semibold text-bunker-300">{email} </p>
<div className="hidden md:block">
<div className="mx-auto h-full w-full pb-4 px-8">
<p className="text-md flex justify-center text-bunker-200">{t('signup.step2-message')}</p>
<p className="text-md flex justify-center font-semibold my-1 text-bunker-200">{email} </p>
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
<ReactCodeInput
name=""
inputMode="tel"
@@ -97,7 +99,7 @@ export default function CodeInputStep({
className="mt-6 mb-2"
/>
</div>
<div className="block md:hidden">
<div className="block md:hidden w-max mt-4 mx-auto">
<ReactCodeInput
name=""
inputMode="tel"
@@ -105,31 +107,37 @@ export default function CodeInputStep({
fields={6}
onChange={setCode}
{...propsPhone}
className="mt-2 mb-6"
className="mt-2 mb-2"
/>
</div>
{codeError && <Error text={t('signup.step2-code-error')} />}
<div className="min-w-28 mx-auto mt-4 mb-2 flex max-h-24 max-w-max flex-col items-center justify-center px-4 text-lg md:p-2">
<Button text={t('signup.verify') ?? ''} onButtonPressed={incrementStep} size="lg" />
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
onClick={incrementStep}
size="sm"
isFullWidth
className='h-14'
colorSchema="primary"
variant="outline_bg"
isLoading={isCodeInputCheckLoading}
> {String(t('signup.verify'))} </Button>
</div>
</div>
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center 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">
<span className="text-bunker-400">{t('signup.step2-resend-alert')}</span>
<u
className={`font-normal ${
isResendingVerificationEmail
? 'text-bunker-400'
: 'text-primary-700 duration-200 hover:text-primary'
}`}
>
<div className="mt-2 text-bunker-400 text-md flex flex-row">
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
{isResendingVerificationEmail
? t('signup.step2-resend-progress')
: t('signup.step2-resend-submit')}
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>
{isResendingVerificationEmail
? t('signup.step2-resend-progress')
: t('signup.step2-resend-submit')}
</span>
</button>
</u>
</div>
</div>
<p className="pb-2 text-sm text-bunker-400">{t('signup.step2-spam-alert')}</p>
<p className="text-sm text-bunker-400 pb-2">{t('signup.step2-spam-alert')}</p>
</div>
</div>
);
@@ -2,8 +2,8 @@ import { useTranslation } from 'react-i18next';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '../basic/buttons/Button';
import issueBackupKey from '../utilities/cryptography/issueBackupKey';
import { Button } from '../v2';
interface DownloadBackupPDFStepProps {
incrementStep: () => void;
@@ -30,33 +30,35 @@ export default function DonwloadBackupPDFStep({
const { t } = useTranslation();
return (
<div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker py-8 px-4 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
<p className="flex justify-center text-center text-4xl font-semibold text-primary">
{t('signup.step4-message')}
<div className="flex flex-col items-center w-full h-full md:px-6 mx-auto mb-36 md:mb-16">
<p className="text-xl text-center font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />{t('signup.step4-message')}
</p>
<div className="text-md mt-4 flex w-full max-w-md flex-col items-center justify-center rounded-md px-2 text-gray-400 md:mt-8">
<div>{t('signup.step4-description1')}</div>
<div className="mt-3">{t('signup.step4-description2')}</div>
</div>
<div className="mx-auto mt-4 flex w-full max-w-xs flex-row items-center rounded-md bg-white/10 p-2 text-gray-400 md:max-w-md">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
{t('signup.step4-description3')}
</div>
<div className="mx-auto mt-4 flex max-h-24 max-w-max flex-col items-center justify-center px-2 py-3 text-lg md:px-4 md:py-5">
<Button
text="Download PDF"
onButtonPressed={async () => {
await issueBackupKey({
email,
password,
personalName: name,
setBackupKeyError: () => {},
setBackupKeyIssued: () => {}
});
incrementStep();
}}
size="lg"
/>
<div className="flex flex-col pb-2 bg-mineshaft-900 border border-mineshaft-700 items-center justify-center text-center lg:w-1/6 w-full md:min-w-[24rem] mt-8 max-w-md text-bunker-300 text-md rounded-md">
<div className="w-full mt-4 md:mt-8 flex flex-row text-center items-center m-2 text-bunker-300 rounded-md lg:w-1/6 lg:w-1/6 w-full md:min-w-[23rem] px-3 mx-auto">
<span className='mb-2'>{t('signup.step4-description1')} {t('signup.step4-description3')}</span>
</div>
<div className="flex flex-col items-center px-3 justify-center mt-0 md:mt-4 mb-2 md:mb-4 lg:w-1/6 w-full md:min-w-[20rem] mt-2 md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
onClick={async () => {
await issueBackupKey({
email,
password,
personalName: name,
setBackupKeyError: () => { },
setBackupKeyIssued: () => { }
});
incrementStep();
}}
size="sm"
isFullWidth
className='h-12'
colorSchema="primary"
variant="outline_bg"
> Download PDF </Button>
</div>
</div>
</div>
</div>
);
@@ -4,8 +4,7 @@ import Link from 'next/link';
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
import Button from '../basic/buttons/Button';
import InputField from '../basic/InputField';
import { Button, Input } from '../v2';
interface DownloadBackupPDFStepProps {
incrementStep: () => void;
@@ -27,7 +26,6 @@ export default function EnterEmailStep({
incrementStep
}: DownloadBackupPDFStepProps): JSX.Element {
const [emailError, setEmailError] = useState(false);
const [emailErrorMessage, setEmailErrorMessage] = useState('');
const { t } = useTranslation();
/**
@@ -37,11 +35,9 @@ export default function EnterEmailStep({
let emailCheckBool = false;
if (!email) {
setEmailError(true);
setEmailErrorMessage('Please enter your email.');
emailCheckBool = true;
} else if (!email.includes('@') || !email.includes('.') || !/[a-z]/.test(email)) {
setEmailError(true);
setEmailErrorMessage('Please enter a valid email.');
emailCheckBool = true;
} else {
setEmailError(false);
@@ -56,41 +52,40 @@ export default function EnterEmailStep({
return (
<div>
<div className="h-7/12 mx-1 w-full max-w-md rounded-xl bg-bunker py-8 drop-shadow-xl md:px-6">
<p className="flex justify-center text-4xl font-semibold text-primary">
<div className="w-full md:px-6 mx-auto">
<p className="text-xl font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
{t('signup.step1-start')}
</p>
<div className="m-auto mt-4 flex max-h-24 w-5/6 items-center justify-center rounded-lg md:w-full md:p-2">
<InputField
label={t('common.email') ?? ''}
onChangeHandler={setEmail}
type="email"
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] m-auto rounded-lg mt-8">
<Input
placeholder="Enter your email address..."
onChange={(e) => setEmail(e.target.value)}
value={email}
placeholder=""
isRequired
error={emailError}
errorText={emailErrorMessage}
autoComplete="username"
className="h-12"
/>
{emailError && <p className="text-red-600 text-xs text-left w-full ml-1.5 mt-1.5">Please enter a valid email.</p>}
</div>
<div className="mx-auto mt-4 flex max-h-28 w-5/6 max-w-xs flex-col items-center justify-center text-center text-sm md:mt-4 md:w-full md:max-w-md md:p-2 md:text-left">
<p className="mt-2 text-gray-400 md:mx-0.5">{t('signup.step1-privacy')}</p>
<div className="text-l m-2 mt-6 px-8 py-1 text-lg md:m-8">
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
text={t('signup.step1-submit') ?? ''}
type="submit"
onButtonPressed={emailCheck}
size="lg"
/>
onClick={emailCheck}
size="sm"
isFullWidth
className='h-14'
colorSchema="primary"
variant="outline_bg"
> {String(t('signup.step1-submit'))} </Button>
</div>
</div>
</div>
<div className="mx-auto mb-48 mt-2 flex w-full max-w-md flex-col items-center justify-center pt-2 md:mb-16 md:pb-2">
<Link href="/login">
<button type="button" className="w-max pb-3 duration-200 hover:opacity-90">
<u className="text-sm font-normal text-primary-500">
<span className="text-sm text-mineshaft-400 hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer">
{t('signup.already-have-account')}
</u>
</span>
</button>
</Link>
</div>
@@ -0,0 +1,65 @@
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import { useRouter } from 'next/router';
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button } from '../v2';
export default function InitialSignupStep({
setIsSignupWithEmail,
}: {
setIsSignupWithEmail: (value: boolean) => void
}) {
const { t } = useTranslation();
const router = useRouter();
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >{t('signup.initial-title')}</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open('/api/v1/auth/redirect/google')
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-14 w-full mx-0"
>
{t('signup.continue-with-google')}
</Button>
</div> */}
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
setIsSignupWithEmail(true);
}}
isFullWidth
className="h-14 w-full mx-0"
>
{t('signup.continue-with-email')}
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
isFullWidth
className="h-14 w-full mx-0"
>
Continue with SAML SSO
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'>
{t('signup.create-policy')}
</div>
<div className="mt-2 text-bunker-400 text-xs flex flex-row">
<Link href="/login">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('signup.already-have-account')}</span>
</Link>
</div>
</div>
}
@@ -7,8 +7,7 @@ import { usePopUp } from '@app/hooks/usePopUp';
import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
import Button from '../basic/buttons/Button';
import { EmailServiceSetupModal } from '../v2';
import { Button, EmailServiceSetupModal } from '../v2';
/**
* This is the last step of the signup flow. People can optionally invite their teammates here.
@@ -37,50 +36,56 @@ export default function TeamInviteStep(): JSX.Element {
};
return (
<div className="h-7/12 mx-auto mb-64 w-max rounded-xl bg-bunker px-8 pt-6 pb-4 drop-shadow-xl md:mb-32">
<p className="flex justify-center text-4xl font-semibold text-primary">
<div className="w-max mx-auto min-w-lg h-full pb-4 px-8 mb-64 md:mb-32">
<p className="text-2xl font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
{t('signup.step5-invite-team')}
</p>
<p className="mb-6 mt-4 flex max-w-xs justify-center text-center text-bunker-300 md:mx-8 md:max-w-sm">
<p className="text-center flex justify-center text-bunker-400 md:mx-8 mb-6 mt-4">
{t('signup.step5-subtitle')}
</p>
<div>
<div className="overflow-auto bg-bunker-800">
<div className="whitespace-pre-wrap break-words bg-transparent" />
<div className="bg-mineshaft-800 border border-mineshaft-500 w-max mx-auto pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-6">
<div>
<div className="text-bunker-300 font-medium pl-1 pb-1 text-sm">
<span>Emails</span>
</div>
<textarea
className="bg-mineshaft-900/70 min-w-[30rem] h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
value={emails}
onChange={(e) => setEmails(e.target.value)}
placeholder="[email protected], [email protected]..."
/>
</div>
<textarea
className="h-20 w-full rounded-md border border-mineshaft-500 bg-bunker-800 py-1 px-2 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 placeholder:text-bunker-400 focus:ring-2"
value={emails}
onChange={(e) => setEmails(e.target.value)}
placeholder="[email protected], [email protected]..."
<div className="flex flex-row items-end justify-end mt-0 md:mt-4 md:mb-2 w-full md:min-w-[30rem] mt-2 md:max-w-md mx-auto text-sm">
<Button
onClick={() => {
if (serverDetails?.emailConfigured) {
inviteUsers({ emails })
} else {
handlePopUpOpen('setUpEmail');
}
}}
size="sm"
// isFullWidth
className='h-10'
colorSchema="primary"
variant="solid"
> {t('signup.step5-send-invites') ?? ''} </Button>
</div>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div>
<div className="min-w-28 mx-auto mt-4 mb-2 flex max-h-24 max-w-max flex-row items-center justify-center px-4 text-lg md:p-2">
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className="text-md mx-3 cursor-pointer rounded-md bg-mineshaft-700 py-3 px-5 text-bunker-300 duration-200 hover:bg-mineshaft-500 md:py-3.5 md:text-sm"
onClick={redirectToHome}
>
{t('signup.step5-skip')}
</div>
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 min-w-[20rem] max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
<Button
text={t('signup.step5-send-invites') ?? ''}
onButtonPressed={() => {
if (serverDetails?.emailConfigured) {
inviteUsers({ emails });
} else {
handlePopUpOpen('setUpEmail');
}
}}
size="lg"
/>
onClick={redirectToHome}
size="sm"
isFullWidth
className='h-12'
colorSchema="secondary"
variant="outline"
> {t('signup.step5-skip') ?? 'Skip'} </Button>
</div>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div>
);
}
+134 -117
View File
@@ -2,7 +2,7 @@ import crypto from 'crypto';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import jsrp from 'jsrp';
import nacl from 'tweetnacl';
@@ -12,13 +12,13 @@ import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccoun
import getOrganizations from '@app/pages/api/organization/getOrgs';
import ProjectService from '@app/services/ProjectService';
import Button from '../basic/buttons/Button';
import InputField from '../basic/InputField';
import passwordCheck from '../utilities/checks/PasswordCheck';
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '../utilities/cryptography/crypto';
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
import SecurityClient from '../utilities/SecurityClient';
import { Button, Input } from '../v2';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
@@ -28,10 +28,13 @@ interface UserInfoStepProps {
email: string;
password: string;
setPassword: (value: string) => void;
firstName: string;
setFirstName: (value: string) => void;
lastName: string;
setLastName: (value: string) => void;
name: string;
setName: (value: string) => void;
organizationName: string;
setOrganizationName: (value: string) => void;
attributionSource: string;
setAttributionSource: (value: string) => void;
providerAuthToken?: string;
}
/**
@@ -52,13 +55,16 @@ export default function UserInfoStep({
email,
password,
setPassword,
firstName,
setFirstName,
lastName,
setLastName
name,
setName,
organizationName,
setOrganizationName,
attributionSource,
setAttributionSource,
providerAuthToken,
}: UserInfoStepProps): JSX.Element {
const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false);
const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
@@ -71,17 +77,17 @@ export default function UserInfoStep({
const signupErrorCheck = async () => {
setIsLoading(true);
let errorCheck = false;
if (!firstName) {
setFirstNameError(true);
if (!name) {
setNameError(true);
errorCheck = true;
} else {
setFirstNameError(false);
setNameError(false);
}
if (!lastName) {
setLastNameError(true);
if (!organizationName) {
setOrganizationNameError(true);
errorCheck = true;
} else {
setLastNameError(false);
setOrganizationNameError(false);
}
errorCheck = passwordCheck({
password,
@@ -146,8 +152,8 @@ export default function UserInfoStep({
const response = await completeAccountInformationSignup({
email,
firstName,
lastName,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(' '),
protectedKey,
protectedKeyIV,
protectedKeyTag,
@@ -155,14 +161,17 @@ export default function UserInfoStep({
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
providerAuthToken,
salt: result.salt,
verifier: result.verifier,
organizationName: `${firstName}'s organization`
organizationName,
attributionSource,
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken('');
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken('');
saveTokenToLocalStorage({
publicKey,
@@ -196,109 +205,117 @@ export default function UserInfoStep({
};
return (
<div className="h-7/12 mx-auto mb-36 w-max rounded-xl bg-bunker py-10 px-8 drop-shadow-xl md:mb-16">
<p className="mx-8 mb-6 flex justify-center text-4xl font-bold text-primary md:mx-16">
<div className="h-full mx-auto mb-36 w-max rounded-xl px-8 md:mb-16">
<p className="mx-8 mb-6 flex justify-center text-xl font-bold text-medium md:mx-16 text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
{t('signup.step3-message')}
</p>
<div className="relative z-0 flex max-h-24 w-full items-center justify-end rounded-lg md:p-2">
<InputField
label={t('common.first-name')}
onChangeHandler={setFirstName}
type="name"
value={firstName}
isRequired
errorText={
t('common.validate-required', {
name: t('common.first-name')
}) as string
}
error={firstNameError}
autoComplete="given-name"
/>
</div>
<div className="mt-2 flex max-h-24 w-full items-center justify-center rounded-lg md:p-2">
<InputField
label={t('common.last-name')}
onChangeHandler={setLastName}
type="name"
value={lastName}
isRequired
errorText={
t('common.validate-required', {
name: t('common.last-name')
}) as string
}
error={lastNameError}
autoComplete="family-name"
/>
</div>
<div className="mt-2 flex max-h-60 w-full flex-col items-center justify-center rounded-lg md:p-2">
<InputField
label={t('section.password.password')}
onChangeHandler={(pass: string) => {
setPassword(pass);
passwordCheck({
password: pass,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
});
}}
type="password"
value={password}
isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
autoComplete="new-password"
id="new-password"
/>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">{t('section.password.validate-base')}</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLength ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-length')}
<div className="h-full mx-auto mb-36 w-max rounded-xl py-6 px-8 md:mb-16 border border-mineshaft-600 bg-mineshaft-800">
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Your Name</p>
<Input
placeholder="Jane Doe"
onChange={(e) => setName(e.target.value)}
value={name}
isRequired
autoComplete="given-name"
className="h-12"
/>
{nameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your name</p>}
</div>
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p>
<Input
placeholder="Infisical"
onChange={(e) => setOrganizationName(e.target.value)}
value={organizationName}
isRequired
className="h-12"
/>
{organizationNameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your organization name</p>}
</div>
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Where did you hear about us? <span className="font-light">(optional)</span></p>
<Input
placeholder=""
onChange={(e) => setAttributionSource(e.target.value)}
value={attributionSource}
isRequired
className="h-12"
/>
</div>
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
<InputField
label={t('section.password.password')}
onChangeHandler={(pass: string) => {
setPassword(pass);
passwordCheck({
password: pass,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
});
}}
type="password"
value={password}
isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
autoComplete="new-password"
id="new-password"
/>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">{t('section.password.validate-base')}</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLength ? (
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-length')}
</div>
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
>
{t('section.password.validate-case')}
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? (
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
>
{t('section.password.validate-case')}
</div>
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-number')}
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-number')}
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
</div>
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
onClick={signupErrorCheck}
size="sm"
isFullWidth
className='h-14'
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
> {String(t('signup.signup'))} </Button>
</div>
) : (
<div className="py-2" />
)}
</div>
<div className="mx-auto flex max-h-48 max-w-max flex-col items-center justify-center px-2 py-3 text-lg md:p-2">
<Button
text={t('signup.signup') ?? ''}
loading={isLoading}
onButtonPressed={signupErrorCheck}
size="lg"
/>
</div>
</div>
</div>
);
@@ -4,8 +4,20 @@ import {
setMfaTempToken,
setSignupTempToken} from '@app/reactQuery';
export const PROVIDER_AUTH_TOKEN_KEY = 'infisical__provider-auth-token';
// depreciated: go for apiRequest module in config/api
export default class SecurityClient {
static setProviderAuthToken(tokenStr: string) {
localStorage.setItem(PROVIDER_AUTH_TOKEN_KEY, tokenStr || '')
}
static getProviderAuthToken() {
return localStorage.getItem(PROVIDER_AUTH_TOKEN_KEY);
}
static setSignupToken(tokenStr: string) {
setSignupTempToken(tokenStr);
}
@@ -26,9 +26,17 @@ interface IsLoginSuccessful {
* @param {string} password - password of user to log in
*/
const attemptLogin = async (
email: string,
password: string
{
email,
password,
providerAuthToken,
}: {
email: string;
password: string;
providerAuthToken?: string;
}
): Promise<IsLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => {
client.init(
@@ -39,7 +47,11 @@ const attemptLogin = async (
async () => {
try {
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
const { serverPublicKey, salt } = await login1({
email,
clientPublicKey,
providerAuthToken,
});
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
@@ -57,8 +69,11 @@ const attemptLogin = async (
iv,
tag
} = await login2(
email,
clientProof
{
email,
clientProof,
providerAuthToken,
}
);
if (mfaEnabled) {
@@ -80,7 +95,9 @@ const attemptLogin = async (
token
) {
// case: MFA is not enabled
// unset provider auth token in case it was used
SecurityClient.setProviderAuthToken('');
// set JWT token
SecurityClient.setToken(token);
@@ -137,4 +154,4 @@ const attemptLogin = async (
});
};
export default attemptLogin;
export default attemptLogin;
@@ -23,10 +23,12 @@ const client = new jsrp.client();
const attemptLoginMfa = async ({
email,
password,
providerAuthToken,
mfaToken
}: {
email: string;
password: string;
providerAuthToken?: string,
mfaToken: string;
}): Promise<Boolean> => {
return new Promise((resolve, reject) => {
@@ -36,7 +38,11 @@ const attemptLoginMfa = async ({
}, async () => {
try {
const clientPublicKey = client.getPublicKey();
const { salt } = await login1(email, clientPublicKey);
const { salt } = await login1({
email,
clientPublicKey,
providerAuthToken,
});
const {
encryptionVersion,
@@ -56,6 +62,7 @@ const attemptLoginMfa = async ({
// unset temporary (MFA) JWT token and set JWT token
SecurityClient.setMfaToken('');
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken('');
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
+9 -4
View File
@@ -63,6 +63,11 @@ const buttonVariants = cva(
variant: 'star',
className: 'bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100'
},
{
colorSchema: 'primary',
variant: 'solid',
className: 'bg-primary-500 bg-opacity-90 hover:bg-primary-500 hover:text-black'
},
{
colorSchema: 'primary',
variant: 'selected',
@@ -71,7 +76,7 @@ const buttonVariants = cva(
{
colorSchema: 'primary',
variant: 'outline_bg',
className: 'bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200 duration-100'
className: 'bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200'
},
{
colorSchema: 'secondary',
@@ -91,7 +96,7 @@ const buttonVariants = cva(
{
colorSchema: 'secondary',
variant: 'outline',
className: 'hover:bg-mineshaft'
className: 'border-mineshaft-700 hover:border-mineshaft-500'
},
{
colorSchema: 'danger',
@@ -172,10 +177,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
>
{isLoading && (
<img
src="/images/loading/loadingblack.gif"
src="/images/loading/loading.gif"
width={36}
alt="loading animation"
className="absolute rounded-xl"
className="absolute rounded-xl opacity-80"
/>
)}
<div
+4 -4
View File
@@ -13,7 +13,7 @@ type Props = {
};
const inputVariants = cva(
'input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
'input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-[0.05rem] hover:ring-bunker-400/60 duration-100',
{
variants: {
size: {
@@ -27,13 +27,13 @@ const inputVariants = cva(
false: ''
},
variant: {
filled: ['bg-bunker-800', 'text-gray-400'],
filled: ['bg-mineshaft-900', 'text-gray-400'],
outline: ['bg-transparent'],
plain: 'bg-transparent outline-none'
},
isError: {
true: 'focus:ring-red/50 placeholder-red-300',
false: 'focus:ring-mineshaft-400/80 duration-200 focus:ring-1'
false: 'focus:ring-primary-400/50 focus:ring-1'
}
},
compoundVariants: []
@@ -48,7 +48,7 @@ const inputParentContainerVariants = cva('inline-flex font-inter items-center bo
},
isError: {
true: 'border-red',
false: 'border-mineshaft-600'
false: 'border-mineshaft-500'
},
isFullWidth: {
true: 'w-full',
+5
View File
@@ -1,5 +1,6 @@
import axios from 'axios';
import SecurityClient from '@app/components/utilities/SecurityClient';
import {
getAuthToken,
getMfaTempToken,
@@ -16,6 +17,7 @@ apiRequest.interceptors.request.use((config) => {
const signupTempToken = getSignupTempToken();
const mfaTempToken = getMfaTempToken();
const token = getAuthToken();
const providerAuthToken = SecurityClient.getProviderAuthToken();
if (signupTempToken && config.headers) {
// eslint-disable-next-line no-param-reassign
@@ -26,6 +28,9 @@ apiRequest.interceptors.request.use((config) => {
} else if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
} else if(providerAuthToken && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${providerAuthToken}`;
}
return config;
});
+4 -1
View File
@@ -16,7 +16,10 @@ export const publicPaths = [
`/terms`,
`/subprocessors`,
`/verify-email`,
`/password-reset`
`/password-reset`,
`/saml-sso`,
`/login/provider/success`,
`/login/provider/error`
];
export const languageMap = {
+99
View File
@@ -0,0 +1,99 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import jwt_decode from 'jwt-decode';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import SecurityClient, { PROVIDER_AUTH_TOKEN_KEY } from '@app/components/utilities/SecurityClient';
export const useProviderAuth = () => {
const router = useRouter();
const { providerAuthToken: redirectedProviderAuthToken } = router.query;
const [email, setEmail] = useState<string>('');
const [userId, setUserId] = useState<string>('');
const [providerAuthToken, setProviderAuthToken] = useState<string>(
redirectedProviderAuthToken as string || ''
);
const [isProviderUserCompleted, setIsProviderUserCompleted] = useState<boolean>();
const { createNotification } = useNotificationContext();
const AUTH_ERROR_KEY = 'PROVIDER_AUTH_ERROR'
const handleRedirectWithToken = () => {
if (providerAuthToken) {
const {
userId: resultUserId,
email: resultEmail,
isUserCompleted: resultIsUserCompleted,
} = jwt_decode(providerAuthToken) as any;
setEmail(resultEmail);
setUserId(resultUserId);
setIsProviderUserCompleted(resultIsUserCompleted);
}
}
useEffect(() => {
handleRedirectWithToken();
// reset when there is no redirect auth token
if (!providerAuthToken) {
SecurityClient.setProviderAuthToken('');
}
window.localStorage.removeItem(AUTH_ERROR_KEY);
const handleStorageChange = (event: StorageEvent) => {
if (event.storageArea !== localStorage) {
return;
}
if (event.key === PROVIDER_AUTH_TOKEN_KEY) {
if (event.newValue) {
const token = event.newValue;
const {
userId: resultUserId,
email: resultEmail,
isUserCompleted: resultIsUserCompleted,
} = jwt_decode(token) as any;
setIsProviderUserCompleted(resultIsUserCompleted);
setProviderAuthToken(token);
setEmail(resultEmail);
setUserId(resultUserId);
} else {
setProviderAuthToken('');
setEmail('');
setUserId('');
setIsProviderUserCompleted(false);
}
setProviderAuthToken(event.newValue || '');
}
if (event.key === AUTH_ERROR_KEY) {
if (event.newValue) {
createNotification({
text: 'An error has occured during login.',
type: 'error',
timeoutMs: 6000,
})
window.localStorage.removeItem(AUTH_ERROR_KEY);
}
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
return {
email,
isProviderUserCompleted,
providerAuthToken,
userId,
setEmail,
setProviderAuthToken,
setUserId,
};
};
+5 -3
View File
@@ -57,9 +57,11 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
) {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
<NotificationProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</NotificationProvider>
</QueryClientProvider>
);
}
@@ -8,6 +8,7 @@ interface Props {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
providerAuthToken?: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
@@ -15,6 +16,7 @@ interface Props {
organizationName: string;
salt: string;
verifier: string;
attributionSource?: string;
}
/**
@@ -49,9 +51,11 @@ const completeAccountInformationSignup = async ({
encryptedPrivateKeyTag,
salt,
verifier,
organizationName
organizationName,
providerAuthToken,
attributionSource
}: Props) => {
const { data } = await apiRequest.post('/api/v2/signup/complete-account/signup', {
const { data } = await apiRequest.post('/api/v3/signup/complete-account/signup', {
email,
firstName,
lastName,
@@ -64,7 +68,9 @@ const completeAccountInformationSignup = async ({
encryptedPrivateKeyTag,
salt,
verifier,
organizationName
organizationName,
providerAuthToken,
attributionSource,
});
return data;
+7 -6
View File
@@ -9,16 +9,17 @@ interface Login1 {
* @param {*} clientPublicKey
* @returns
*/
const login1 = async (email: string, clientPublicKey: string) => {
const response = await fetch("/api/v2/auth/login1", {
const login1 = async (loginDetails: {
email: string;
clientPublicKey: string;
providerAuthToken?: string;
}) => {
const response = await fetch("/api/v3/auth/login1", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
clientPublicKey,
}),
body: JSON.stringify(loginDetails),
});
// need precise error handling about the status code
if (response?.status === 200) {
+7 -6
View File
@@ -17,16 +17,17 @@ interface Login2Response {
* @param {*} clientPublicKey
* @returns
*/
const login2 = async (email: string, clientProof: string) => {
const response = await fetch('/api/v2/auth/login2', {
const login2 = async (loginDetails: {
email: string;
clientProof: string;
providerAuthToken?: string;
}) => {
const response = await fetch('/api/v3/auth/login2', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
clientProof
}),
body: JSON.stringify(loginDetails),
credentials: 'include'
});
// need precise error handling about the status code
+66 -30
View File
@@ -5,9 +5,12 @@ import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import ListBox from '@app/components/basic/Listbox';
// import ListBox from '@app/components/basic/Listbox';
import InitialLoginStep from '@app/components/login/InitialLoginStep';
import LoginStep from '@app/components/login/LoginStep';
import MFAStep from '@app/components/login/MFAStep';
import PasswordInputStep from '@app/components/login/PasswordInputStep';
import { useProviderAuth } from '@app/hooks/useProviderAuth';
import { isLoggedIn } from '@app/reactQuery';
import getWorkspaces from './api/workspace/getWorkspaces';
@@ -18,12 +21,23 @@ export default function Login() {
const [password, setPassword] = useState('');
const [step, setStep] = useState(1);
const { t } = useTranslation();
const lang = router.locale ?? 'en';
// const lang = router.locale ?? 'en';
const [isLoginWithEmail, setIsLoginWithEmail] = useState(false);
const {
providerAuthToken,
email: providerEmail,
setProviderAuthToken,
isProviderUserCompleted
} = useProviderAuth();
const setLanguage = async (to: string) => {
router.push('/login', '/login', { locale: to });
localStorage.setItem('lang', to);
};
if (providerAuthToken && isProviderUserCompleted === false) {
router.push(`/signup?providerAuthToken=${encodeURIComponent(providerAuthToken)}`);
}
// const setLanguage = async (to: string) => {
// router.push('/login', '/login', { locale: to });
// localStorage.setItem('lang', to);
// };
useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
@@ -42,29 +56,51 @@ export default function Login() {
}
}, []);
const renderStep = (loginStep: number) => {
// TODO: add MFA step
switch (loginStep) {
case 1:
return (
<LoginStep
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
setStep={setStep}
/>
);
case 2:
// TODO: add MFA step
return <MFAStep email={email} password={password} />;
default:
return <div />;
const renderView = (loginStep: number) => {
if (providerAuthToken && step === 1) {
return (
<PasswordInputStep
email={providerEmail}
password={password}
providerAuthToken={providerAuthToken}
setPassword={setPassword}
setProviderAuthToken={setProviderAuthToken}
setStep={setStep}
/>
);
}
if (isLoginWithEmail && loginStep === 1) {
return (
<LoginStep
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
setStep={setStep}
/>
);
}
if (!isLoginWithEmail && loginStep === 1) {
return <InitialLoginStep setIsLoginWithEmail={setIsLoginWithEmail} />;
}
if (step === 2) {
return (
<MFAStep
email={email || providerEmail}
password={password}
providerAuthToken={providerAuthToken}
/>
);
}
return <div />;
};
return (
<div className="flex h-screen flex-col justify-start bg-bunker-800 px-6">
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t('common.head-title', { title: t('login.title') })}</title>
<link rel="icon" href="/infisical.ico" />
@@ -73,12 +109,12 @@ export default function Login() {
<meta name="og:description" content={t('login.og-description') ?? ''} />
</Head>
<Link href="/">
<div className="mb-8 mt-20 flex cursor-pointer justify-center">
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
{renderStep(step)}
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
{renderView(step)}
{/* <div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
<div className="mx-auto w-48">
<ListBox
isSelected={lang}
@@ -88,7 +124,7 @@ export default function Login() {
text={`${t('common.language')}: `}
/>
</div>
</div>
</div> */}
</div>
);
}
@@ -0,0 +1,10 @@
import { useEffect } from "react";
export default function LoginProviderError() {
useEffect(() => {
window.localStorage.setItem('PROVIDER_AUTH_ERROR', 'err');
window.close();
}, [])
return <div />
}
@@ -0,0 +1,16 @@
import { useEffect } from "react";
import { useRouter } from "next/router"
import SecurityClient from '@app/components/utilities/SecurityClient';
export default function LoginProviderSuccess() {
const router = useRouter();
useEffect(() => {
const { token } = router.query;
SecurityClient.setProviderAuthToken(token as string);
window.close();
}, [])
return <div />
}
+90
View File
@@ -0,0 +1,90 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Button, Input } from '@app/components/v2';
import { isLoggedIn } from '@app/reactQuery';
import getWorkspaces from './api/workspace/getWorkspaces';
export default function Login() {
const router = useRouter();
const [password, setPassword] = useState('');
const { t } = useTranslation();
useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
let userWorkspace;
try {
const userWorkspaces = await getWorkspaces();
userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
router.push(`/dashboard/${userWorkspace}`);
} catch (error) {
console.log('Error - Not logged in yet');
}
};
if (isLoggedIn()) {
redirectToDashboard();
}
}, []);
return (
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t('common.head-title', { title: t('login.title') })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t('login.og-title') ?? ''} />
<meta name="og:description" content={t('login.og-description') ?? ''} />
</Head>
<Link href="/">
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<div className="mx-auto w-full max-w-md px-6">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What’s your email?
</p>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your email..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {}}
isFullWidth
className="h-14"
>
{t('login.login')}
</Button>
</div>
<div className="flex flex-row items-center justify-center mt-4">
<button
onClick={() => {router.push('/login')}}
type="button"
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t('login.other-option')}
</button>
</div>
</div>
</div>
);
}
+79 -45
View File
@@ -3,16 +3,17 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import CodeInputStep from '@app/components/signup/CodeInputStep';
import DownloadBackupPDF from '@app/components/signup/DonwloadBackupPDFStep';
import EnterEmailStep from '@app/components/signup/EnterEmailStep';
import InitialSignupStep from '@app/components/signup/InitialSignupStep';
import TeamInviteStep from '@app/components/signup/TeamInviteStep';
import UserInfoStep from '@app/components/signup/UserInfoStep';
import SecurityClient from '@app/components/utilities/SecurityClient';
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';
@@ -23,15 +24,26 @@ import getWorkspaces from './api/workspace/getWorkspaces';
export default function SignUp() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [name, setName] = useState('');
const [organizationName, setOrganizationName] = useState('');
const [attributionSource, setAttributionSource] = useState('');
const [code, setCode] = useState('123456');
const [codeError, setCodeError] = useState(false);
const [step, setStep] = useState(1);
const router = useRouter();
const { data: serverDetails } = useFetchServerStatus();
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
const { t } = useTranslation();
const { email: providerEmail, providerAuthToken, isProviderUserCompleted } = useProviderAuth();
if (providerAuthToken && isProviderUserCompleted) {
router.push(`/login?providerAuthToken=${encodeURIComponent(providerAuthToken)}`);
}
if (providerAuthToken && step < 3) {
setStep(3);
}
useEffect(() => {
const tryAuth = async () => {
@@ -57,6 +69,7 @@ export default function SignUp() {
if (step === 1 || step === 3 || step === 4) {
setStep(step + 1);
} else if (step === 2) {
setIsCodeInputCheckLoading(true);
// Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200) {
@@ -66,6 +79,7 @@ export default function SignUp() {
} else {
setCodeError(true);
}
setIsCodeInputCheckLoading(false);
}
};
@@ -82,8 +96,65 @@ export default function SignUp() {
}
}, [step]);
const renderView = (registerStep: number) => {
if (isSignupWithEmail && registerStep === 1) {
return <EnterEmailStep email={email} setEmail={setEmail} incrementStep={incrementStep} />;
}
if (!isSignupWithEmail && registerStep === 1) {
return <InitialSignupStep setIsSignupWithEmail={setIsSignupWithEmail} />;
}
if (registerStep === 2) {
return (
<CodeInputStep
email={email}
incrementStep={incrementStep}
setCode={setCode}
codeError={codeError}
isCodeInputCheckLoading={isCodeInputCheckLoading}
/>
);
}
if (registerStep === 3) {
return (
<UserInfoStep
incrementStep={incrementStep}
email={email || providerEmail}
password={password}
setPassword={setPassword}
name={name}
setName={setName}
organizationName={organizationName}
setOrganizationName={setOrganizationName}
attributionSource={attributionSource}
setAttributionSource={setAttributionSource}
providerAuthToken={providerAuthToken}
/>
);
}
if (registerStep === 4) {
return (
<DownloadBackupPDF
incrementStep={incrementStep}
email={email || providerEmail}
password={password}
name={name}
/>
);
}
if (serverDetails?.emailConfigured) {
return <TeamInviteStep />;
}
return '';
};
return (
<div className="flex h-screen flex-col items-center justify-center bg-bunker-800">
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t('common.head-title', { title: t('signup.title') })}</title>
<link rel="icon" href="/infisical.ico" />
@@ -91,47 +162,10 @@ export default function SignUp() {
<meta property="og:title" content={t('signup.og-title') as string} />
<meta name="og:description" content={t('signup.og-description') as string} />
</Head>
<div className="flex flex-col items-center justify-center">
<Link href="/">
<div className="mb-2 flex cursor-pointer justify-center md:mb-8">
<Image src="/images/biglogo.png" height={90} width={120} alt="Infisical Wide Logo" />
</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 />
) : (
''
)}
</form>
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
</div>
<form onSubmit={(e) => e.preventDefault()}>{renderView(step)}</form>
</div>
);
}
+9 -9
View File
@@ -180,7 +180,7 @@ export default function SignupInvite() {
// Step 4 of the sign up process (download the emergency kit pdf)
const stepConfirmEmail = (
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<div className="border border-mineshaft-600 bg-mineshaft-800 flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold mb-6 flex justify-center text-primary-100">
Confirm your email
</p>
@@ -219,8 +219,8 @@ export default function SignupInvite() {
// Because this is the invite signup - we directly go to the last step of signup (email is already verified)
const main = (
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-32 md:mb-16">
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
<div className="border border-mineshaft-600 bg-mineshaft-800 w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-32 md:mb-16">
<p className="text-4xl font-bold flex justify-center mb-6 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-tr from-mineshaft-300 to-white">
Almost there!
</p>
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
@@ -322,8 +322,8 @@ export default function SignupInvite() {
// Step 4 of the sign up process (download the emergency kit pdf)
const step4 = (
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
<div className="border border-mineshaft-600 bg-mineshaft-800 flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 pt-8 pb-6 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-white to-mineshaft-300">
Save your Emergency Kit
</p>
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
@@ -336,7 +336,7 @@ export default function SignupInvite() {
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
It contains your Secret Key which we cannot access or recover for you if you lose it.
</div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-4 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<Button
text="Download PDF"
onButtonPressed={async () => {
@@ -368,14 +368,14 @@ export default function SignupInvite() {
);
return (
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
<div className="bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 h-screen flex flex-col items-center justify-center">
<Head>
<title>Sign Up</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Link href="/">
<div className="flex justify-center mb-2 md:mb-4 opacity-80 cursor-pointer">
<Image src="/images/biglogo.png" height={90} width={120} alt="Infisical Wide Logo" />
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
</div>
</Link>
{step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
@@ -571,6 +571,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
onClick={handleSubmit(onSaveSecret)}
className="h-10"
color="primary"
variant="star"
>
{isRollbackMode ? 'Rollback' : 'Save Changes'}
</Button>
@@ -54,7 +54,8 @@ export const OrgNameChangeSection = ({ onOrgNameChange, orgName }: Props): JSX.E
</div>
<Button
isLoading={isSubmitting}
color="mineshaft"
color="primary"
variant="outline_bg"
size="sm"
type="submit"
isDisabled={!isDirty || isSubmitting}
@@ -57,7 +57,8 @@ export const ProjectNameChangeSection = ({
</div>
<Button
isLoading={isSubmitting}
color="mineshaft"
color="primary"
variant="outline_bg"
size="sm"
type="submit"
isDisabled={!isDirty || isSubmitting}
@@ -82,6 +82,7 @@ stringData:
"JWT_AUTH_SECRET" (randAlphaNum 32 | lower)
"JWT_SERVICE_SECRET" (randAlphaNum 32 | lower)
"JWT_MFA_SECRET" (randAlphaNum 32 | lower)
"JWT_PROVIDER_AUTH_SECRET" (randAlphaNum 32 | lower)
"MONGO_URL" (include "infisical.mongodb.connectionString" .) }}
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (include "infisical.backend.fullname" .)) | default dict }}
{{- $secretData := (get $secretObj "data") | default dict }}
+2
View File
@@ -131,6 +131,7 @@ backendEnvironmentVariables:
## @param backendEnvironmentVariables.JWT_AUTH_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))</br><kbd>auto-generated</kbd> variable (if not provided, and not found in an existing secret)
## @param backendEnvironmentVariables.JWT_SERVICE_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))</br><kbd>auto-generated</kbd> variable (if not provided, and not found in an existing secret)
## @param backendEnvironmentVariables.JWT_MFA_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))</br><kbd>auto-generated</kbd> variable (if not provided, and not found in an existing secret)
## @param backendEnvironmentVariables.JWT_PROVIDER_AUTH_SECRET **Required** Secrets to sign JWT OAuth tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))</br><kbd>auto-generated</kbd> variable (if not provided, and not found in an existing secret)
## Command to generate the required value (linux) : 'hexdump -vn16 -e'4/4 "%08X" 1 "\n"' /dev/urandom', 'openssl rand -hex 16'
##
JWT_SIGNUP_SECRET: ""
@@ -138,6 +139,7 @@ backendEnvironmentVariables:
JWT_AUTH_SECRET: ""
JWT_SERVICE_SECRET: ""
JWT_MFA_SECRET: ""
JWT_PROVIDER_AUTH_SECRET: ""
## @param backendEnvironmentVariables.SMTP_HOST **Required** Hostname to connect to for establishing SMTP connections
## @param backendEnvironmentVariables.SMTP_PORT Port to connect to for establishing SMTP connections
## @param backendEnvironmentVariables.SMTP_SECURE If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
+2
View File
@@ -17,6 +17,8 @@ services:
generateValue: true
- key: JWT_AUTH_SECRET
generateValue: true
- key: JWT_PROVIDER_AUTH_SECRET
generateValue: true
- key: MONGO_URL
sync: false
- key: PORT