mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'issue-318' of https://github.com/esau-morais/infisical into issue-318
This commit is contained in:
86
.github/values.yaml
vendored
86
.github/values.yaml
vendored
@@ -1,11 +1,5 @@
|
||||
#####
|
||||
# INFISICAL K8 DEFAULT VALUES FILE
|
||||
# PLEASE REPLACE VALUES/EDIT AS REQUIRED
|
||||
#####
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
frontend:
|
||||
enabled: true
|
||||
name: frontend
|
||||
podAnnotations: {}
|
||||
deploymentAnnotations:
|
||||
@@ -13,17 +7,18 @@ frontend:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/frontend
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
kubeSecretRef: managed-secret-frontend
|
||||
service:
|
||||
# type of the frontend service
|
||||
type: ClusterIP
|
||||
# define the nodePort if service type is NodePort
|
||||
# nodePort:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
|
||||
frontendEnvironmentVariables: null
|
||||
|
||||
backend:
|
||||
enabled: true
|
||||
name: backend
|
||||
podAnnotations: {}
|
||||
deploymentAnnotations:
|
||||
@@ -31,63 +26,46 @@ backend:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/backend
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
kubeSecretRef: managed-backend-secret
|
||||
service:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
|
||||
backendEnvironmentVariables: null
|
||||
|
||||
## Mongo DB persistence
|
||||
mongodb:
|
||||
name: mongodb
|
||||
podAnnotations: {}
|
||||
image:
|
||||
repository: mongo
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
service:
|
||||
annotations: {}
|
||||
enabled: true
|
||||
persistence:
|
||||
enabled: false
|
||||
|
||||
# By default the backend will be connected to a Mongo instance in the cluster.
|
||||
# However, it is recommended to add a managed document DB connection string because the DB instance in the cluster does not have persistence yet ( data will be deleted on next deploy).
|
||||
# Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
|
||||
mongodbConnection: {}
|
||||
# externalMongoDBConnectionString: <>
|
||||
## By default the backend will be connected to a Mongo instance within the cluster
|
||||
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
|
||||
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
|
||||
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
|
||||
mongodbConnection:
|
||||
externalMongoDBConnectionString: ""
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
hostName: gamma.infisical.com # replace with your domain
|
||||
frontend:
|
||||
# cert-manager.io/issuer: letsencrypt-nginx
|
||||
hostName: gamma.infisical.com ## <- Replace with your own domain
|
||||
frontend:
|
||||
path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
path: /api
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
tls:
|
||||
[]
|
||||
# - secretName: letsencrypt-nginx
|
||||
# hosts:
|
||||
# - infisical.local
|
||||
|
||||
|
||||
## Complete Ingress example
|
||||
# ingress:
|
||||
# enabled: true
|
||||
# annotations:
|
||||
# kubernetes.io/ingress.class: "nginx"
|
||||
# cert-manager.io/issuer: letsencrypt-nginx
|
||||
# hostName: k8.infisical.com
|
||||
# frontend:
|
||||
# path: /
|
||||
# pathType: Prefix
|
||||
# backend:
|
||||
# path: /api
|
||||
# pathType: Prefix
|
||||
# tls:
|
||||
# - secretName: letsencrypt-nginx
|
||||
# hosts:
|
||||
# - k8.infisical.com
|
||||
|
||||
###
|
||||
### YOU MUST FILL IN ALL SECRETS BELOW
|
||||
###
|
||||
backendEnvironmentVariables: {}
|
||||
|
||||
frontendEnvironmentVariables: {}
|
||||
mailhog:
|
||||
enabled: false
|
||||
|
||||
34
README.md
34
README.md
@@ -55,7 +55,7 @@
|
||||
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
|
||||
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** for rolling back to any snapshot of your secrets
|
||||
- **Role-based Access Controls** per environment
|
||||
- 🔜 **2FA** (next week)
|
||||
- **2FA** (more options coming soon)
|
||||
- 🔜 **1-Click Deploy** to AWS
|
||||
- 🔜 **Automatic Secret Rotation**
|
||||
- 🔜 **Smart Security Alerts**
|
||||
@@ -83,22 +83,6 @@ If you care about efficiency and security, then Infisical is right for you.
|
||||
|
||||
We are currently working hard to make Infisical more extensive. Need any integrations or want a new feature? Feel free to [create an issue](https://github.com/Infisical/infisical/issues) or [contribute](https://infisical.com/docs/contributing/overview) directly to the repository.
|
||||
|
||||
## 🌱 Contributing
|
||||
|
||||
Whether it's big or small, we love contributions ❤️ Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview).
|
||||
|
||||
Not sure where to get started? You can:
|
||||
|
||||
- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
|
||||
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a>, and ask us any questions there.
|
||||
|
||||
## 💚 Community & Support
|
||||
|
||||
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) (For live discussion with the community and the Infisical team)
|
||||
- [GitHub Discussions](https://github.com/Infisical/infisical/discussions) (For help with building and deeper conversations about features)
|
||||
- [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) (For any bugs and errors you encounter using Infisical)
|
||||
- [Twitter](https://twitter.com/infisical) (Get news fast)
|
||||
|
||||
## 🔌 Integrations
|
||||
|
||||
We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :)
|
||||
@@ -334,6 +318,13 @@ We're currently setting the foundation and building [integrations](https://infis
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 💚 Community & Support
|
||||
|
||||
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) (For live discussion with the community and the Infisical team)
|
||||
- [GitHub Discussions](https://github.com/Infisical/infisical/discussions) (For help with building and deeper conversations about features)
|
||||
- [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) (For any bugs and errors you encounter using Infisical)
|
||||
- [Twitter](https://twitter.com/infisical) (Get news fast)
|
||||
|
||||
## 🏘 Open-source vs. paid
|
||||
|
||||
This repo is entirely MIT licensed, with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license in the future. We're currently focused on developing non-enterprise offerings first that should suit most use-cases.
|
||||
@@ -348,6 +339,15 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o
|
||||
|
||||

|
||||
|
||||
## 🌱 Contributing
|
||||
|
||||
Whether it's big or small, we love contributions ❤️ Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview).
|
||||
|
||||
Not sure where to get started? You can:
|
||||
|
||||
- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
|
||||
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a>, and ask us any questions there.
|
||||
|
||||
## 🦸 Contributors
|
||||
|
||||
[//]: contributor-faces
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import cookieParser from 'cookie-parser';
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
integrationAuth as v1IntegrationAuthRouter
|
||||
} from './routes/v1';
|
||||
import {
|
||||
signup as v2SignupRouter,
|
||||
auth as v2AuthRouter,
|
||||
users as v2UsersRouter,
|
||||
organizations as v2OrganizationsRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
@@ -110,6 +112,8 @@ app.use('/api/v1/integration', v1IntegrationRouter);
|
||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||
|
||||
// v2 routes
|
||||
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);
|
||||
|
||||
@@ -5,6 +5,8 @@ const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
|
||||
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
|
||||
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
||||
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
|
||||
const JWT_MFA_LIFETIME = process.env.JWT_MFA_LIFETIME! || '5m';
|
||||
const JWT_MFA_SECRET = process.env.JWT_MFA_SECRET!;
|
||||
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
|
||||
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
|
||||
const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!;
|
||||
@@ -56,6 +58,8 @@ export {
|
||||
SALT_ROUNDS,
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_MFA_LIFETIME,
|
||||
JWT_MFA_SECRET,
|
||||
JWT_REFRESH_LIFETIME,
|
||||
JWT_REFRESH_SECRET,
|
||||
JWT_SERVICE_SECRET,
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node';
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
const jsrp = require('jsrp');
|
||||
import { User, LoginSRPDetail } from '../../models';
|
||||
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
|
||||
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
|
||||
import {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT
|
||||
@@ -111,7 +111,7 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// issue tokens
|
||||
const tokens = await issueTokens({ userId: user._id.toString() });
|
||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, EMAIL_TOKEN_LIFETIME } from '../../config';
|
||||
import { MembershipOrg, Organization, User, Token } from '../../models';
|
||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||
import { MembershipOrg, Organization, User } from '../../models';
|
||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
||||
import { checkEmailVerification } from '../../helpers/signup';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
|
||||
import { TokenService } from '../../services';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
|
||||
|
||||
/**
|
||||
* Delete organization membership with id [membershipOrgId] from organization
|
||||
@@ -163,18 +162,11 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
const organization = await Organization.findOne({ _id: organizationId });
|
||||
|
||||
if (organization) {
|
||||
const token = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
await Token.findOneAndUpdate(
|
||||
{ email: inviteeEmail },
|
||||
{
|
||||
email: inviteeEmail,
|
||||
token,
|
||||
createdAt: new Date(),
|
||||
ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email: inviteeEmail,
|
||||
organizationId: organization._id
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
template: 'organizationInvitation.handlebars',
|
||||
@@ -226,10 +218,12 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
|
||||
if (!membershipOrg)
|
||||
throw new Error('Failed to find any invitations for email');
|
||||
|
||||
await checkEmailVerification({
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email,
|
||||
code
|
||||
organizationId: membershipOrg.organization,
|
||||
token: code
|
||||
});
|
||||
|
||||
if (user && user?.publicKey) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require('jsrp');
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
import { User, Token, BackupPrivateKey, LoginSRPDetail } from '../../models';
|
||||
import { checkEmailVerification } from '../../helpers/signup';
|
||||
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { EMAIL_TOKEN_LIFETIME, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
|
||||
import { TokenService } from '../../services';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
|
||||
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
|
||||
/**
|
||||
@@ -31,20 +31,12 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
error: 'Failed to send email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
await Token.findOneAndUpdate(
|
||||
{ email },
|
||||
{
|
||||
email,
|
||||
token,
|
||||
createdAt: new Date(),
|
||||
ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||
email
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
template: 'passwordReset.handlebars',
|
||||
subjectLine: 'Infisical password reset',
|
||||
@@ -55,7 +47,6 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
callback_url: SITE_URL + '/password-reset'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@@ -88,10 +79,11 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
error: 'Failed email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
await checkEmailVerification({
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||
email,
|
||||
code
|
||||
token: code
|
||||
});
|
||||
|
||||
// generate temporary password-reset token
|
||||
@@ -174,8 +166,18 @@ export const srp1 = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const changePassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
||||
req.body;
|
||||
const {
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
} = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select('+salt +verifier');
|
||||
@@ -205,9 +207,13 @@ export const changePassword = async (req: Request, res: Response) => {
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
@@ -341,9 +347,12 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
export const resetPassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
} = req.body;
|
||||
@@ -351,9 +360,13 @@ export const resetPassword = async (req: Request, res: Response) => {
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config';
|
||||
import { User, MembershipOrg } from '../../models';
|
||||
import { completeAccount } from '../../helpers/user';
|
||||
import { User } from '../../models';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config';
|
||||
import {
|
||||
sendEmailVerification,
|
||||
checkEmailVerification,
|
||||
initializeDefaultOrg
|
||||
} from '../../helpers/signup';
|
||||
import { issueTokens, createToken } from '../../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../../variables';
|
||||
import axios from 'axios';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
|
||||
/**
|
||||
@@ -112,201 +108,3 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||
token
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
organizationName
|
||||
} = req.body;
|
||||
|
||||
// get user
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// complete setting up user's account
|
||||
user = await completeAccount({
|
||||
userId: user._id.toString(),
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
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 issueTokens({
|
||||
userId: user._id.toString()
|
||||
});
|
||||
|
||||
token = tokens.token;
|
||||
refreshToken = tokens.refreshToken;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (process.env.LOOPS_API_KEY) {
|
||||
await axios.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
"lastName": lastName
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
|
||||
},
|
||||
});
|
||||
}
|
||||
} 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,
|
||||
refreshToken
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Complete setting up user by adding their personal and auth information as part of the
|
||||
* invite flow
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
let user, token, refreshToken;
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
} = req.body;
|
||||
|
||||
// get user
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
||||
|
||||
// complete setting up user's account
|
||||
user = await completeAccount({
|
||||
userId: user._id.toString(),
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
});
|
||||
|
||||
if (!user)
|
||||
throw new Error('Failed to complete account for non-existent 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 issueTokens({
|
||||
userId: user._id.toString()
|
||||
});
|
||||
|
||||
token = tokens.token;
|
||||
refreshToken = tokens.refreshToken;
|
||||
} 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,
|
||||
refreshToken
|
||||
});
|
||||
};
|
||||
|
||||
309
backend/src/controllers/v2/authController.ts
Normal file
309
backend/src/controllers/v2/authController.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
/* 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 } from '../../helpers/auth';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { TokenService } from '../../services';
|
||||
import {
|
||||
NODE_ENV,
|
||||
JWT_MFA_LIFETIME,
|
||||
JWT_MFA_SECRET
|
||||
} from '../../config';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import {
|
||||
TOKEN_EMAIL_MFA
|
||||
} from '../../variables';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
const clientPublicKeys: any = {};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
clientPublicKey
|
||||
}: { email: string; clientPublicKey: string } = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
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({ email: email }, {
|
||||
email: email,
|
||||
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 {
|
||||
const { email, clientProof } = 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');
|
||||
|
||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||
|
||||
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: JWT_MFA_LIFETIME,
|
||||
secret: JWT_MFA_SECRET
|
||||
});
|
||||
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: 'emailMfa.handlebars',
|
||||
subjectLine: 'Infisical MFA code',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
mfaEnabled: true,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
// 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: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
|
||||
// case: user does not have MFA enabled
|
||||
// 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;
|
||||
}
|
||||
|
||||
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?'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send MFA token to email [email]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const sendMfaToken = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: 'emailMfa.handlebars',
|
||||
subjectLine: 'Infisical MFA code',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to send MFA code'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully sent new MFA code'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
|
||||
* MFA token [mfaToken] is valid
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const verifyMfaToken = async (req: Request, res: Response) => {
|
||||
const { email, mfaToken } = req.body;
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email,
|
||||
token: mfaToken
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// 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: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
|
||||
interface VerifyMfaTokenRes {
|
||||
encryptionVersion: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
const resObj: VerifyMfaTokenRes = {
|
||||
encryptionVersion: user.encryptionVersion,
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey as string,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey as string,
|
||||
iv: user.iv as string,
|
||||
tag: user.tag as string
|
||||
}
|
||||
|
||||
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
|
||||
resObj.protectedKey = user.protectedKey;
|
||||
resObj.protectedKeyIV = user.protectedKeyIV;
|
||||
resObj.protectedKeyTag = user.protectedKeyTag;
|
||||
}
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as authController from './authController';
|
||||
import * as signupController from './signupController';
|
||||
import * as usersController from './usersController';
|
||||
import * as organizationsController from './organizationsController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
@@ -9,6 +11,8 @@ import * as environmentController from './environmentController';
|
||||
import * as tagController from './tagController';
|
||||
|
||||
export {
|
||||
authController,
|
||||
signupController,
|
||||
usersController,
|
||||
organizationsController,
|
||||
workspaceController,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import to from 'await-to-js';
|
||||
import { Types } from 'mongoose';
|
||||
import { Request, Response } from 'express';
|
||||
import { ISecret, Membership, Secret, Workspace } from '../../models';
|
||||
import { ISecret, Secret } from '../../models';
|
||||
import { IAction } from '../../ee/models';
|
||||
import {
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
@@ -20,6 +21,252 @@ import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
||||
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
|
||||
import Tag from '../../models/tag';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
BatchSecretRequest,
|
||||
BatchSecret
|
||||
} from '../../types/secret';
|
||||
|
||||
/**
|
||||
* Peform a batch of any specified CUD secret operations
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const batchSecrets = async (req: Request, res: Response) => {
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent']);
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
requests
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
requests: BatchSecretRequest[];
|
||||
}= req.body;
|
||||
|
||||
const createSecrets: BatchSecret[] = [];
|
||||
const updateSecrets: BatchSecret[] = [];
|
||||
const deleteSecrets: Types.ObjectId[] = [];
|
||||
const actions: IAction[] = [];
|
||||
|
||||
requests.forEach((request) => {
|
||||
switch (request.method) {
|
||||
case 'POST':
|
||||
createSecrets.push({
|
||||
...request.secret,
|
||||
version: 1,
|
||||
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
|
||||
environment,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
break;
|
||||
case 'PATCH':
|
||||
updateSecrets.push({
|
||||
...request.secret,
|
||||
_id: new Types.ObjectId(request.secret._id)
|
||||
});
|
||||
break;
|
||||
case 'DELETE':
|
||||
deleteSecrets.push(new Types.ObjectId(request.secret._id));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// handle create secrets
|
||||
let createdSecrets: ISecret[] = [];
|
||||
if (createSecrets.length > 0) {
|
||||
createdSecrets = await Secret.insertMany(createSecrets);
|
||||
// (EE) add secret versions for new secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: createdSecrets.map((n: any) => {
|
||||
return ({
|
||||
...n._doc,
|
||||
_id: new Types.ObjectId(),
|
||||
secret: n._id,
|
||||
isDeleted: false
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
const addAction = await EELogService.createAction({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
userId: req.user._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: createdSecrets.map((n) => n._id)
|
||||
}) as IAction;
|
||||
actions.push(addAction);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets added',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: createdSecrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel,
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// handle update secrets
|
||||
let updatedSecrets: ISecret[] = [];
|
||||
if (updateSecrets.length > 0 && req.secrets) {
|
||||
// construct object containing all secrets
|
||||
let listedSecretsObj: {
|
||||
[key: string]: {
|
||||
version: number;
|
||||
type: string;
|
||||
}
|
||||
} = {};
|
||||
|
||||
listedSecretsObj = req.secrets.reduce((obj: any, secret: ISecret) => ({
|
||||
...obj,
|
||||
[secret._id.toString()]: secret
|
||||
}), {});
|
||||
|
||||
const updateOperations = updateSecrets.map((u) => ({
|
||||
updateOne: {
|
||||
filter: { _id: new Types.ObjectId(u._id) },
|
||||
update: {
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
...u,
|
||||
_id: new Types.ObjectId(u._id)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
await Secret.bulkWrite(updateOperations);
|
||||
|
||||
const secretVersions = updateSecrets.map((u) => ({
|
||||
secret: new Types.ObjectId(u._id),
|
||||
version: listedSecretsObj[u._id.toString()].version,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type: listedSecretsObj[u._id.toString()].type,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext: u.secretKeyCiphertext,
|
||||
secretKeyIV: u.secretKeyIV,
|
||||
secretKeyTag: u.secretKeyTag,
|
||||
secretValueCiphertext: u.secretValueCiphertext,
|
||||
secretValueIV: u.secretValueIV,
|
||||
secretValueTag: u.secretValueTag,
|
||||
secretCommentCiphertext: u.secretCommentCiphertext,
|
||||
secretCommentIV: u.secretCommentIV,
|
||||
secretCommentTag: u.secretCommentTag,
|
||||
tags: u.tags
|
||||
}));
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions
|
||||
});
|
||||
|
||||
updatedSecrets = await Secret.find({
|
||||
_id: {
|
||||
$in: updateSecrets.map((u) => new Types.ObjectId(u._id))
|
||||
}
|
||||
});
|
||||
|
||||
const updateAction = await EELogService.createAction({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
userId: req.user._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: updatedSecrets.map((u) => u._id)
|
||||
}) as IAction;
|
||||
actions.push(updateAction);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets modified',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: updateSecrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel,
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// handle delete secrets
|
||||
if (deleteSecrets.length > 0) {
|
||||
await Secret.deleteMany({
|
||||
_id: {
|
||||
$in: deleteSecrets
|
||||
}
|
||||
});
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: deleteSecrets
|
||||
});
|
||||
|
||||
const deleteAction = await EELogService.createAction({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId: req.user._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: deleteSecrets
|
||||
}) as IAction;
|
||||
actions.push(deleteAction);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets deleted',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: deleteSecrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel,
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length > 0) {
|
||||
// (EE) create (audit) log
|
||||
await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
actions,
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
}
|
||||
|
||||
// // trigger event - push secrets
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
});
|
||||
|
||||
const resObj: { [key: string]: ISecret[] | string[] } = {}
|
||||
|
||||
if (createSecrets.length > 0) {
|
||||
resObj['createdSecrets'] = createdSecrets;
|
||||
}
|
||||
|
||||
if (updateSecrets.length > 0) {
|
||||
resObj['updatedSecrets'] = updatedSecrets;
|
||||
}
|
||||
|
||||
if (deleteSecrets.length > 0) {
|
||||
resObj['deletedSecrets'] = deleteSecrets.map((d) => d.toString());
|
||||
}
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
||||
@@ -166,11 +413,9 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
@@ -187,11 +432,9 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
|
||||
250
backend/src/controllers/v2/signupController.ts
Normal file
250
backend/src/controllers/v2/signupController.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
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 } from '../../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../../variables';
|
||||
import { NODE_ENV } from '../../config';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 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
|
||||
}: {
|
||||
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;
|
||||
} = req.body;
|
||||
|
||||
// get user
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// 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 (process.env.LOOPS_API_KEY) {
|
||||
await axios.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
"lastName": lastName
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
} 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
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete setting up user by adding their personal and auth information as part of the
|
||||
* invite flow
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
let user, token, refreshToken;
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
} = req.body;
|
||||
|
||||
// get user
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
||||
|
||||
// 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');
|
||||
|
||||
// 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;
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
} 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
|
||||
});
|
||||
};
|
||||
@@ -55,6 +55,35 @@ export const getMe = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current user's MFA-enabled status [isMfaEnabled].
|
||||
* Note: Infisical currently only supports email-based 2FA only; this will expand to
|
||||
* include SMS and authenticator app modes of authentication in the future.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
try {
|
||||
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
|
||||
req.user.isMfaEnabled = isMfaEnabled;
|
||||
await req.user.save();
|
||||
|
||||
user = req.user;
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to update current user's MFA status"
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return organizations that the current user is part of.
|
||||
* @param req
|
||||
|
||||
@@ -158,11 +158,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
} = oldSecretVersion;
|
||||
|
||||
// update secret
|
||||
@@ -179,11 +177,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
},
|
||||
{
|
||||
new: true
|
||||
@@ -204,11 +200,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
secretValueTag
|
||||
}).save();
|
||||
|
||||
// take secret snapshot
|
||||
|
||||
@@ -5,22 +5,19 @@ import {
|
||||
} from '../../variables';
|
||||
|
||||
export interface ISecretVersion {
|
||||
_id: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId; // new
|
||||
type: string; // new
|
||||
user: Types.ObjectId; // new
|
||||
user?: Types.ObjectId; // new
|
||||
environment: string; // new
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
@@ -72,9 +69,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -87,9 +81,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String
|
||||
},
|
||||
tags: {
|
||||
ref: 'Tag',
|
||||
type: [Schema.Types.ObjectId],
|
||||
|
||||
@@ -211,7 +211,7 @@ const getAuthAPIKeyPayload = async ({
|
||||
* @return {String} obj.token - issued JWT token
|
||||
* @return {String} obj.refreshToken - issued refresh token
|
||||
*/
|
||||
const issueTokens = async ({ userId }: { userId: string }) => {
|
||||
const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
let token: string;
|
||||
let refreshToken: string;
|
||||
try {
|
||||
@@ -298,6 +298,6 @@ export {
|
||||
getAuthSTDPayload,
|
||||
getAuthAPIKeyPayload,
|
||||
createToken,
|
||||
issueTokens,
|
||||
issueAuthTokens,
|
||||
clearTokens
|
||||
};
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
import { Token, IToken, IUser } from '../models';
|
||||
import { IUser } from '../models';
|
||||
import { createOrganization } from './organization';
|
||||
import { addMembershipsOrg } from './membershipOrg';
|
||||
import { createWorkspace } from './workspace';
|
||||
import { addMemberships } from './membership';
|
||||
import { OWNER, ADMIN, ACCEPTED } from '../variables';
|
||||
import { OWNER, ACCEPTED } from '../variables';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
||||
import { TokenService } from '../services';
|
||||
import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
|
||||
|
||||
/**
|
||||
* Send magic link to verify email to [email]
|
||||
@@ -15,22 +13,13 @@ import { EMAIL_TOKEN_LIFETIME } from '../config';
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.email - email
|
||||
* @returns {Boolean} success - whether or not operation was successful
|
||||
*
|
||||
*/
|
||||
const sendEmailVerification = async ({ email }: { email: string }) => {
|
||||
try {
|
||||
const token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||
|
||||
await Token.findOneAndUpdate(
|
||||
{ email },
|
||||
{
|
||||
email,
|
||||
token,
|
||||
createdAt: new Date(),
|
||||
ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_CONFIRMATION,
|
||||
email
|
||||
});
|
||||
|
||||
// send mail
|
||||
await sendMail({
|
||||
@@ -64,21 +53,11 @@ const checkEmailVerification = async ({
|
||||
code: string;
|
||||
}) => {
|
||||
try {
|
||||
const token = await Token.findOne({
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_CONFIRMATION,
|
||||
email,
|
||||
token: code
|
||||
});
|
||||
|
||||
if (token && Math.floor(Date.now() / 1000) > token.ttl) {
|
||||
await Token.deleteOne({
|
||||
email,
|
||||
token: code
|
||||
});
|
||||
|
||||
throw new Error('Verification token has expired')
|
||||
}
|
||||
|
||||
if (!token) throw new Error('Failed to find email verification token');
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@@ -114,18 +93,6 @@ const initializeDefaultOrg = async ({
|
||||
roles: [OWNER],
|
||||
statuses: [ACCEPTED]
|
||||
});
|
||||
|
||||
// initialize a default workspace inside the new organization
|
||||
const workspace = await createWorkspace({
|
||||
name: `Example Project`,
|
||||
organizationId: organization._id.toString()
|
||||
});
|
||||
|
||||
await addMemberships({
|
||||
userIds: [user._id.toString()],
|
||||
workspaceId: workspace._id.toString(),
|
||||
roles: [ADMIN]
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
|
||||
}
|
||||
|
||||
217
backend/src/helpers/token.ts
Normal file
217
backend/src/helpers/token.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { TokenData } from '../models';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
TOKEN_EMAIL_CONFIRMATION,
|
||||
TOKEN_EMAIL_MFA,
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
} from '../variables';
|
||||
import {
|
||||
SALT_ROUNDS
|
||||
} from '../config';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
/**
|
||||
* Create and store a token in the database for purpose [type]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.type
|
||||
* @param {String} obj.email
|
||||
* @param {String} obj.phoneNumber
|
||||
* @param {Types.ObjectId} obj.organizationId
|
||||
* @returns {String} token - the created token
|
||||
*/
|
||||
const createTokenHelper = async ({
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId
|
||||
}: {
|
||||
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organizationId?: Types.ObjectId
|
||||
}) => {
|
||||
let token, expiresAt, triesLeft;
|
||||
try {
|
||||
// generate random token based on specified token use-case
|
||||
// type [type]
|
||||
switch (type) {
|
||||
case TOKEN_EMAIL_CONFIRMATION:
|
||||
// generate random 6-digit code
|
||||
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||
expiresAt = new Date((new Date()).getTime() + 86400000);
|
||||
break;
|
||||
case TOKEN_EMAIL_MFA:
|
||||
// generate random 6-digit code
|
||||
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||
triesLeft = 5;
|
||||
expiresAt = new Date((new Date()).getTime() + 300000);
|
||||
break;
|
||||
case TOKEN_EMAIL_ORG_INVITATION:
|
||||
// generate random hex
|
||||
token = crypto.randomBytes(16).toString('hex');
|
||||
expiresAt = new Date((new Date()).getTime() + 259200000);
|
||||
break;
|
||||
case TOKEN_EMAIL_PASSWORD_RESET:
|
||||
// generate random hex
|
||||
token = crypto.randomBytes(16).toString('hex');
|
||||
expiresAt = new Date((new Date()).getTime() + 86400000);
|
||||
break;
|
||||
default:
|
||||
token = crypto.randomBytes(16).toString('hex');
|
||||
expiresAt = new Date();
|
||||
break;
|
||||
}
|
||||
|
||||
interface TokenDataQuery {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organization?: Types.ObjectId;
|
||||
}
|
||||
|
||||
interface TokenDataUpdate {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organization?: Types.ObjectId;
|
||||
tokenHash: string;
|
||||
triesLeft?: number;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
const query: TokenDataQuery = { type };
|
||||
const update: TokenDataUpdate = {
|
||||
type,
|
||||
tokenHash: await bcrypt.hash(token, SALT_ROUNDS),
|
||||
expiresAt
|
||||
}
|
||||
|
||||
if (email) {
|
||||
query.email = email;
|
||||
update.email = email;
|
||||
}
|
||||
if (phoneNumber) {
|
||||
query.phoneNumber = phoneNumber;
|
||||
update.phoneNumber = phoneNumber;
|
||||
}
|
||||
if (organizationId) {
|
||||
query.organization = organizationId
|
||||
update.organization = organizationId
|
||||
}
|
||||
|
||||
if (triesLeft) {
|
||||
update.triesLeft = triesLeft;
|
||||
}
|
||||
|
||||
await TokenData.findOneAndUpdate(
|
||||
query,
|
||||
update,
|
||||
{
|
||||
new: true,
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error(
|
||||
"Failed to create token"
|
||||
);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.email - email associated with the token
|
||||
* @param {String} obj.token - value of the token
|
||||
*/
|
||||
const validateTokenHelper = async ({
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId,
|
||||
token
|
||||
}: {
|
||||
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organizationId?: Types.ObjectId;
|
||||
token: string;
|
||||
}) => {
|
||||
interface Query {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organization?: Types.ObjectId;
|
||||
}
|
||||
|
||||
const query: Query = { type };
|
||||
|
||||
if (email) { query.email = email; }
|
||||
if (phoneNumber) { query.phoneNumber = phoneNumber; }
|
||||
if (organizationId) { query.organization = organizationId; }
|
||||
|
||||
const tokenData = await TokenData.findOne(query).select('+tokenHash');
|
||||
|
||||
if (!tokenData) throw new Error('Failed to find token to validate');
|
||||
|
||||
if (tokenData.expiresAt < new Date()) {
|
||||
// case: token expired
|
||||
await TokenData.findByIdAndDelete(tokenData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'MFA session expired. Please log in again',
|
||||
context: {
|
||||
code: 'mfa_expired'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
|
||||
if (!isValid) {
|
||||
// case: token is not valid
|
||||
if (tokenData?.triesLeft !== undefined) {
|
||||
// case: token has a try-limit
|
||||
if (tokenData.triesLeft === 1) {
|
||||
// case: token is out of tries
|
||||
await TokenData.findByIdAndDelete(tokenData._id);
|
||||
} else {
|
||||
// case: token has more than 1 try left
|
||||
await TokenData.findByIdAndUpdate(tokenData._id, {
|
||||
triesLeft: tokenData.triesLeft - 1
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'MFA code is invalid',
|
||||
context: {
|
||||
code: 'mfa_invalid',
|
||||
triesLeft: tokenData.triesLeft - 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'MFA code is invalid',
|
||||
context: {
|
||||
code: 'mfa_invalid'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// case: token is valid
|
||||
await TokenData.findByIdAndDelete(tokenData._id);
|
||||
}
|
||||
|
||||
export {
|
||||
createTokenHelper,
|
||||
validateTokenHelper
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { User, IUser } from '../models';
|
||||
import { User } from '../models';
|
||||
|
||||
/**
|
||||
* Initialize a user under email [email]
|
||||
@@ -28,10 +28,14 @@ const setupAccount = async ({ email }: { email: string }) => {
|
||||
* @param {String} obj.userId - id of user to finish setting up
|
||||
* @param {String} obj.firstName - first name of user
|
||||
* @param {String} obj.lastName - last name of user
|
||||
* @param {Number} obj.encryptionVersion - version of auth encryption scheme used
|
||||
* @param {String} obj.protectedKey - protected key in encryption version 2
|
||||
* @param {String} obj.protectedKeyIV - IV of protected key in encryption version 2
|
||||
* @param {String} obj.protectedKeyTag - tag of protected key in encryption version 2
|
||||
* @param {String} obj.publicKey - publickey of user
|
||||
* @param {String} obj.encryptedPrivateKey - (encrypted) private key of user
|
||||
* @param {String} obj.iv - iv for (encrypted) private key of user
|
||||
* @param {String} obj.tag - tag for (encrypted) private key of user
|
||||
* @param {String} obj.encryptedPrivateKeyIV - iv for (encrypted) private key of user
|
||||
* @param {String} obj.encryptedPrivateKeyTag - tag for (encrypted) private key of user
|
||||
* @param {String} obj.salt - salt for auth SRP
|
||||
* @param {String} obj.verifier - verifier for auth SRP
|
||||
* @returns {Object} user - the completed user
|
||||
@@ -40,20 +44,28 @@ const completeAccount = async ({
|
||||
userId,
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
}: {
|
||||
userId: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
encryptionVersion: number;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
}) => {
|
||||
@@ -67,10 +79,14 @@ const completeAccount = async ({
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
|
||||
@@ -138,7 +138,7 @@ const exchangeCodeAzure = async ({
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
scope: 'https://vault.azure.net/.default openid offline_access',
|
||||
@@ -147,16 +147,16 @@ const exchangeCodeAzure = async ({
|
||||
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err: any) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
||||
}
|
||||
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
@@ -175,36 +175,36 @@ const exchangeCodeAzure = async ({
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeHeroku = async ({
|
||||
code
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
code: string;
|
||||
}) => {
|
||||
let res: ExchangeCodeHerokuResponse;
|
||||
const accessExpiresAt = new Date();
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
let res: ExchangeCodeHerokuResponse;
|
||||
const accessExpiresAt = new Date();
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,7 +234,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Vercel');
|
||||
throw new Error(`Failed OAuth2 code-token exchange with Vercel [err=${err}]`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
} from "../variables";
|
||||
import { access, appendFile } from "fs";
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
@@ -181,7 +180,8 @@ const syncSecretsAzureKeyVault = async ({
|
||||
while (url) {
|
||||
const res = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -202,7 +202,8 @@ const syncSecretsAzureKeyVault = async ({
|
||||
|
||||
const azureKeyVaultSecret = await axios.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -259,7 +260,8 @@ const syncSecretsAzureKeyVault = async ({
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -270,7 +272,8 @@ const syncSecretsAzureKeyVault = async ({
|
||||
deleteSecrets.forEach(async (secret) => {
|
||||
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -488,6 +491,7 @@ const syncSecretsHeroku = async ({
|
||||
headers: {
|
||||
Accept: "application/vnd.heroku+json; version=3",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -506,6 +510,7 @@ const syncSecretsHeroku = async ({
|
||||
headers: {
|
||||
Accept: "application/vnd.heroku+json; version=3",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -552,7 +557,7 @@ const syncSecretsVercel = async ({
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
|
||||
const res = (
|
||||
await Promise.all(
|
||||
(
|
||||
@@ -561,7 +566,8 @@ const syncSecretsVercel = async ({
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
}
|
||||
))
|
||||
@@ -573,7 +579,8 @@ const syncSecretsVercel = async ({
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
}
|
||||
)).data)
|
||||
@@ -633,6 +640,7 @@ const syncSecretsVercel = async ({
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -649,6 +657,7 @@ const syncSecretsVercel = async ({
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -664,6 +673,7 @@ const syncSecretsVercel = async ({
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -723,6 +733,7 @@ const syncSecretsNetlify = async ({
|
||||
params: getParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -837,6 +848,7 @@ const syncSecretsNetlify = async ({
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -854,6 +866,7 @@ const syncSecretsNetlify = async ({
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -868,6 +881,7 @@ const syncSecretsNetlify = async ({
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -882,6 +896,7 @@ const syncSecretsNetlify = async ({
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1035,6 +1050,7 @@ const syncSecretsRender = async ({
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1088,6 +1104,7 @@ const syncSecretsFlyio = async ({
|
||||
method: "post",
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
data: {
|
||||
query: SetSecrets,
|
||||
@@ -1167,6 +1184,7 @@ const syncSecretsFlyio = async ({
|
||||
headers: {
|
||||
Authorization: "Bearer " + accessToken,
|
||||
"Content-Type": "application/json",
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
data: {
|
||||
query: DeleteSecrets,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import requireAuth from './requireAuth';
|
||||
import requireMfaAuth from './requireMfaAuth';
|
||||
import requireBotAuth from './requireBotAuth';
|
||||
import requireSignupAuth from './requireSignupAuth';
|
||||
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||
@@ -15,6 +16,7 @@ import validateRequest from './validateRequest';
|
||||
|
||||
export {
|
||||
requireAuth,
|
||||
requireMfaAuth,
|
||||
requireBotAuth,
|
||||
requireSignupAuth,
|
||||
requireWorkspaceAuth,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ErrorRequestHandler } from "express";
|
||||
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { InternalServerError } from "../utils/errors";
|
||||
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
|
||||
import { getLogger } from "../utils/logger";
|
||||
import RequestError, { LogLevel } from "../utils/requestError";
|
||||
import { NODE_ENV } from "../config";
|
||||
|
||||
import { TokenExpiredError } from 'jsonwebtoken';
|
||||
|
||||
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
|
||||
if (res.headersSent) return next();
|
||||
|
||||
43
backend/src/middleware/requireMfaAuth.ts
Normal file
43
backend/src/middleware/requireMfaAuth.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { User } from '../models';
|
||||
import { JWT_MFA_SECRET } from '../config';
|
||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if (MFA) JWT temporary token on request is valid (e.g. not expired)
|
||||
* and if there is an associated user.
|
||||
*/
|
||||
const requireMfaAuth = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
// JWT (temporary) authentication middleware for complete signup
|
||||
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
|
||||
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
|
||||
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(AUTH_TOKEN_VALUE, JWT_MFA_SECRET)
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
|
||||
if (!user)
|
||||
return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'}))
|
||||
|
||||
req.user = user;
|
||||
return next();
|
||||
};
|
||||
|
||||
export default requireMfaAuth;
|
||||
@@ -10,7 +10,7 @@ import MembershipOrg, { IMembershipOrg } from './membershipOrg';
|
||||
import Organization, { IOrganization } from './organization';
|
||||
import Secret, { ISecret } from './secret';
|
||||
import ServiceToken, { IServiceToken } from './serviceToken';
|
||||
import Token, { IToken } from './token';
|
||||
import TokenData, { ITokenData } from './tokenData';
|
||||
import User, { IUser } from './user';
|
||||
import UserAction, { IUserAction } from './userAction';
|
||||
import Workspace, { IWorkspace } from './workspace';
|
||||
@@ -43,8 +43,8 @@ export {
|
||||
ISecret,
|
||||
ServiceToken,
|
||||
IServiceToken,
|
||||
Token,
|
||||
IToken,
|
||||
TokenData,
|
||||
ITokenData,
|
||||
User,
|
||||
IUser,
|
||||
UserAction,
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface IToken {
|
||||
email: string;
|
||||
token: string;
|
||||
createdAt: Date;
|
||||
ttl: Number;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
const tokenSchema = new Schema<IToken>({
|
||||
|
||||
61
backend/src/models/tokenData.ts
Normal file
61
backend/src/models/tokenData.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Schema, Types, model } from 'mongoose';
|
||||
|
||||
export interface ITokenData {
|
||||
type: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organization?: Types.ObjectId;
|
||||
tokenHash: string;
|
||||
triesLeft?: number;
|
||||
expiresAt: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const tokenDataSchema = new Schema<ITokenData>({
|
||||
type: {
|
||||
type: String,
|
||||
enum: [
|
||||
'emailConfirmation',
|
||||
'emailMfa',
|
||||
'organizationInvitation',
|
||||
'passwordReset'
|
||||
],
|
||||
required: true
|
||||
},
|
||||
email: {
|
||||
type: String
|
||||
},
|
||||
phoneNumber: {
|
||||
type: String
|
||||
},
|
||||
organization: { // organizationInvitation-specific field
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Organization'
|
||||
},
|
||||
tokenHash: {
|
||||
type: String,
|
||||
select: false,
|
||||
required: true
|
||||
},
|
||||
triesLeft: {
|
||||
type: Number
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
expires: 0,
|
||||
required: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
tokenDataSchema.index({
|
||||
expiresAt: 1
|
||||
}, {
|
||||
expireAfterSeconds: 0
|
||||
});
|
||||
|
||||
const TokenData = model<ITokenData>('TokenData', tokenDataSchema);
|
||||
|
||||
export default TokenData;
|
||||
@@ -5,6 +5,10 @@ export interface IUser {
|
||||
email: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
encryptionVersion: number;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey?: string;
|
||||
encryptedPrivateKey?: string;
|
||||
iv?: string;
|
||||
@@ -12,6 +16,7 @@ export interface IUser {
|
||||
salt?: string;
|
||||
verifier?: string;
|
||||
refreshVersion?: number;
|
||||
isMfaEnabled: boolean;
|
||||
seenIps: [string];
|
||||
}
|
||||
|
||||
@@ -27,6 +32,23 @@ const userSchema = new Schema<IUser>(
|
||||
lastName: {
|
||||
type: String
|
||||
},
|
||||
encryptionVersion: {
|
||||
type: Number,
|
||||
select: false,
|
||||
default: 1 // to resolve backward-compatibility issues
|
||||
},
|
||||
protectedKey: { // introduced as part of encryption version 2
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
protectedKeyIV: { // introduced as part of encryption version 2
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
protectedKeyTag: { // introduced as part of encryption version 2
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
publicKey: {
|
||||
type: String,
|
||||
select: false
|
||||
@@ -35,11 +57,11 @@ const userSchema = new Schema<IUser>(
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
iv: {
|
||||
iv: { // iv of [encryptedPrivateKey]
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
tag: {
|
||||
tag: { // tag of [encryptedPrivateKey]
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
@@ -56,6 +78,10 @@ const userSchema = new Schema<IUser>(
|
||||
default: 0,
|
||||
select: false
|
||||
},
|
||||
isMfaEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
seenIps: [String]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import { authLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post('/token', validateRequest, authController.getNewToken);
|
||||
|
||||
router.post(
|
||||
router.post( // deprecated (moved to api/v2/auth/login1)
|
||||
'/login1',
|
||||
authLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
@@ -16,7 +16,7 @@ router.post(
|
||||
authController.login1
|
||||
);
|
||||
|
||||
router.post(
|
||||
router.post( // deprecated (moved to api/v2/auth/login2)
|
||||
'/login2',
|
||||
authLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
|
||||
@@ -31,7 +31,7 @@ router.patch(
|
||||
requireBotAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
body('isActive').isBoolean(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('botKey'),
|
||||
validateRequest,
|
||||
botController.setBotActiveState
|
||||
|
||||
@@ -10,7 +10,7 @@ router.post(
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('clientPublicKey').exists().trim().notEmpty(),
|
||||
body('clientPublicKey').exists().isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
passwordController.srp1
|
||||
);
|
||||
@@ -22,11 +22,14 @@ router.post(
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('clientProof').exists().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd
|
||||
body('iv').exists().trim().notEmpty(), // new iv for private key
|
||||
body('tag').exists().trim().notEmpty(), // new tag for private key
|
||||
body('salt').exists().trim().notEmpty(), // part of new pwd
|
||||
body('verifier').exists().trim().notEmpty(), // part of new pwd
|
||||
body('protectedKey').exists().isString().trim().notEmpty(),
|
||||
body('protectedKeyIV').exists().isString().trim().notEmpty(),
|
||||
body('protectedKeyTag').exists().isString().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // private key encrypted under new pwd
|
||||
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), // new iv for private key
|
||||
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), // new tag for private key
|
||||
body('salt').exists().isString().trim().notEmpty(), // part of new pwd
|
||||
body('verifier').exists().isString().trim().notEmpty(), // part of new pwd
|
||||
validateRequest,
|
||||
passwordController.changePassword
|
||||
);
|
||||
@@ -34,7 +37,7 @@ router.post(
|
||||
router.post(
|
||||
'/email/password-reset',
|
||||
passwordLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
body('email').exists().isString().trim().notEmpty().isEmail(),
|
||||
validateRequest,
|
||||
passwordController.emailPasswordReset
|
||||
);
|
||||
@@ -42,8 +45,8 @@ router.post(
|
||||
router.post(
|
||||
'/email/password-reset-verify',
|
||||
passwordLimiter,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
body('code').exists().trim().notEmpty(),
|
||||
body('email').exists().isString().trim().notEmpty().isEmail(),
|
||||
body('code').exists().isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
passwordController.emailPasswordResetVerify
|
||||
);
|
||||
@@ -61,12 +64,12 @@ router.post(
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('clientProof').exists().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key
|
||||
body('iv').exists().trim().notEmpty(), // new iv for (backup) private key
|
||||
body('tag').exists().trim().notEmpty(), // new tag for (backup) private key
|
||||
body('salt').exists().trim().notEmpty(), // salt generated from strong key
|
||||
body('verifier').exists().trim().notEmpty(), // salt generated from strong key
|
||||
body('clientProof').exists().isString().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key
|
||||
body('iv').exists().isString().trim().notEmpty(), // new iv for (backup) private key
|
||||
body('tag').exists().isString().trim().notEmpty(), // new tag for (backup) private key
|
||||
body('salt').exists().isString().trim().notEmpty(), // salt generated from strong key
|
||||
body('verifier').exists().isString().trim().notEmpty(), // salt generated from strong key
|
||||
validateRequest,
|
||||
passwordController.createBackupPrivateKey
|
||||
);
|
||||
@@ -74,11 +77,14 @@ router.post(
|
||||
router.post(
|
||||
'/password-reset',
|
||||
requireSignupAuth,
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty(), // private key encrypted under new pwd
|
||||
body('iv').exists().trim().notEmpty(), // new iv for private key
|
||||
body('tag').exists().trim().notEmpty(), // new tag for private key
|
||||
body('salt').exists().trim().notEmpty(), // part of new pwd
|
||||
body('verifier').exists().trim().notEmpty(), // part of new pwd
|
||||
body('protectedKey').exists().isString().trim().notEmpty(),
|
||||
body('protectedKeyIV').exists().isString().trim().notEmpty(),
|
||||
body('protectedKeyTag').exists().isString().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // private key encrypted under new pwd
|
||||
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), // new iv for private key
|
||||
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), // new tag for private key
|
||||
body('salt').exists().isString().trim().notEmpty(), // part of new pwd
|
||||
body('verifier').exists().isString().trim().notEmpty(), // part of new pwd
|
||||
validateRequest,
|
||||
passwordController.resetPassword
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireSignupAuth, validateRequest } from '../../middleware';
|
||||
import { validateRequest } from '../../middleware';
|
||||
import { signupController } from '../../controllers/v1';
|
||||
import { authLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
@@ -22,39 +22,4 @@ router.post(
|
||||
signupController.verifyEmailSignup
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/complete-account/signup',
|
||||
authLimiter,
|
||||
requireSignupAuth,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
body('firstName').exists().trim().notEmpty(),
|
||||
body('lastName').exists().trim().notEmpty(),
|
||||
body('publicKey').exists().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty(),
|
||||
body('iv').exists().trim().notEmpty(),
|
||||
body('tag').exists().trim().notEmpty(),
|
||||
body('salt').exists().trim().notEmpty(),
|
||||
body('verifier').exists().trim().notEmpty(),
|
||||
body('organizationName').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
signupController.completeAccountSignup
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/complete-account/invite',
|
||||
authLimiter,
|
||||
requireSignupAuth,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
body('firstName').exists().trim().notEmpty(),
|
||||
body('lastName').exists().trim().notEmpty(),
|
||||
body('publicKey').exists().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty(),
|
||||
body('iv').exists().trim().notEmpty(),
|
||||
body('tag').exists().trim().notEmpty(),
|
||||
body('salt').exists().trim().notEmpty(),
|
||||
body('verifier').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
signupController.completeAccountInvite
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
44
backend/src/routes/v2/auth.ts
Normal file
44
backend/src/routes/v2/auth.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireMfaAuth, validateRequest } from '../../middleware';
|
||||
import { authController } from '../../controllers/v2';
|
||||
import { authLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post(
|
||||
'/login1',
|
||||
authLimiter,
|
||||
body('email').isString().trim().notEmpty(),
|
||||
body('clientPublicKey').isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login1
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/login2',
|
||||
authLimiter,
|
||||
body('email').isString().trim().notEmpty(),
|
||||
body('clientProof').isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login2
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/mfa/send',
|
||||
authLimiter,
|
||||
body('email').isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.sendMfaToken
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/mfa/verify',
|
||||
authLimiter,
|
||||
requireMfaAuth,
|
||||
body('email').isString().trim().notEmpty(),
|
||||
body('mfaToken').isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.verifyMfaToken
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,3 +1,5 @@
|
||||
import auth from './auth';
|
||||
import signup from './signup';
|
||||
import users from './users';
|
||||
import organizations from './organizations';
|
||||
import workspace from './workspace';
|
||||
@@ -9,6 +11,8 @@ import environment from "./environment"
|
||||
import tags from "./tags"
|
||||
|
||||
export {
|
||||
auth,
|
||||
signup,
|
||||
users,
|
||||
organizations,
|
||||
workspace,
|
||||
|
||||
@@ -6,14 +6,52 @@ import {
|
||||
requireSecretsAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { query, check, body } from 'express-validator';
|
||||
import { query, body } from 'express-validator';
|
||||
import { secretsController } from '../../controllers/v2';
|
||||
import { validateSecrets } from '../../helpers/secret';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED
|
||||
} from '../../variables';
|
||||
import {
|
||||
BatchSecretRequest
|
||||
} from '../../types/secret';
|
||||
|
||||
router.post(
|
||||
'/batch',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('requests')
|
||||
.exists()
|
||||
.custom(async (requests: BatchSecretRequest[], { req }) => {
|
||||
if (Array.isArray(requests)) {
|
||||
const secretIds = requests
|
||||
.map((request) => request.secret._id)
|
||||
.filter((secretId) => secretId !== undefined)
|
||||
|
||||
if (secretIds.length > 0) {
|
||||
const relevantSecrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds
|
||||
});
|
||||
|
||||
req.secrets = relevantSecrets;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
validateRequest,
|
||||
secretsController.batchSecrets
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
|
||||
49
backend/src/routes/v2/signup.ts
Normal file
49
backend/src/routes/v2/signup.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireSignupAuth, validateRequest } from '../../middleware';
|
||||
import { signupController } from '../../controllers/v2';
|
||||
import { authLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post(
|
||||
'/complete-account/signup',
|
||||
authLimiter,
|
||||
requireSignupAuth,
|
||||
body('email').exists().isString().trim().notEmpty().isEmail(),
|
||||
body('firstName').exists().isString().trim().notEmpty(),
|
||||
body('lastName').exists().isString().trim().notEmpty(),
|
||||
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(),
|
||||
validateRequest,
|
||||
signupController.completeAccountSignup
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/complete-account/invite',
|
||||
authLimiter,
|
||||
requireSignupAuth,
|
||||
body('email').exists().isString().trim().notEmpty().isEmail(),
|
||||
body('firstName').exists().isString().trim().notEmpty(),
|
||||
body('lastName').exists().isString().trim().notEmpty(),
|
||||
body('protectedKey').exists().isString().trim().notEmpty(),
|
||||
body('protectedKeyIV').exists().isString().trim().notEmpty(),
|
||||
body('protectedKeyTag').exists().isString().trim().notEmpty(),
|
||||
body('publicKey').exists().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(),
|
||||
validateRequest,
|
||||
signupController.completeAccountInvite
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,8 +1,10 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth
|
||||
requireAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body, param } from 'express-validator';
|
||||
import { usersController } from '../../controllers/v2';
|
||||
|
||||
router.get(
|
||||
@@ -13,6 +15,16 @@ router.get(
|
||||
usersController.getMe
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/me/mfa',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
body('isMfaEnabled').exists().isBoolean(),
|
||||
validateRequest,
|
||||
usersController.updateMyMfaEnabled
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/me/organizations',
|
||||
requireAuth({
|
||||
|
||||
69
backend/src/services/TokenService.ts
Normal file
69
backend/src/services/TokenService.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Types } from 'mongoose';
|
||||
import { createTokenHelper, validateTokenHelper } from '../helpers/token';
|
||||
|
||||
/**
|
||||
* Class to handle token actions
|
||||
* TODO: elaborate more on this class
|
||||
*/
|
||||
class TokenService {
|
||||
/**
|
||||
* Create a token [token] for type [type] with associated details
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.type - type or context of token (e.g. emailConfirmation)
|
||||
* @param {String} obj.email - email associated with the token
|
||||
* @param {String} obj.phoneNumber - phone number associated with the token
|
||||
* @param {Types.ObjectId} obj.organizationId - id of organization associated with the token
|
||||
* @returns {String} token - the token to create
|
||||
*/
|
||||
static async createToken({
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId
|
||||
}: {
|
||||
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organizationId?: Types.ObjectId;
|
||||
}) {
|
||||
return await createTokenHelper({
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether or not token [token] and its associated details match a token in the DB
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.type - type or context of token (e.g. emailConfirmation)
|
||||
* @param {String} obj.email - email associated with the token
|
||||
* @param {String} obj.phoneNumber - phone number associated with the token
|
||||
* @param {Types.ObjectId} obj.organizationId - id of organization associated with the token
|
||||
* @param {String} obj.token - the token to validate
|
||||
*/
|
||||
static async validateToken({
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId,
|
||||
token
|
||||
}: {
|
||||
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
organizationId?: Types.ObjectId;
|
||||
token: string;
|
||||
}) {
|
||||
return await validateTokenHelper({
|
||||
type,
|
||||
email,
|
||||
phoneNumber,
|
||||
organizationId,
|
||||
token
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default TokenService;
|
||||
@@ -3,11 +3,13 @@ import postHogClient from './PostHogClient';
|
||||
import BotService from './BotService';
|
||||
import EventService from './EventService';
|
||||
import IntegrationService from './IntegrationService';
|
||||
import TokenService from './TokenService';
|
||||
|
||||
export {
|
||||
DatabaseService,
|
||||
postHogClient,
|
||||
BotService,
|
||||
EventService,
|
||||
IntegrationService
|
||||
IntegrationService,
|
||||
TokenService
|
||||
}
|
||||
19
backend/src/templates/emailMfa.handlebars
Normal file
19
backend/src/templates/emailMfa.handlebars
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>MFA Code</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Sign in attempt requires further verification</h2>
|
||||
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
|
||||
<h2>{{code}}</h2>
|
||||
<p>The MFA code will be valid for 2 minutes.</p>
|
||||
<p>Not you? Contact Infisical or your administrator immediately.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,14 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Email Verification</title>
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Confirm your email address</h2>
|
||||
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
||||
<h1>{{code}}</h1>
|
||||
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
38
backend/src/types/secret/index.d.ts
vendored
38
backend/src/types/secret/index.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
import { Types } from 'mongoose';
|
||||
import { Assign, Omit } from 'utility-types';
|
||||
import { ISecret } from '../../models';
|
||||
import { mongo } from 'mongoose';
|
||||
|
||||
// Everything is required, except the omitted types
|
||||
export type CreateSecretRequestBody = Omit<ISecret, "user" | "version" | "environment" | "workspace">;
|
||||
@@ -12,3 +14,39 @@ export type SanitizedSecretModify = Partial<Omit<ISecret, "user" | "version" | "
|
||||
|
||||
// Everything is required, except the omitted types
|
||||
export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">;
|
||||
|
||||
export interface BatchSecretRequest {
|
||||
id: string;
|
||||
method: 'POST' | 'PATCH' | 'DELETE';
|
||||
secret: Secret;
|
||||
}
|
||||
|
||||
export interface BatchSecret {
|
||||
_id: string;
|
||||
type: 'shared' | 'personal',
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface BatchSecret {
|
||||
_id: string;
|
||||
type: 'shared' | 'personal',
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
tags: string[];
|
||||
}
|
||||
@@ -40,10 +40,19 @@ import {
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
} from "./action";
|
||||
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from "./smtp";
|
||||
import { PLAN_STARTER, PLAN_PRO } from "./stripe";
|
||||
ACTION_READ_SECRETS
|
||||
} from './action';
|
||||
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
|
||||
import { PLAN_STARTER, PLAN_PRO } from './stripe';
|
||||
import {
|
||||
MFA_METHOD_EMAIL
|
||||
} from './user';
|
||||
import {
|
||||
TOKEN_EMAIL_CONFIRMATION,
|
||||
TOKEN_EMAIL_MFA,
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
} from './token';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
@@ -94,4 +103,9 @@ export {
|
||||
SMTP_HOST_MAILGUN,
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO,
|
||||
MFA_METHOD_EMAIL,
|
||||
TOKEN_EMAIL_CONFIRMATION,
|
||||
TOKEN_EMAIL_MFA,
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
};
|
||||
|
||||
11
backend/src/variables/token.ts
Normal file
11
backend/src/variables/token.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation';
|
||||
const TOKEN_EMAIL_MFA = 'emailMfa';
|
||||
const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation';
|
||||
const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset';
|
||||
|
||||
export {
|
||||
TOKEN_EMAIL_CONFIRMATION,
|
||||
TOKEN_EMAIL_MFA,
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
}
|
||||
5
backend/src/variables/user.ts
Normal file
5
backend/src/variables/user.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const MFA_METHOD_EMAIL = 'email';
|
||||
|
||||
export {
|
||||
MFA_METHOD_EMAIL
|
||||
}
|
||||
@@ -7,8 +7,8 @@ require (
|
||||
github.com/muesli/mango-cobra v1.2.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/crypto v0.3.0
|
||||
golang.org/x/term v0.3.0
|
||||
golang.org/x/crypto v0.6.0
|
||||
golang.org/x/term v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -31,8 +31,8 @@ require (
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/net v0.6.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -106,10 +106,14 @@ go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAV
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -123,9 +127,13 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -128,6 +128,68 @@ func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Req
|
||||
return secretsResponse, nil
|
||||
}
|
||||
|
||||
func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) {
|
||||
var loginOneV2Response GetLoginOneV2Response
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&loginOneV2Response).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
SetBody(request).
|
||||
Post(fmt.Sprintf("%v/v2/auth/login1", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return GetLoginOneV2Response{}, fmt.Errorf("CallLogin1V2: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return GetLoginOneV2Response{}, fmt.Errorf("CallLogin1V2: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return loginOneV2Response, nil
|
||||
}
|
||||
|
||||
func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest) (*VerifyMfaTokenResponse, *VerifyMfaTokenErrorResponse, error) {
|
||||
var verifyMfaTokenResponse VerifyMfaTokenResponse
|
||||
var responseError VerifyMfaTokenErrorResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&verifyMfaTokenResponse).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
SetError(&responseError).
|
||||
SetBody(request).
|
||||
Post(fmt.Sprintf("%v/v2/auth/mfa/verify", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("CallVerifyMfaToken: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return nil, &responseError, nil
|
||||
}
|
||||
|
||||
return &verifyMfaTokenResponse, nil, nil
|
||||
}
|
||||
|
||||
func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLoginTwoV2Response, error) {
|
||||
var loginTwoV2Response GetLoginTwoV2Response
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&loginTwoV2Response).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
SetBody(request).
|
||||
Post(fmt.Sprintf("%v/v2/auth/login2", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return loginTwoV2Response, nil
|
||||
}
|
||||
|
||||
func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) {
|
||||
var workSpacesResponse GetWorkSpacesResponse
|
||||
response, err := httpClient.
|
||||
|
||||
@@ -263,3 +263,63 @@ type GetAccessibleEnvironmentsResponse struct {
|
||||
IsWriteDenied bool `json:"isWriteDenied"`
|
||||
} `json:"accessibleEnvironments"`
|
||||
}
|
||||
|
||||
type GetLoginOneV2Request struct {
|
||||
Email string `json:"email"`
|
||||
ClientPublicKey string `json:"clientPublicKey"`
|
||||
}
|
||||
|
||||
type GetLoginOneV2Response struct {
|
||||
ServerPublicKey string `json:"serverPublicKey"`
|
||||
Salt string `json:"salt"`
|
||||
}
|
||||
|
||||
type GetLoginTwoV2Request struct {
|
||||
Email string `json:"email"`
|
||||
ClientProof string `json:"clientProof"`
|
||||
}
|
||||
|
||||
type GetLoginTwoV2Response struct {
|
||||
MfaEnabled bool `json:"mfaEnabled"`
|
||||
EncryptionVersion int `json:"encryptionVersion"`
|
||||
Token string `json:"token"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
EncryptedPrivateKey string `json:"encryptedPrivateKey"`
|
||||
Iv string `json:"iv"`
|
||||
Tag string `json:"tag"`
|
||||
ProtectedKey string `json:"protectedKey"`
|
||||
ProtectedKeyIV string `json:"protectedKeyIV"`
|
||||
ProtectedKeyTag string `json:"protectedKeyTag"`
|
||||
}
|
||||
|
||||
type VerifyMfaTokenRequest struct {
|
||||
Email string `json:"email"`
|
||||
MFAToken string `json:"mfaToken"`
|
||||
}
|
||||
|
||||
type VerifyMfaTokenResponse struct {
|
||||
EncryptionVersion int `json:"encryptionVersion"`
|
||||
Token string `json:"token"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
EncryptedPrivateKey string `json:"encryptedPrivateKey"`
|
||||
Iv string `json:"iv"`
|
||||
Tag string `json:"tag"`
|
||||
ProtectedKey string `json:"protectedKey"`
|
||||
ProtectedKeyIV string `json:"protectedKeyIV"`
|
||||
ProtectedKeyTag string `json:"protectedKeyTag"`
|
||||
}
|
||||
|
||||
type VerifyMfaTokenErrorResponse struct {
|
||||
Type string `json:"type"`
|
||||
Message string `json:"message"`
|
||||
Context struct {
|
||||
Code string `json:"code"`
|
||||
TriesLeft int `json:"triesLeft"`
|
||||
} `json:"context"`
|
||||
Level int `json:"level"`
|
||||
LevelName string `json:"level_name"`
|
||||
StatusCode int `json:"status_code"`
|
||||
DatetimeIso time.Time `json:"datetime_iso"`
|
||||
Application string `json:"application"`
|
||||
Extra []interface{} `json:"extra"`
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/Infisical/infisical-merge/packages/crypto"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/srp"
|
||||
@@ -23,8 +22,17 @@ import (
|
||||
"github.com/manifoldco/promptui"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
type params struct {
|
||||
memory uint32
|
||||
iterations uint32
|
||||
parallelism uint8
|
||||
saltLength uint32
|
||||
keyLength uint32
|
||||
}
|
||||
|
||||
// loginCmd represents the login command
|
||||
var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
@@ -55,36 +63,146 @@ var loginCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse email and password for authentication")
|
||||
}
|
||||
|
||||
userCredentials, err := getFreshUserCredentials(email, password)
|
||||
loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password)
|
||||
if err != nil {
|
||||
log.Infoln("Unable to authenticate with the provided credentials, please try again")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(userCredentials.EncryptedPrivateKey)
|
||||
tag, err := base64.StdEncoding.DecodeString(userCredentials.Tag)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
if loginTwoResponse.MfaEnabled {
|
||||
i := 1
|
||||
for i < 6 {
|
||||
mfaVerifyCode := askForMFACode()
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(loginTwoResponse.Token)
|
||||
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
|
||||
Email: email,
|
||||
MFAToken: mfaVerifyCode,
|
||||
})
|
||||
|
||||
if requestError != nil {
|
||||
util.HandleError(err)
|
||||
break
|
||||
} else if mfaErrorResponse != nil {
|
||||
if mfaErrorResponse.Context.Code == "mfa_invalid" {
|
||||
msg := fmt.Sprintf("Incorrect, MFA code. You have %v attempts left", 5-i)
|
||||
fmt.Println(msg)
|
||||
if i == 5 {
|
||||
util.PrintErrorMessageAndExit("No tries left, please try again in a bit")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if mfaErrorResponse.Context.Code == "mfa_expired" {
|
||||
util.PrintErrorMessageAndExit("Your MFA code has expired, please try logging in again")
|
||||
break
|
||||
}
|
||||
i++
|
||||
} else {
|
||||
loginTwoResponse.EncryptedPrivateKey = verifyMFAresponse.EncryptedPrivateKey
|
||||
loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion
|
||||
loginTwoResponse.Iv = verifyMFAresponse.Iv
|
||||
loginTwoResponse.ProtectedKey = verifyMFAresponse.ProtectedKey
|
||||
loginTwoResponse.ProtectedKeyIV = verifyMFAresponse.ProtectedKeyIV
|
||||
loginTwoResponse.ProtectedKeyTag = verifyMFAresponse.ProtectedKeyTag
|
||||
loginTwoResponse.PublicKey = verifyMFAresponse.PublicKey
|
||||
loginTwoResponse.Tag = verifyMFAresponse.Tag
|
||||
loginTwoResponse.Token = verifyMFAresponse.Token
|
||||
loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IV, err := base64.StdEncoding.DecodeString(userCredentials.IV)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
var decryptedPrivateKey []byte
|
||||
|
||||
paddedPassword := fmt.Sprintf("%032s", password)
|
||||
key := []byte(paddedPassword)
|
||||
if loginTwoResponse.EncryptionVersion == 1 {
|
||||
encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey)
|
||||
tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
decryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV)
|
||||
if err != nil || len(decryptedPrivateKey) == 0 {
|
||||
util.HandleError(err)
|
||||
IV, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
paddedPassword := fmt.Sprintf("%032s", password)
|
||||
key := []byte(paddedPassword)
|
||||
|
||||
decryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV)
|
||||
if err != nil || len(decryptedPrivateKey) == 0 {
|
||||
util.HandleError(err)
|
||||
}
|
||||
} else if loginTwoResponse.EncryptionVersion == 2 {
|
||||
protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
protectedKeyTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyTag)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
protectedKeyIV, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyIV)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
nonProtectedTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
nonProtectedIv, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
parameters := ¶ms{
|
||||
memory: 64 * 1024,
|
||||
iterations: 3,
|
||||
parallelism: 1,
|
||||
keyLength: 32,
|
||||
}
|
||||
|
||||
derivedKey, err := generateFromPassword(password, []byte(loginOneResponse.Salt), parameters)
|
||||
if err != nil {
|
||||
util.HandleError(fmt.Errorf("unable to generate argon hash from password [err=%s]", err))
|
||||
}
|
||||
|
||||
decryptedProtectedKey, err := crypto.DecryptSymmetric(derivedKey, protectedKey, protectedKeyTag, protectedKeyIV)
|
||||
if err != nil {
|
||||
util.HandleError(fmt.Errorf("unable to get decrypted protected key [err=%s]", err))
|
||||
}
|
||||
|
||||
encryptedPrivateKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
decryptedProtectedKeyInHex, err := hex.DecodeString(string(decryptedProtectedKey))
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
decryptedPrivateKey, err = crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
} else {
|
||||
util.PrintErrorMessageAndExit("Insufficient details to decrypt private key")
|
||||
}
|
||||
|
||||
userCredentialsToBeStored := &models.UserCredentials{
|
||||
Email: email,
|
||||
PrivateKey: string(decryptedPrivateKey),
|
||||
JTWToken: userCredentials.JTWToken,
|
||||
JTWToken: loginTwoResponse.Token,
|
||||
}
|
||||
|
||||
err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored)
|
||||
@@ -155,7 +273,7 @@ func askForLoginCredentials() (email string, password string, err error) {
|
||||
return userEmail, userPassword, nil
|
||||
}
|
||||
|
||||
func getFreshUserCredentials(email string, password string) (*api.LoginTwoResponse, error) {
|
||||
func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) {
|
||||
log.Debugln("getFreshUserCredentials:", "email", email, "password", password)
|
||||
httpClient := resty.New()
|
||||
httpClient.SetRetryCount(5)
|
||||
@@ -166,36 +284,24 @@ func getFreshUserCredentials(email string, password string) (*api.LoginTwoRespon
|
||||
srpA := hex.EncodeToString(srpClient.ComputeA())
|
||||
|
||||
// ** Login one
|
||||
loginOneRequest := api.LoginOneRequest{
|
||||
loginOneResponseResult, err := api.CallLogin1V2(httpClient, api.GetLoginOneV2Request{
|
||||
Email: email,
|
||||
ClientPublicKey: srpA,
|
||||
}
|
||||
|
||||
var loginOneResponseResult api.LoginOneResponse
|
||||
|
||||
loginOneResponse, err := httpClient.
|
||||
R().
|
||||
SetBody(loginOneRequest).
|
||||
SetResult(&loginOneResponseResult).
|
||||
Post(fmt.Sprintf("%v/v1/auth/login1", config.INFISICAL_URL))
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if loginOneResponse.StatusCode() > 299 {
|
||||
return nil, fmt.Errorf("ops, unsuccessful response code. [response=%v]", loginOneResponse)
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
// **** Login 2
|
||||
serverPublicKey_bytearray, err := hex.DecodeString(loginOneResponseResult.ServerPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
userSalt, err := hex.DecodeString(loginOneResponseResult.ServerSalt)
|
||||
userSalt, err := hex.DecodeString(loginOneResponseResult.Salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
srpClient.SetSalt(userSalt, []byte(email), []byte(password))
|
||||
@@ -203,27 +309,16 @@ func getFreshUserCredentials(email string, password string) (*api.LoginTwoRespon
|
||||
|
||||
srpM1 := srpClient.ComputeM1()
|
||||
|
||||
LoginTwoRequest := api.LoginTwoRequest{
|
||||
loginTwoResponseResult, err := api.CallLogin2V2(httpClient, api.GetLoginTwoV2Request{
|
||||
Email: email,
|
||||
ClientProof: hex.EncodeToString(srpM1),
|
||||
}
|
||||
|
||||
var loginTwoResponseResult api.LoginTwoResponse
|
||||
loginTwoResponse, err := httpClient.
|
||||
R().
|
||||
SetBody(LoginTwoRequest).
|
||||
SetResult(&loginTwoResponseResult).
|
||||
Post(fmt.Sprintf("%v/v1/auth/login2", config.INFISICAL_URL))
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if loginTwoResponse.StatusCode() > 299 {
|
||||
return nil, fmt.Errorf("ops, unsuccessful response code. [response=%v]", loginTwoResponse)
|
||||
}
|
||||
|
||||
return &loginTwoResponseResult, nil
|
||||
return &loginOneResponseResult, &loginTwoResponseResult, nil
|
||||
}
|
||||
|
||||
func shouldOverrideLoginPrompt(currentLoggedInUserEmail string) (bool, error) {
|
||||
@@ -237,3 +332,21 @@ func shouldOverrideLoginPrompt(currentLoggedInUserEmail string) (bool, error) {
|
||||
}
|
||||
return result == "Yes", err
|
||||
}
|
||||
|
||||
func generateFromPassword(password string, salt []byte, p *params) (hash []byte, err error) {
|
||||
hash = argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func askForMFACode() string {
|
||||
mfaCodePromptUI := promptui.Prompt{
|
||||
Label: "MFA verification code",
|
||||
}
|
||||
|
||||
mfaVerifyCode, err := mfaCodePromptUI.Run()
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
return mfaVerifyCode
|
||||
}
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
---
|
||||
title: "Circle CI"
|
||||
description: "How to automatically sync secrets from Infisical into your CircleCI project."
|
||||
---
|
||||
|
||||
Coming soon.
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
## Navigate to your project's integrations tab
|
||||
|
||||

|
||||
|
||||
## Authorize Infisical for CircleCI
|
||||
|
||||
Obtain a Fly.io access token in Access Tokens
|
||||
|
||||

|
||||

|
||||
|
||||
Press on the Fly.io tile and input your Fly.io access token to grant Infisical access to your Fly.io account.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
If this is your project's first cloud integration, then you'll have to grant
|
||||
Infisical access to your project's environment variables. Although this step
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which Fly.io app and press create integration to start syncing secrets to Fly.io.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -21,6 +21,8 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
|
||||
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
|
||||
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
|
||||
| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
|
||||
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
|
||||
| [React](/integrations/frameworks/react) | Framework | Available |
|
||||
| [Vue](/integrations/frameworks/vue) | Framework | Available |
|
||||
| [Express](/integrations/frameworks/express) | Framework | Available |
|
||||
@@ -39,8 +41,6 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| GCP | Cloud | Coming soon |
|
||||
| Azure | Cloud | Coming soon |
|
||||
| DigitalOcean | Cloud | Coming soon |
|
||||
| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
|
||||
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon |
|
||||
| TravisCI | CI/CD | Coming soon |
|
||||
| GitHub Actions | CI/CD | Coming soon |
|
||||
| Jenkins | CI/CD | Coming soon |
|
||||
|
||||
@@ -7,22 +7,50 @@ Infisical stores a range of data namely user, secrets, keys, organization, proje
|
||||
|
||||
## Users
|
||||
|
||||
The `User` model includes the fields `email`, `firstName`, `lastName`, `publicKey`, `encryptedPrivateKey`, `iv`, `tag`, `salt`, `verifier`, and `refreshVersion`.
|
||||
The `User` model includes the fields `email`, `firstName`, `lastName`, `publicKey`, `encryptionVersion`, `protectedKey`, `protectedKeyIV`, `protectedKeyTag`, `encryptedPrivateKey`, `iv`, `tag`, `salt`, `verifier`, and `refreshVersion`.
|
||||
|
||||
Infisical makes a usability-security tradeoff to give users convenient access to public-private key pairs across different devices upon login, solving key-storage and transfer challenges across device and browser mediums, in exchange for it storing `encryptedPrivateKey`. In any case, private keys are symmetrically encrypted locally by user passwords which are not sent to the server — this is done with SRP.
|
||||
Infisical makes a usability-security tradeoff that is to give users convenient access to public-private key pairs across different devices upon login, solving key-storage and transfer challenges across device and browser mediums, in exchange for it storing `encryptedPrivateKey`.
|
||||
|
||||
<Note>
|
||||
`encryptedPrivateKey` is obtained by symmetrically encrypting the user's
|
||||
private key locally with a protected key which is encrypted by the key derived
|
||||
from the user's password and salt. Encryption is done via `AES256-GCM` and key
|
||||
derivation via `argon2id`. The user's password is not sent to the server —
|
||||
this is done with SRP.
|
||||
</Note>
|
||||
|
||||
## Secrets
|
||||
|
||||
The `Secret` model includes the fields `workspace`, `type`, `user`, `environment`, `secretKeyCiphertext`, `secretKeyIV`, `secretKeyTag`, `secretKeyHash`, `secretValueCiphertext`, `secretValueIV`, `secretValueTag`, and `secretValueHash`.
|
||||
The `Secret` model includes the fields `workspace`, `type`, `user`, `environment`, `secretKeyCiphertext`, `secretKeyIV`, `secretKeyTag`, `secretValueCiphertext`, `secretValueIV`, and `secretValueTag`.
|
||||
|
||||
Each secret is symmetrically encrypted by the key of the project that it belongs to; that key's encrypted copies are stored in a separate `Key` collection.
|
||||
|
||||
## Keys
|
||||
## Project Keys
|
||||
|
||||
The `Key` model includes the fields `encryptedKey`, `nonce`, `sender`, `receiver`, and `workspace`.
|
||||
|
||||
Infisical stores copies of project keys, one for each member of a project, encrypted under each member's public key.
|
||||
|
||||
## Bots
|
||||
|
||||
The `Bot` model contains the fields `name`, `workspace`, `isActive`, `publicKey`, `encryptedPrivateKey`, `iv`, and `tag`.
|
||||
|
||||
Each project comes with a bot that has its own public-private key pair; its private key is encrypted by the server's symmetric key. If needed, a user can opt-in to share their project key with the bot (i.e. Infisical) to give the platform access to the project's secrets.
|
||||
|
||||
<Note>
|
||||
Sharing secrets with Infisical so they can be synced to integrations like
|
||||
Vercel, GitHub, and Netlify is something we make sure users consent to before
|
||||
opting in.
|
||||
</Note>
|
||||
|
||||
## Organizations and Workspaces
|
||||
|
||||
The `Organization`, `Workspace`, `MembershipOrg`, and `Membership` models contain enrollment information for organizations and projects; they are used to check if users are authorized to retrieve select secrets.
|
||||
|
||||
## Service Tokens
|
||||
|
||||
The `ServiceTokenData` model contains data for service tokens that enable users to fetch secrets from a particular project and environment; each service token data record includes an (encrypted) copy of the project key that it is bound to as well as a validation hash for `bcrypt`.
|
||||
|
||||
## API Keys
|
||||
|
||||
The `APIKeyData` model contains data for API keys that enable users to interact with [Infisical's Open API](https://infisical.com/docs/api-reference/overview/introduction); each API key data record includes a validation hash for `bcrypt`.
|
||||
|
||||
@@ -5,7 +5,11 @@ description: "Quick explanation of how Infisical works."
|
||||
|
||||
## Signup
|
||||
|
||||
During account signup, a user confirms their email address via OTP, generates a public-private key pair to be stored locally (private keys are symmetrically encrypted by the user's newly-made password), and forwards SRP-related values and user identifier information to the server. This includes `email`, `firstName`, `lastName`, `publicKey`, `encryptedPrivateKey`, `iv`, `tag`, `salt`, `verifier`, and `organizationName`.
|
||||
During account signup, a user confirms their email address via OTP, generates a public-private key pair to be stored locally, generates a user salt, generates a 256-bit key, and enters their password.
|
||||
|
||||
The 256-bit key is used to encrypt the private key; the 256-bit key itself is then encrypted by a key generated from the user's password and salt with key derivation function `argon2id`. The resulting, 256-bit key the protected key.
|
||||
|
||||
The encrypted private key, protected key, user identifier information, and SRP details are forwarded to the server.
|
||||
|
||||
Once authenticated via SRP, a user is issued a JWT and refresh token. The JWT token is stored in browser memory under a write-only class `SecurityClient` that appends the token to all future outbound requests requiring authentication. The refresh token is stored in an `HttpOnly` cookie and included in future requests to `/api/token` for JWT token renewal. This design side-steps potential XSS attacks on local storage.
|
||||
|
||||
|
||||
@@ -24,114 +24,88 @@ Before you can deploy the Helm chart, you must fill out the required environment
|
||||
Refer to the available [environment variables](../../self-hosting/configuration/envars) to learn more
|
||||
|
||||
<Accordion title="values.yaml">
|
||||
[View all available Helm chart values parameters](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical)
|
||||
```yaml
|
||||
#####
|
||||
# INFISICAL K8 DEFAULT VALUES FILE
|
||||
# PLEASE REPLACE VALUES/EDIT AS REQUIRED
|
||||
#####
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
frontend:
|
||||
enabled: true
|
||||
name: frontend
|
||||
podAnnotations: {}
|
||||
deploymentAnnotations: {}
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/frontend
|
||||
pullPolicy: Always
|
||||
tag: "latest" # It it highly recommended to select a specific tag for prod deployment so it is easy to rollback: https://hub.docker.com/r/infisical/frontend/tags
|
||||
# kubeSecretRef: some-kube-secret-name
|
||||
tag: "latest"
|
||||
pullPolicy: IfNotPresent
|
||||
kubeSecretRef: ""
|
||||
service:
|
||||
# type of the frontend service
|
||||
type: ClusterIP
|
||||
# define the nodePort if service type is NodePort
|
||||
# nodePort:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
|
||||
frontendEnvironmentVariables:
|
||||
SITE_URL: infisical.local
|
||||
|
||||
backend:
|
||||
enabled: true
|
||||
name: backend
|
||||
podAnnotations: {}
|
||||
deploymentAnnotations: {}
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/backend
|
||||
pullPolicy: Always
|
||||
tag: "latest" # It it highly recommended to select a specific tag for prod deployment so it is easy to rollback: https://hub.docker.com/r/infisical/backend/tags
|
||||
# kubeSecretRef: some-kube-secret-name
|
||||
service:
|
||||
annotations: {}
|
||||
|
||||
mongodb:
|
||||
name: mongodb
|
||||
podAnnotations: {}
|
||||
image:
|
||||
repository: mongo
|
||||
tag: "latest"
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
kubeSecretRef: ""
|
||||
service:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
|
||||
# By default the backend will be connected to a Mongo instance in the cluster.
|
||||
# However, it is recommended to add a managed document DB connection string because the DB instance in the cluster does not have persistence yet ( data will be deleted on next deploy).
|
||||
# Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
|
||||
mongodbConnection: {}
|
||||
# externalMongoDBConnectionString: <>
|
||||
backendEnvironmentVariables:
|
||||
ENCRYPTION_KEY: MUST_REPLACE
|
||||
JWT_SIGNUP_SECRET: MUST_REPLACE
|
||||
JWT_REFRESH_SECRET: MUST_REPLACE
|
||||
JWT_AUTH_SECRET: MUST_REPLACE
|
||||
JWT_SERVICE_SECRET: MUST_REPLACE
|
||||
SMTP_HOST: MUST_REPLACE
|
||||
SMTP_PORT: 587
|
||||
SMTP_SECURE: false
|
||||
SMTP_FROM_NAME: Infisical
|
||||
SMTP_FROM_ADDRESS: MUST_REPLACE
|
||||
SMTP_USERNAME: MUST_REPLACE
|
||||
SMTP_PASSWORD: MUST_REPLACE
|
||||
SITE_URL: infisical.local
|
||||
|
||||
## Mongo DB persistence
|
||||
mongodb:
|
||||
enabled: true
|
||||
|
||||
## By default the backend will be connected to a Mongo instance within the cluster
|
||||
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
|
||||
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
|
||||
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
|
||||
mongodbConnection:
|
||||
externalMongoDBConnectionString: ""
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
hostName: example.com # replace with your domain
|
||||
frontend:
|
||||
# cert-manager.io/issuer: letsencrypt-nginx
|
||||
hostName: infisical.local ## <- Replace with your own domain
|
||||
frontend:
|
||||
path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
path: /api
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
# - secretName: letsencrypt-nginx
|
||||
# hosts:
|
||||
# - infisical.local
|
||||
|
||||
|
||||
## Complete Ingress example
|
||||
# ingress:
|
||||
# enabled: true
|
||||
# annotations:
|
||||
# kubernetes.io/ingress.class: "nginx"
|
||||
# cert-manager.io/issuer: letsencrypt-nginx
|
||||
# hostName: k8.infisical.com
|
||||
# frontend:
|
||||
# path: /
|
||||
# pathType: Prefix
|
||||
# backend:
|
||||
# path: /api
|
||||
# pathType: Prefix
|
||||
# tls:
|
||||
# - secretName: letsencrypt-nginx
|
||||
# hosts:
|
||||
# - k8.infisical.com
|
||||
|
||||
###
|
||||
### YOU MUST FILL IN ALL SECRETS BELOW
|
||||
###
|
||||
backendEnvironmentVariables:
|
||||
# Required keys for platform encryption/decryption ops. Replace with nacl sk keys
|
||||
ENCRYPTION_KEY: MUST_REPLACE
|
||||
|
||||
# JWT
|
||||
# Required secrets to sign JWT tokens
|
||||
JWT_SIGNUP_SECRET: MUST_REPLACE
|
||||
JWT_REFRESH_SECRET: MUST_REPLACE
|
||||
JWT_AUTH_SECRET: MUST_REPLACE
|
||||
|
||||
# Mail/SMTP
|
||||
# Required to send emails
|
||||
SMTP_HOST: MUST_REPLACE
|
||||
SMTP_NAME: MUST_REPLACE
|
||||
SMTP_USERNAME: MUST_REPLACE
|
||||
SMTP_PASSWORD: MUST_REPLACE
|
||||
|
||||
frontendEnvironmentVariables: {}
|
||||
|
||||
mailhog:
|
||||
enabled: false
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: ["next.config.js"]
|
||||
}
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
@@ -87,6 +92,7 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
},
|
||||
ignorePatterns: ['next.config.js'],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
**/
|
||||
const { i18n } = require("./next-i18next.config.js");
|
||||
const path = require('path');
|
||||
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
@@ -65,7 +67,33 @@ module.exports = {
|
||||
},
|
||||
];
|
||||
},
|
||||
webpack: (config, { isServer, webpack }) => {
|
||||
webpack: (config, { isServer, webpack }) => { // config
|
||||
config.module.rules.push({
|
||||
test: /\.wasm$/,
|
||||
loader: "base64-loader",
|
||||
type: "javascript/auto",
|
||||
});
|
||||
|
||||
config.module.noParse = /\.wasm$/;
|
||||
|
||||
config.module.rules.forEach((rule) => {
|
||||
(rule.oneOf || []).forEach((oneOf) => {
|
||||
if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) {
|
||||
oneOf.exclude.push(/\.wasm$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!isServer) {
|
||||
config.resolve.fallback.fs = false;
|
||||
}
|
||||
|
||||
// Perform customizations to webpack config
|
||||
config.plugins.push(
|
||||
new webpack.IgnorePlugin({ resourceRegExp: /\/__tests__\// })
|
||||
);
|
||||
|
||||
// Important: return the modified config
|
||||
return config;
|
||||
},
|
||||
i18n,
|
||||
|
||||
805
frontend/package-lock.json
generated
805
frontend/package-lock.json
generated
@@ -27,14 +27,18 @@
|
||||
"@radix-ui/react-progress": "^1.0.1",
|
||||
"@radix-ui/react-select": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@radix-ui/react-tabs": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
"@reduxjs/toolkit": "^1.8.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.46.0",
|
||||
"@tanstack/react-query": "^4.23.0",
|
||||
"@types/argon2-browser": "^1.18.1",
|
||||
"add": "^2.0.6",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"axios": "^0.27.2",
|
||||
"axios-auth-refresh": "^3.3.3",
|
||||
"base64-loader": "^1.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cookies": "^0.8.0",
|
||||
"cva": "npm:class-variance-authority@^0.4.0",
|
||||
@@ -2338,54 +2342,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz",
|
||||
"integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz",
|
||||
"integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz",
|
||||
"integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz",
|
||||
@@ -2402,294 +2358,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz",
|
||||
"integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz",
|
||||
"integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz",
|
||||
"integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz",
|
||||
"integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz",
|
||||
"integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz",
|
||||
"integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz",
|
||||
"integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz",
|
||||
"integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz",
|
||||
"integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz",
|
||||
"integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz",
|
||||
"integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
|
||||
@@ -3292,36 +2960,6 @@
|
||||
"integrity": "sha512-NXGXGFGiOKEnvBIHq9cdFTKbHO2/4B3Zd9K27M7j1DioIQVar7oVRqZMYs0h3XMVEZLwjjkdAtqRPCzzd3RtXg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@next/swc-android-arm-eabi": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz",
|
||||
"integrity": "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-android-arm64": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz",
|
||||
"integrity": "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz",
|
||||
@@ -3337,156 +2975,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz",
|
||||
"integrity": "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-freebsd-x64": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz",
|
||||
"integrity": "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm-gnueabihf": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz",
|
||||
"integrity": "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz",
|
||||
"integrity": "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz",
|
||||
"integrity": "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz",
|
||||
"integrity": "sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.4.tgz",
|
||||
"integrity": "sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz",
|
||||
"integrity": "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz",
|
||||
"integrity": "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz",
|
||||
"integrity": "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -4271,6 +3759,26 @@
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz",
|
||||
"integrity": "sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.2.tgz",
|
||||
@@ -6935,6 +6443,11 @@
|
||||
"@testing-library/dom": ">=7.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/argon2-browser": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.1.tgz",
|
||||
"integrity": "sha512-PZffP/CqH9m2kovDSRQMfMMxUC3V98I7i7/caa0RB0/nvsXzYbL9bKyqZpNMFmLFGZslROlG1R60ONt7abrwlA=="
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
|
||||
@@ -8343,6 +7856,11 @@
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/argon2-browser": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz",
|
||||
"integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw=="
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
@@ -8937,6 +8455,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/base64-loader": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64-loader/-/base64-loader-1.0.0.tgz",
|
||||
"integrity": "sha512-p32+F8dg+ANGx7s8QsZS74ZPHfIycmC2yZcoerzFgbersIYWitPbbF39G6SBx3gyvzyLH5nt1ooocxr0IHuWKA=="
|
||||
},
|
||||
"node_modules/before-after-hook": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
||||
@@ -24279,27 +23802,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz",
|
||||
"integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
|
||||
},
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz",
|
||||
"integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz",
|
||||
"integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz",
|
||||
@@ -24307,132 +23809,6 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz",
|
||||
"integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz",
|
||||
"integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz",
|
||||
"integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ia32": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz",
|
||||
"integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-loong64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz",
|
||||
"integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-mips64el": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz",
|
||||
"integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ppc64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz",
|
||||
"integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-riscv64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz",
|
||||
"integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-s390x": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz",
|
||||
"integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/netbsd-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/openbsd-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/sunos-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-arm64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz",
|
||||
"integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-ia32": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz",
|
||||
"integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-x64": {
|
||||
"version": "0.16.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz",
|
||||
"integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
|
||||
@@ -24883,84 +24259,12 @@
|
||||
"integrity": "sha512-NXGXGFGiOKEnvBIHq9cdFTKbHO2/4B3Zd9K27M7j1DioIQVar7oVRqZMYs0h3XMVEZLwjjkdAtqRPCzzd3RtXg==",
|
||||
"dev": true
|
||||
},
|
||||
"@next/swc-android-arm-eabi": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz",
|
||||
"integrity": "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-android-arm64": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz",
|
||||
"integrity": "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-darwin-arm64": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz",
|
||||
"integrity": "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-darwin-x64": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz",
|
||||
"integrity": "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-freebsd-x64": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz",
|
||||
"integrity": "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-arm-gnueabihf": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz",
|
||||
"integrity": "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-arm64-gnu": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz",
|
||||
"integrity": "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-arm64-musl": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz",
|
||||
"integrity": "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-x64-gnu": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz",
|
||||
"integrity": "sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-x64-musl": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.4.tgz",
|
||||
"integrity": "sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-win32-arm64-msvc": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz",
|
||||
"integrity": "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-win32-ia32-msvc": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz",
|
||||
"integrity": "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-win32-x64-msvc": {
|
||||
"version": "12.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz",
|
||||
"integrity": "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg==",
|
||||
"optional": true
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -25552,6 +24856,22 @@
|
||||
"@radix-ui/react-use-size": "1.0.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-tabs": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz",
|
||||
"integrity": "sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-toast": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.2.tgz",
|
||||
@@ -27446,6 +26766,11 @@
|
||||
"@babel/runtime": "^7.12.5"
|
||||
}
|
||||
},
|
||||
"@types/argon2-browser": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.1.tgz",
|
||||
"integrity": "sha512-PZffP/CqH9m2kovDSRQMfMMxUC3V98I7i7/caa0RB0/nvsXzYbL9bKyqZpNMFmLFGZslROlG1R60ONt7abrwlA=="
|
||||
},
|
||||
"@types/aria-query": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
|
||||
@@ -28577,6 +27902,11 @@
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true
|
||||
},
|
||||
"argon2-browser": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz",
|
||||
"integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw=="
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
@@ -29012,6 +28342,11 @@
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"base64-loader": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64-loader/-/base64-loader-1.0.0.tgz",
|
||||
"integrity": "sha512-p32+F8dg+ANGx7s8QsZS74ZPHfIycmC2yZcoerzFgbersIYWitPbbF39G6SBx3gyvzyLH5nt1ooocxr0IHuWKA=="
|
||||
},
|
||||
"before-after-hook": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
||||
|
||||
@@ -34,14 +34,18 @@
|
||||
"@radix-ui/react-progress": "^1.0.1",
|
||||
"@radix-ui/react-select": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@radix-ui/react-tabs": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
"@reduxjs/toolkit": "^1.8.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.46.0",
|
||||
"@types/argon2-browser": "^1.18.1",
|
||||
"@tanstack/react-query": "^4.23.0",
|
||||
"add": "^2.0.6",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"axios": "^0.27.2",
|
||||
"axios-auth-refresh": "^3.3.3",
|
||||
"base64-loader": "^1.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cookies": "^0.8.0",
|
||||
"cva": "npm:class-variance-authority@^0.4.0",
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface SecretDataProps {
|
||||
value: string | undefined;
|
||||
valueOverride: string | undefined;
|
||||
id: string;
|
||||
idOverride?: string;
|
||||
comment: string;
|
||||
tags: Tag[];
|
||||
}
|
||||
235
frontend/public/images/dragon-book.svg
Normal file
235
frontend/public/images/dragon-book.svg
Normal file
@@ -0,0 +1,235 @@
|
||||
<svg width="1096" height="1024" viewBox="0 0 1096 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M652.84 516.37L655.57 496.81H661.39L663.89 486.77H667.21L669.61 476.51C669.61 476.51 712.12 478.93 749.44 484.55C786.76 490.17 826.43 499.87 826.43 499.87C826.43 499.87 830.12 453.85 831.88 427.3C833.63 400.75 841.09 361.75 841.09 361.75L847.03 381.57L854.24 352.91L867.19 387.39C867.19 387.39 867.68 364.88 871.36 357.72C875.04 350.56 875.72 339.86 876.39 341.44C877.06 343.02 902.81 414.74 920.98 445.7C939.15 476.66 947.96 494.14 947.96 494.14C947.96 494.14 955.48 504.33 958.31 510.78C961.14 517.24 959.89 522.41 958.92 525.31C957.95 528.21 958 533.77 950.07 540.91C942.15 548.04 933.07 549.85 879.61 540.42C826.15 530.99 774.34 526.59 742.93 524.83C711.52 523.07 652.86 516.38 652.86 516.38L652.84 516.37Z" fill="#E5F7B7"/>
|
||||
<path d="M655.09 514.47C655.09 514.47 698.6 509.96 773.53 518.61C848.46 527.26 890.16 538.4 912.52 540.9C934.88 543.41 944.76 538.47 944.76 538.47C944.76 538.47 949.79 536.8 952.68 532.69C955.57 528.58 956.14 523.64 956.27 521.98C956.4 520.31 956.68 518.69 956.27 517.45C955.86 516.21 952.45 510.6 951.59 509.6C950.73 508.6 950.12 507.13 950.12 507.13C950.12 507.13 953.73 510.22 956.48 514.16C959.23 518.1 959.39 523.39 958.89 525.3C958.39 527.21 958.15 540.12 943.24 545.98C933.61 549.76 917.62 547.09 871.8 539.07C825.98 531.05 793.78 527.77 777.34 526.55C760.9 525.33 728.21 522.38 713.15 521.1C698.09 519.82 655.08 514.46 655.08 514.46L655.09 514.47Z" fill="#B4BF8F"/>
|
||||
<path d="M702.2 888.02C702.2 888.02 576.88 875.18 575.97 874.24C575.06 873.29 573.81 874.27 575.24 866.6C576.67 858.93 610.77 685.18 616.71 641.48C622.65 597.78 636.23 541.36 638.48 529.91C640.73 518.46 639.62 515.85 647.67 516.03C655.72 516.21 739.33 524.73 773.75 527.61C808.17 530.49 894.49 543.5 912.38 546.01C930.27 548.52 945.04 547.55 952.5 541.21C959.96 534.87 965.23 526.39 965.59 517.28C965.95 508.17 962.71 497.6 962.71 497.6L967.15 493.31C967.15 493.31 974.07 505.43 968.81 526.58C963.55 547.72 920.27 678.91 896.02 778.85C871.78 878.79 863.96 894.74 864.14 899.4C864.32 904.06 858.14 909.92 843.14 910.51C828.14 911.09 823.58 910.18 819.72 905.03C815.86 899.88 702.18 888.02 702.18 888.02H702.2Z" fill="url(#paint0_linear_121_754)"/>
|
||||
<path d="M950.06 540.9C956.45 534.8 957.48 529.26 958.91 525.3C960.34 521.35 960.29 514.3 957.21 508.41C955.51 505.15 938.98 479.83 927.55 458.57C916.12 437.31 895.58 391.31 895.58 391.31C895.58 391.31 906.51 352.41 907.19 350.81C907.87 349.21 909.75 353.7 912.65 362.23C915.55 370.76 936.81 423.13 943.46 437.88C950.11 452.63 955.33 462.79 955.33 462.79C955.33 462.79 961.09 491.47 963.53 500.62C965.97 509.76 967.4 518.52 964 525.31C960.6 532.1 960.14 533.92 956.32 537.56C952.5 541.2 950.05 540.9 950.05 540.9H950.06Z" fill="url(#paint1_linear_121_754)"/>
|
||||
<path d="M940 480.33C940 480.33 955.17 462.92 963.15 453.35C971.13 443.77 978.18 441.09 985.36 441.48C992.54 441.87 999.94 449.97 1008.96 464.17C1017.98 478.37 1021.23 482.04 1021.23 482.04C1021.23 482.04 1012.53 469.81 998.42 470.88C984.31 471.95 978.66 474.74 974.5 482.04C970.33 489.33 956.32 506.78 956.32 506.78L940 482.17V480.34V480.33Z" fill="#DEEB34"/>
|
||||
<path d="M1048.16 768.08C1048.16 768.08 1061.91 744.67 1063.97 734.72C1066.04 724.77 1068.1 712.8 1064.72 701.98C1061.34 691.16 1052.28 678.21 1052.28 678.21C1052.28 678.21 1075.23 682.53 1086.88 704.5C1098.52 726.47 1092.33 742.05 1089.32 753.13C1086.32 764.21 1079.74 782.23 1079.74 782.23C1079.74 782.23 1080.49 762.7 1079.74 755.38C1078.99 748.06 1077.67 744.3 1070.91 749.37C1064.15 754.44 1051.2 767.39 1051.2 767.39L1048.15 768.08H1048.16Z" fill="#DEEB34"/>
|
||||
<path d="M1000.21 471.41C1000.21 471.41 1008.15 471.23 1016.68 478.54C1025.21 485.86 1025.5 495.62 1022.56 510.77C1019.63 525.92 1010.68 560.26 1008.56 576.38C1006.44 592.5 1001.9 612.83 1009.63 634.86C1017.36 656.89 1038.81 670.03 1038.81 670.03C1038.81 670.03 1001.4 651.73 992.04 632.15C982.69 612.57 980.25 596.24 985.47 570.71C990.69 545.19 998.42 516.97 1001.7 500.93C1004.99 484.89 1004.21 477.56 1002.86 475.43C1001.51 473.3 1000.2 471.39 1000.2 471.39L1000.21 471.41Z" fill="#DEEB34"/>
|
||||
<path d="M1003.79 497.89C1004.96 503.21 1014.23 528.72 1009.61 558.42C1005 588.12 1021.23 519.51 1023.08 507.97C1024.93 496.44 1026.24 488.41 1019.19 481.05C1012.14 473.69 1004.64 471.81 1002.43 471.61L1000.21 471.41C1000.21 471.41 1003.53 475.9 1004.15 482.42C1004.76 488.94 1003.79 497.89 1003.79 497.89V497.89Z" fill="#AAAE11"/>
|
||||
<path d="M943.41 478.52L958.16 500.89L954.94 505.85L940 480.33L943.41 478.52Z" fill="#AAAE11"/>
|
||||
<path d="M983.57 442.52L1001.12 471.42C1001.12 471.42 1008.92 473.64 1013.96 476.4C1019 479.16 1017.9 476.82 1016.67 475.45C1015.44 474.08 998.61 446.45 996.02 445.7C993.43 444.95 986.21 443.56 985.78 442.52C985.35 441.48 983.56 442.52 983.56 442.52H983.57Z" fill="#AAAE11"/>
|
||||
<path d="M987.14 615.44C989.43 621.88 993.41 627.85 999.81 635.72C1006.21 643.59 1015.88 647.36 1015.88 647.36L1025.94 661.42C1025.94 661.42 1006.91 651.69 997.13 638.43C987.35 625.17 987.14 615.44 987.14 615.44V615.44Z" fill="#AAAE11"/>
|
||||
<path d="M1087.14 753.97C1089.82 743.76 1086.5 723.73 1077.95 711.87C1069.4 700.01 1064.55 696.94 1064.55 696.94L1055.24 678.2C1055.24 678.2 1074.61 684.02 1084.89 701.07C1095.17 718.12 1094.36 731.04 1093.15 737.31C1091.93 743.58 1087.14 753.96 1087.14 753.96V753.97Z" fill="#AAAE11"/>
|
||||
<path d="M663.11 494.49C663.11 494.49 693.21 493.65 740.1 497.72C786.99 501.79 880.38 524.04 910.43 533.48C910.43 533.48 826.19 514.19 796.33 509.37C766.47 504.54 699.89 496.46 682.16 496.68C664.42 496.9 663.12 494.49 663.12 494.49H663.11Z" fill="#B4BF8F"/>
|
||||
<path d="M870.81 367.6C870.81 367.6 878.9 401.49 895.58 431.57C912.25 461.65 943.52 499.9 947.27 503.92C951.02 507.94 926.63 481.64 915.5 468.77C904.37 455.9 883.1 416.83 876.66 407.89C870.22 398.94 867.26 384.98 867.26 384.98C867.26 384.98 868.92 367.47 869.24 367.6C869.56 367.73 870.81 367.6 870.81 367.6Z" fill="#B4BF8F"/>
|
||||
<path d="M850.87 369.64C850.87 369.64 862.73 403.37 880.18 430.63C897.63 457.89 938.14 501.85 938.14 501.85C938.14 501.85 908.67 476.15 876.07 430.63C843.47 385.12 847.76 378.65 847.76 378.65L850.88 369.63L850.87 369.64Z" fill="#B4BF8F"/>
|
||||
<path d="M934.45 530.61L940.81 506.78C940.81 506.78 949.74 513.68 950.15 517.88C950.56 522.09 950.05 527.02 943.27 529.69C936.49 532.36 934.44 530.61 934.44 530.61H934.45Z" fill="#B4BF8F"/>
|
||||
<path d="M4.32007 678.12C4.32007 678.12 12.2001 604.24 32.8901 544.94C53.5801 485.64 72.8001 435.61 101.45 389.31C130.1 343.01 180.92 284.36 206.21 262.03C231.5 239.7 240.77 233.22 254.68 233.08C268.58 232.94 271.24 244.93 272.74 262.03C274.25 279.14 283.28 334.28 288.09 356.38C292.9 378.48 299.63 407.57 299.63 407.57C299.63 407.57 315.06 348.37 339.66 299.3C364.26 250.23 388.38 226.79 400.36 222.99C412.34 219.19 420.25 221.01 429.33 226.2C438.41 231.39 448.75 249.11 448.75 249.11C448.75 249.11 464.7 234.94 474.2 225.6C483.7 216.26 502.13 185.22 502.13 185.22C502.13 185.22 458.61 169.33 426.14 132.18C393.67 95.03 380.17 66.95 379.66 58.54L379.15 50.13C379.15 50.13 410.82 78.46 446.68 100.33C482.54 122.2 504.02 125.53 519.05 132.42C534.08 139.31 537.07 137.41 538.53 142.89C539.99 148.37 542.4 152.56 542.4 152.56C542.4 152.56 553.11 145.17 574 145.35C594.89 145.53 597.95 147.95 597.95 147.95C597.95 147.95 580.33 126.46 566.14 91.4C551.95 56.34 548.35 31.77 547.84 25.4C547.33 19.03 546.64 9.45996 546.64 9.45996C546.64 9.45996 571.28 59.04 601.86 90.38C632.44 121.72 642.58 118.49 646.2 127.84C649.81 137.19 645.18 144.7 644.42 149.52C643.66 154.34 644.49 162.29 644.49 162.29C644.49 162.29 643.9 163.1 650.01 169.09C656.12 175.08 673.12 191.87 678.77 203.51C684.42 215.14 683.39 220.74 680.68 224.11C677.97 227.48 677.82 227.29 678.26 229C678.7 230.71 681.45 233.34 681.06 239.26C680.67 245.18 679.25 255.27 679.86 257.33C680.47 259.39 691.4 269.61 703.58 280.13C715.76 290.65 720.64 294.86 720.64 294.86C720.64 294.86 728.68 286.96 738.26 290.28C747.85 293.6 753.23 300.13 751.72 314.62C750.21 329.11 742.5 340.64 742.5 340.64C742.5 340.64 741.54 347.97 738.26 355.37C734.98 362.77 730.49 366.31 730.49 366.31L731.16 353.55C731.16 353.55 726.75 361.63 724.62 366.68C722.49 371.73 711.54 379.98 697.43 380.9C683.32 381.83 680.12 381.21 680.12 381.21L677.59 382.87L677 378.97C677 378.97 660.44 373.73 653.14 360C645.84 346.27 633.44 330.26 621.74 319.58C610.04 308.9 611.63 307.76 598.97 308.28C586.31 308.8 557.97 319.19 543.43 306.73C528.89 294.27 511.52 264.64 511.52 264.64C511.52 264.64 471.7 313.6 460.34 393.35C448.98 473.1 458.06 556.12 486 648.88C513.95 741.64 522.2 757.54 529.53 796.59C536.86 835.64 539.67 862.09 539.67 862.09C539.67 862.09 567.37 824.2 567.87 794.79C568.37 765.38 556.55 731.07 551.51 709.5C546.47 687.94 550.11 672.68 553.51 662.25C556.91 651.82 561.75 646.79 564.26 645C566.77 643.21 571.25 641.5 571.25 641.5L566.59 653.56C566.59 653.56 574.91 655.28 580.39 659.25C585.87 663.22 589.31 671.48 588.24 676.36C587.17 681.24 587.02 687.11 587.02 687.11C587.02 687.11 589.53 679.94 598.49 672.6C607.45 665.25 613.25 662.4 613.25 662.4L574.49 870.52C574.49 870.52 573.8 874.4 577.98 875C582.16 875.6 625.21 881.4 625.21 881.4L702.17 888.05C702.17 888.05 703.17 884.06 704.63 878.27C706.09 872.48 714 856.51 714 856.51L714.95 871.41C714.95 871.41 719.69 872.79 723.82 874.62C727.95 876.45 731.92 878.27 731.92 878.27C731.92 878.27 734.67 867.28 737.58 863.62C740.48 859.95 746.41 853.74 746.41 853.74L742.04 870.04C742.04 870.04 746.47 870.8 751.96 874.32C757.45 877.84 763.06 885.17 763.06 885.17C763.06 885.17 766.98 879.67 769.56 877.53C772.13 875.39 774.54 874.63 774.54 874.63L778.1 862.86L782.69 876.01C782.69 876.01 791.56 877.46 794.77 884.07C797.98 890.68 800.58 897.54 800.58 897.54C800.58 897.54 806.63 897.56 809.72 899.55C812.81 901.54 814.19 907.35 812.5 911.47C810.82 915.6 801.02 930.12 788.88 929.66C776.73 929.2 767.41 922.8 767.41 922.8C767.41 922.8 762.81 933.48 749.21 943.11C735.61 952.74 712.07 960.08 710.39 959.93C708.71 959.78 709.79 990.12 706.8 1005.45C703.81 1020.78 699.71 1036.66 699.71 1036.66H160.76C160.76 1036.66 177.03 995.13 214.36 955.09C251.69 915.05 280.88 884.87 310.37 823.7C339.87 762.54 342.19 707.19 347.71 675.91C353.23 644.63 352.3 575.71 352.3 575.71C352.3 575.71 333.93 565.74 321.48 555.6C309.03 545.46 300.72 542.18 300.72 542.18C300.72 542.18 292.35 554.55 283.61 575.06C274.86 595.57 264.21 617.67 264.21 617.67C264.21 617.67 265.01 597.97 267.91 573.12C270.81 548.27 275.4 520.48 275.4 520.48C275.4 520.48 258 506.46 242.77 520C227.54 533.53 220.54 555.53 214.74 577.04C208.94 598.55 202.68 625.64 202.68 625.64C202.68 625.64 199.27 572.19 196.25 549.74C193.23 527.28 186.99 517.62 176.06 521.46C165.13 525.3 155.03 536.71 143.19 575.08C131.35 613.45 127.48 653.9 126.51 658.73C125.54 663.56 124.09 684.11 124.09 684.11C124.09 684.11 114.07 639.16 111.83 612.57C109.59 585.99 107.17 558.19 104.75 550.22C102.33 542.24 93.4401 532.1 80.3401 540.07C67.2401 548.04 53.7401 565.98 35.8701 601.94C18.0001 637.9 4.30005 678.17 4.30005 678.17L4.32007 678.12Z" fill="#A6CF3D"/>
|
||||
<path d="M524.59 1033.99C524.59 1033.99 532.39 1015.81 533.63 1010.65C534.88 1005.48 537.02 999.07 537.02 999.07L556.09 982.32L551.1 998.54C551.1 998.54 572.48 987.8 591.73 961.63C610.97 935.46 614.36 925.66 619.35 909.62C624.34 893.58 625.24 881.38 625.24 881.38L700.6 887.89C700.6 887.89 706.99 916.06 708.08 945.17C709.17 974.28 708.29 997.41 704.66 1014.4C701.02 1031.39 699.71 1036.64 699.71 1036.64L524.59 1034V1033.99Z" fill="#DEEB34"/>
|
||||
<path d="M538.95 859.6C538.95 859.6 560.82 827.85 564.76 798.73C568.7 769.61 563.31 746.97 555.52 726.02C547.73 705.07 547.11 687.06 550.32 674.64C553.53 662.22 558.78 652.69 561.53 648.83C564.28 644.97 571.27 641.47 571.27 641.47C571.27 641.47 568.42 647.29 567.26 649.67C566.1 652.05 566.61 653.53 566.61 653.53C566.61 653.53 575.74 654.15 580.41 659.22C585.08 664.29 584.81 668.84 586.54 672.59C588.26 676.34 587.16 684.72 587.16 684.72C587.16 684.72 589.2 680.14 596.57 674.25C603.94 668.35 613.26 662.38 613.26 662.38L620.19 629.93C620.19 629.93 594.57 575.85 585.59 555.64C576.61 535.43 563.87 497.59 560.64 463.58C557.41 429.57 555.12 414.34 559.07 383.24C563.03 352.15 581.15 316.44 581.15 316.44C581.15 316.44 557.53 317.56 543.44 306.73C529.35 295.9 524.76 288.11 520.06 278.1C515.35 268.09 511.8 262.17 511.8 262.17C511.8 262.17 477.72 311.52 466.27 363.09C454.82 414.66 452.48 484.72 462.47 547.32C472.46 609.92 489.46 660.72 499.74 690.58C510.02 720.44 518.31 743.52 526.32 781.08C534.33 818.64 538.94 859.62 538.94 859.62L538.95 859.6Z" fill="#DEEB34"/>
|
||||
<path d="M518.45 130.95C518.9 131 535.86 130.11 550.89 134.13C565.92 138.15 576.19 143.8 576.19 143.8C576.19 143.8 565.38 142.65 557.74 145.82C550.1 148.99 541.3 150.37 541.3 150.37C541.3 150.37 542.57 142.31 536.47 139.04C530.37 135.77 518.47 130.95 518.47 130.95H518.45Z" fill="#30923A"/>
|
||||
<path d="M415.98 235.65C415.98 235.65 424.57 222.64 437.19 210.97C449.81 199.3 468.93 191.23 477.15 188.54C485.38 185.85 494.2 183.16 494.2 183.16L502.87 186.69C502.87 186.69 487.17 203.76 463.71 241.87C440.24 279.98 418.11 312.24 400.45 360.08C382.8 407.93 371.04 454.26 366.35 479.5C361.66 504.74 358.28 535.61 358.28 535.61L345.68 513.16L325.33 474.67C325.33 474.67 329.83 465.52 341.05 455.08C352.27 444.64 363.93 434.85 369.19 419.17C374.45 403.49 382.34 384.77 389.21 366.82C396.08 348.87 399.3 340.14 396.65 336.16C393.99 332.18 385.08 334.83 375.79 341.47C366.5 348.11 356.69 360.08 356.69 360.08C356.69 360.08 366.69 336.54 376.93 324.97C387.17 313.4 395.33 307.33 404.81 302.02C414.29 296.71 424.94 287.42 431.95 276.23C438.95 265.04 454.46 238.25 459.44 232.04C464.42 225.82 466.17 218.87 461.94 217.02C457.7 215.17 443.32 216.63 435.55 221.68C427.77 226.73 416 235.66 416 235.66L415.98 235.65Z" fill="#30923A"/>
|
||||
<path d="M263.62 775.68C263.87 775.22 272.1 761.44 283.58 754.65C295.05 747.85 304.54 747.73 307.24 755.66C309.94 763.59 306.78 781.89 298.52 805.24C290.26 828.59 276.19 854.73 258.08 879.18C239.97 903.63 228.88 917.89 221.41 924C213.94 930.11 185.47 940.06 165.88 951.69C146.29 963.32 135.27 971.28 129.14 981.39C123.02 991.49 117.81 1008.4 117.81 1008.4C117.81 1008.4 129.1 995.53 140.37 988.18C151.64 980.83 162.03 985.38 166.4 988.13C170.77 990.88 167.88 1002.8 166.3 1011.96C164.72 1021.11 162.22 1026.22 162.22 1026.22C162.22 1026.22 193.09 978.06 210.48 959.3C227.88 940.54 261.39 903.88 284.96 869.46C308.53 835.05 326.6 790.04 335.17 757.59C343.74 725.14 343.04 678.42 348.29 642.23C353.54 606.04 355.12 575.9 355.12 575.9L337.45 569.66C337.45 569.66 336.09 579.41 333.33 613.7C330.57 647.99 327.22 682.92 308.53 707.99C289.84 733.07 285.85 729.72 275.16 747.18C264.47 764.64 263.61 775.67 263.61 775.67L263.62 775.68Z" fill="#30923A"/>
|
||||
<path d="M563.23 227.79C563.23 227.79 570.52 223.95 576.28 223.8C582.05 223.65 592.19 224.66 601.99 230.3C611.8 235.94 621.81 233.84 623.64 240.66C625.46 247.48 629.15 266.45 629.15 266.45C629.15 266.45 625.63 260.5 621.75 260.29C617.87 260.08 599.05 263.31 583.57 251.98C568.09 240.65 563.23 227.79 563.23 227.79Z" fill="white"/>
|
||||
<path d="M665.63 246.95C665.63 246.95 667.96 234.5 668.4 233.47C668.84 232.44 676.88 227.93 677.2 227.72C677.52 227.51 678.16 230.62 677.2 235.38C676.24 240.14 670.32 251.39 670.32 251.39L665.63 246.96V246.95Z" fill="#D4D4D4"/>
|
||||
<path d="M493.29 377.45C489.48 400.94 485.16 467.06 495.97 525.31C506.77 583.56 524.59 622.86 524.59 622.86C524.59 622.86 512.76 571.59 507.21 542.39C501.65 513.19 498.08 483.09 495.97 452.58C493.87 422.06 493.29 377.45 493.29 377.45V377.45Z" fill="white"/>
|
||||
<path d="M716.24 334.84C716.24 334.84 719.05 338.57 726.74 339.8C734.43 341.03 740.89 334.23 743.34 328.44C745.79 322.65 751.35 309.34 747.4 298.62C743.45 287.9 753.9 304.15 751.74 314.6C749.58 325.05 741.88 342.76 741.4 343.96C740.92 345.16 736.96 349.78 732.58 348.88C728.2 347.98 721.84 345.94 719.44 343C717.04 340.06 716.24 334.85 716.24 334.85V334.84Z" fill="#30923A"/>
|
||||
<path d="M718.15 356.64C719.12 353.88 720.32 352.38 722.26 353.13C724.2 353.88 722.86 358.66 721.21 362.1C719.57 365.54 718.37 368.06 714.86 370.01C711.35 371.96 702.26 373.18 702.26 373.18C702.26 373.18 694.77 375.83 685.8 375.09L680.72 379.8C680.72 379.8 699.3 381.79 710.61 377.04C721.92 372.29 724.5 364.63 727.42 360.75C730.34 356.87 731.86 351.38 731.91 351.18C731.96 350.98 733.7 348.95 730.27 347.68C726.83 346.41 723.18 343.07 720.9 345.75C718.62 348.43 718.17 356.65 718.17 356.65L718.15 356.64Z" fill="#30923A"/>
|
||||
<path d="M701.96 370.19C699.28 369.86 697.01 368.78 697.06 367.31C697.11 365.84 698.43 364 703.21 364.79C707.99 365.58 701 360.81 698.01 360.64C695.02 360.47 691.34 361.48 691.23 361.69C691.12 361.9 688.5 368.72 688.5 368.72C688.5 368.72 690.02 367.31 693.33 368.72C696.64 370.14 700.95 371.14 700.95 371.14L701.96 370.18V370.19Z" fill="#30923A"/>
|
||||
<path d="M683.61 380.22C685.08 379.45 690.97 374.83 693.33 368.73C695.69 362.63 689.7 363.74 689.7 363.74C689.7 363.74 685.5 373.99 683.61 376.03C681.72 378.08 680.19 379.71 680.19 379.71L683.61 380.21V380.22Z" fill="#30923A"/>
|
||||
<path d="M716.24 334.84C716.24 334.84 711.04 345.78 696.66 350.97C682.28 356.15 670.04 349.6 664.87 342.91C659.7 336.22 662.27 324.59 662.27 324.59L653.44 327.75C653.44 327.75 651.36 339.85 657.42 346.92C663.47 354 680.07 359.88 679.87 360.99C679.67 362.1 685.67 363.77 689.22 362.48C692.77 361.19 703.28 358.24 707.98 352.97C712.68 347.7 714.09 343.36 715.11 340.74C716.13 338.12 716.24 334.84 716.24 334.84V334.84Z" fill="#30923A"/>
|
||||
<path d="M674.03 312.34C672.33 313.33 670.94 314.83 671.04 317.67C671.14 320.51 673.88 322.49 677.49 322.04C681.1 321.59 684.25 321.71 686.57 322.04C688.89 322.37 691.48 322.64 691.48 322.64C691.48 322.64 686.37 319.11 681.26 316.59C676.15 314.06 674.03 312.34 674.03 312.34Z" fill="#30923A"/>
|
||||
<path d="M731.18 319.57C731.18 319.57 731.51 313.99 732.56 310.43C733.61 306.87 735.72 303.72 739.32 302.87C742.93 302.02 745.39 304.77 745.34 307.33C745.28 309.89 741.58 308.09 738.82 310.44C736.07 312.79 731.18 319.58 731.18 319.58V319.57Z" fill="#30923A"/>
|
||||
<path d="M668.37 327.92C668.37 327.92 660.76 326.67 657.17 322.64C653.58 318.61 654.13 309.28 659.4 303.03C664.67 296.77 659.4 303.89 659.31 310.43C659.22 316.97 659.45 317.14 661.27 320.59C663.09 324.04 668.37 327.92 668.37 327.92Z" fill="#30923A"/>
|
||||
<path d="M598.58 287.33C606.21 287.09 612.73 286.49 619.68 287.77C626.63 289.06 655.53 313.65 655.53 313.65C655.53 313.65 657.19 320.7 659.73 322.64C662.27 324.58 661.27 326.11 661.27 326.11L657.5 330.74L651.58 327.92C651.58 327.92 621.22 296.26 614.36 292.49C607.5 288.72 599.26 288.2 599.26 288.2L598.58 287.33V287.33Z" fill="#30923A"/>
|
||||
<path d="M631.04 271.34C631.04 271.34 601.67 269.76 582.99 253.84C564.31 237.92 578.12 248.31 578.12 248.31C578.12 248.31 590.32 260.12 603.62 260.3C616.92 260.48 623.77 259.82 626.09 261.26C628.41 262.7 631.04 271.34 631.04 271.34V271.34Z" fill="#30923A"/>
|
||||
<path d="M640.16 251.83C638.76 252.41 635.59 253.43 632.89 258.15C630.19 262.88 631.04 271.33 631.04 271.33C631.04 271.33 630.05 262.93 627.74 254.65C625.43 246.36 621.29 239.19 621.29 239.19C621.29 239.19 625.44 238.54 630.03 235.38C634.61 232.22 637 226 637 226C637 226 637.73 230.81 635.85 235.89C633.96 240.98 634.55 244.71 635.4 247.16C636.54 250.47 640.17 251.83 640.17 251.83H640.16Z" fill="#30923A"/>
|
||||
<path d="M543.45 219.94C543.45 219.94 549.96 212.62 566.15 211.5C582.34 210.37 597.23 217.45 609.98 222.12C622.72 226.78 627.34 227.65 630.73 227.46C634.12 227.27 636.3 223.94 636.2 222C636.2 222 637.81 235.38 625.15 235.89C612.49 236.4 598.84 228.29 586.47 222.76C574.1 217.23 568.61 214.54 559.99 215.44C551.37 216.34 543.46 219.95 543.46 219.95L543.45 219.94Z" fill="#30923A"/>
|
||||
<path d="M543.45 219.94C543.45 219.94 556.42 228.11 567.26 239.55C578.1 250.99 562.18 231.16 562.49 229.43C562.8 227.7 566.96 227.78 572.75 225.12C578.54 222.46 579.33 218.81 570.55 217.06C561.77 215.3 553.27 216.01 550.02 217.06C546.77 218.11 543.44 219.95 543.44 219.95L543.45 219.94Z" fill="#30923A"/>
|
||||
<path d="M634.66 157.89C634.66 157.89 645.57 166.32 659.68 187.01C673.79 207.7 675.39 224.62 675.56 226.6C675.73 228.58 668.4 231.13 668.4 231.13C668.4 231.13 664.24 206.5 655.11 190.17C645.99 173.84 634.66 157.89 634.66 157.89V157.89Z" fill="#30923A"/>
|
||||
<path d="M669.06 192.25C673.17 198.25 676.32 209.98 674.27 219.02C672.22 228.07 674.73 228.3 677.71 226.2C680.69 224.1 683.35 225.48 681.73 215.95C680.11 206.42 678.91 204.06 675.57 198.71C672.23 193.35 669.06 192.25 669.06 192.25V192.25Z" fill="#30923A"/>
|
||||
<path d="M670.55 251.91C670.55 251.91 673.85 248.32 675.57 241.62C677.29 234.91 678.43 229.43 678.43 229.43C678.43 229.43 680.82 237.57 680.57 244.97C680.32 252.37 679.26 260.62 679.26 260.62L670.55 251.91Z" fill="#30923A"/>
|
||||
<path d="M545.62 155.85C549.04 153.37 564.17 145.31 581.54 146.76C598.9 148.21 580.18 144.29 569.77 144.75C559.36 145.21 546.26 147.62 544.28 148.52C542.31 149.42 541.59 151 541.59 151L545.62 155.84V155.85Z" fill="#30923A"/>
|
||||
<path d="M566.15 155.12C566.15 155.12 572.56 154.2 583.46 155.12C594.36 156.04 607.24 160.83 607.24 160.83C607.24 160.83 607.32 168.57 600.16 165.54C592.99 162.52 574.7 156.65 574.39 156.57C574.08 156.49 566.15 155.11 566.15 155.11V155.12Z" fill="#30923A"/>
|
||||
<path d="M649.05 248.98C653.78 247.91 659.05 245.71 661.25 240.66C663.45 235.61 660.37 217.02 654.43 203.04C648.49 189.07 639.23 174.76 628.24 161.45C617.24 148.14 596.16 127.86 596.16 127.86C596.16 127.86 618.5 141.02 629.93 154.36C641.36 167.71 654.12 188 661.05 203.44C667.97 218.88 669.57 231.29 668.42 237.58C667.27 243.87 664.81 247.43 659.95 248.3C655.08 249.16 649.07 248.97 649.07 248.97L649.05 248.98Z" fill="#30923A"/>
|
||||
<path d="M592.3 141.7L600.86 142.89L603.91 150.29L596.15 148.57L592.3 141.7Z" fill="#30923A"/>
|
||||
<path d="M623.24 145.82C627.21 146.52 633.77 148.47 636.25 145.82C638.73 143.17 639.75 134.39 638.73 130.9C637.71 127.42 622.4 118.23 608.55 104.23C594.7 90.23 568.33 63.95 548.97 18.15C548.97 18.15 568.99 51.65 587.95 74.49C606.91 97.33 633.39 116.23 641.03 121.7C648.67 127.17 648.6 133.87 647.15 139.37C645.7 144.86 644.69 162.99 644.69 162.99C644.69 162.99 637.59 158.14 632.19 155.12C626.79 152.1 623.41 148.98 623.41 148.98L623.22 145.81L623.24 145.82Z" fill="#30923A"/>
|
||||
<path d="M396.57 90.5701C405.92 105.09 420.54 123.34 448.75 144.72C476.96 166.1 522.9 190.17 522.9 190.17C522.9 190.17 472.44 175.53 450.43 155.12C428.42 134.71 412.4 121.48 407.04 111.32C401.68 101.16 396.58 90.5701 396.58 90.5701H396.57Z" fill="#30923A"/>
|
||||
<path d="M522.9 190.17C522.9 190.17 512.05 188.74 503.58 198.15C495.11 207.56 453.48 260.57 431.96 304.98C410.43 349.39 395.98 384.91 380.19 448.84C364.4 512.78 365.09 542.39 365.09 542.39L358.78 533.56C358.78 533.56 364.01 483.93 372.01 453.5C380.01 423.07 396.47 366.72 406.69 344.26C416.91 321.8 435.01 281.79 451.09 259.06C467.17 236.33 502.14 185.21 502.14 185.21C502.14 185.21 510.4 186.81 511.05 186.68C511.7 186.55 522.9 190.16 522.9 190.16V190.17Z" fill="#30923A"/>
|
||||
<path d="M275.59 527.71C277.42 539.98 281.94 549.15 284.34 553.38C286.74 557.61 288.96 561.56 288.96 561.56L299.83 541.4L275.4 520.44L275.6 527.71H275.59Z" fill="#30923A"/>
|
||||
<path d="M264.94 610.23C264.94 610.23 271.64 588.55 277.84 573.09C284.05 557.62 291.24 547.04 291.24 547.04L295.19 551.27L264.94 610.23V610.23Z" fill="#30923A"/>
|
||||
<path d="M316.48 391.19C316.48 391.19 338.04 347.92 355.31 320.59C372.58 293.26 386.76 287.1 396.01 285.66C405.26 284.22 412.57 291.91 414.11 294.5C414.11 294.5 421.72 291.1 427.97 280.93C434.21 270.76 443.69 271.06 427.97 263.46C412.25 255.86 385.51 252.87 374.47 268.01C363.44 283.14 346.44 311.03 339.66 330.89C332.88 350.75 316.49 391.2 316.49 391.2L316.48 391.19Z" fill="#30923A"/>
|
||||
<path d="M377.53 260.75C383.15 254.76 393.48 246.34 406.1 242.77C418.73 239.2 427.18 245.11 431.95 249.1C436.71 253.1 441.34 260.74 441.34 260.74L435.65 266.2C435.65 266.2 423.45 251.08 411.08 250.17C398.71 249.26 377.53 260.74 377.53 260.74V260.75Z" fill="#30923A"/>
|
||||
<path d="M410.49 222.54C414.49 222.67 423.7 224.61 430.71 233.05C437.72 241.48 443.41 255.13 443.41 255.13L448.83 248.35C448.83 248.35 440.83 232.06 429.66 226.39C418.49 220.72 411.09 220.98 411.09 220.98L410.49 222.54Z" fill="#30923A"/>
|
||||
<path d="M332.23 461.78C334.44 449.97 346.52 404.35 351.53 387.39C356.54 370.43 362.14 368.96 363.9 369.25C365.66 369.54 369.04 380.32 369.04 397.19C369.04 414.06 369.21 425.23 369.21 425.23C369.21 425.23 352.12 452.59 344.46 459.83C336.8 467.06 332.23 461.79 332.23 461.79V461.78Z" fill="#30923A"/>
|
||||
<path d="M307.03 426.42L314.44 401.78C314.44 401.78 314.22 444.47 314.44 450.2C314.66 455.93 307.03 426.43 307.03 426.43V426.42Z" fill="#30923A"/>
|
||||
<path d="M65.6899 481.9C65.6899 481.9 87.3999 448.99 110.51 413.17C133.63 377.35 160.55 343.32 178.59 325.79C196.63 308.26 211.1 298.61 225.84 296.58C240.57 294.55 248.96 305.6 253.53 317.6C258.1 329.6 263.18 352.79 273.6 385.52C284.01 418.25 291.28 435.78 300.73 457.88C310.18 479.98 338.88 523.04 338.88 523.04C338.88 523.04 294.14 440.04 283 391.39C271.86 342.74 264.47 306.4 258.11 292.72C251.75 279.04 243.87 272.14 226.1 274.73C208.33 277.32 184.54 302.72 160.31 329.97C136.07 357.21 113.55 394.66 102.65 411.14C91.75 427.62 65.7 481.9 65.7 481.9H65.6899Z" fill="#30923A"/>
|
||||
<path d="M111.84 514.59C111.84 514.59 127.02 485.18 133.32 472.91C139.62 460.64 145.09 449.69 147.08 449.69C149.07 449.69 149.4 454.17 147.08 462.79C144.76 471.41 139.11 500.39 139.11 500.39C139.11 500.39 154.33 438.99 161.87 424.49C169.42 409.99 170.47 402.98 167.81 404.26C165.14 405.54 164.33 406.56 156.7 417.53C149.07 428.5 131.46 467.56 127.26 476.93C123.06 486.31 111.83 514.59 111.83 514.59H111.84Z" fill="#30923A"/>
|
||||
<path d="M194.05 474.61C194.05 474.61 203.62 437.83 207.45 424.34C211.27 410.85 214.49 411.57 216.07 413.11C217.65 414.65 218.01 422.74 219.08 431.89C220.15 441.04 224.69 474.6 224.69 474.6C224.69 474.6 221.37 409.63 222.55 394.66C223.73 379.69 223.16 369.01 220.84 371.55C218.53 374.09 214.8 382.84 211.91 392.16C209.02 401.48 202.59 429.58 200.62 439.3C198.65 449.02 194.05 474.6 194.05 474.6V474.61Z" fill="#30923A"/>
|
||||
<path d="M6.72998 670.28C6.72998 670.28 13.42 640.41 28.44 600.71C43.47 561.01 59.73 541.39 73.12 530.82C86.51 520.25 104.13 523.74 108.83 533.86C113.53 543.98 111.13 572.47 114.27 611.64C117.42 650.81 108.36 580.4 107.42 565.83C106.48 551.26 102.72 539.99 92.38 537.4C82.04 534.82 71.47 545.86 59.02 564.42C46.57 582.98 34.5799 602.01 28.4799 617.99C22.3699 633.97 6.72998 670.28 6.72998 670.28V670.28Z" fill="#30923A"/>
|
||||
<path d="M123.01 669.16C123.01 669.16 121.53 651.83 127.4 611.65C133.27 571.47 141.07 551.62 150.9 535.61C160.2 520.45 167.21 513.87 176.76 512.32C188.29 510.45 194.7 521 197.01 538.23C199.33 555.46 193.7 530.74 188.16 524.44C182.62 518.14 169.93 519.78 164.76 528.48C159.59 537.17 144.32 564.19 138.92 590.04C133.52 615.88 127.75 648.21 126.53 658.69C125.31 669.18 123.02 669.16 123.02 669.16H123.01Z" fill="#30923A"/>
|
||||
<path d="M382.09 584.38C361.19 576.12 336.13 562.47 318.98 545.63C301.83 528.8 295.23 521.27 281.57 512.1C267.91 502.93 253.79 502.29 240.69 510.43C227.59 518.57 221.17 530.36 214.76 555.76C208.34 581.16 204.22 611.72 204.22 611.72C204.22 611.72 217.34 558.79 228.38 539.32C239.42 519.85 247.51 513.75 260.38 514.23C273.25 514.72 285.92 527.55 302.11 542.76C318.3 557.97 334.04 567.26 349.55 574.13C365.05 580.99 382.1 584.38 382.1 584.38H382.09Z" fill="#30923A"/>
|
||||
<path d="M528.13 802.64C529.43 815.43 530.94 842.38 530.19 853.35C529.44 864.32 528.13 873.18 528.13 873.18L540.95 860.26C540.95 860.26 540.55 849.22 536.94 833.52C533.33 817.82 528.13 802.64 528.13 802.64V802.64Z" fill="#30923A"/>
|
||||
<path d="M582.76 875.63C582.76 875.63 581.18 883.85 581.04 885.11C580.9 886.37 582.16 886.92 585.5 887.48C588.85 888.04 622.86 892.5 622.86 892.5L625.29 878.99L582.76 875.63Z" fill="#30923A"/>
|
||||
<path d="M609.71 884.06C609.71 884.06 606.22 908.52 593.21 937.22C580.2 965.92 554.88 993.52 554.88 993.52C554.88 993.52 576.38 981.66 590.89 962.74C605.4 943.82 615.29 924.04 618.81 911.33C622.33 898.62 623.55 888.65 623.55 888.65L609.71 884.05V884.06Z" fill="#30923A"/>
|
||||
<path d="M567.85 957.74C567.85 957.74 544.24 986.93 497.29 1002.11C450.35 1017.3 411.12 1024.18 387.08 1023.43C363.04 1022.68 430.68 1033.59 450.08 1031.76C469.48 1029.93 524.58 1036.63 524.58 1036.63L538.18 997.19C538.18 997.19 549.93 994.08 554.87 986.24C559.81 978.4 561.64 972.91 563.67 969.18C565.7 965.45 567.84 957.75 567.84 957.75L567.85 957.74Z" fill="#30923A"/>
|
||||
<path d="M764.01 925.88C755.35 934.14 741.43 940.15 729.8 941.83C718.17 943.52 708.8 941.83 708.8 941.83L701.56 892.49C701.56 892.49 704.91 909.59 719.51 915.91C734.11 922.23 738.25 920.93 738.25 920.93C738.25 920.93 742.39 925.5 750.45 925.71C758.51 925.93 764.01 925.88 764.01 925.88V925.88Z" fill="#30923A"/>
|
||||
<path d="M735.49 918.99C727.89 913.86 723.72 902.71 723.72 890.7C723.72 878.69 725.11 878.99 722.83 877.31C720.55 875.62 713.37 873.47 713.37 873.47C713.37 873.47 718.89 873.06 724.43 874.84C729.97 876.62 731.66 880.19 731.66 880.19C731.66 880.19 729.08 892.39 731.16 902.11C733.24 911.83 735.49 918.99 735.49 918.99V918.99Z" fill="#30923A"/>
|
||||
<path d="M707.37 895.76C708.59 899.36 711.22 904.74 719.21 910.01C727.2 915.27 735.49 914.88 735.49 914.88L738.26 920.93C738.26 920.93 726.33 921.11 717.95 914.19C709.57 907.27 707.38 895.75 707.38 895.75L707.37 895.76Z" fill="#30923A"/>
|
||||
<path d="M736.3 913.54C738.16 916.69 743.16 919.67 749.22 920.93C755.28 922.19 761.74 918.58 763.09 916.43C764.44 914.29 766.76 921.59 766.76 921.59C766.76 921.59 760.19 928.12 749.22 924.52C738.25 920.92 735.49 914.87 735.49 914.87L736.31 913.54H736.3Z" fill="#30923A"/>
|
||||
<path d="M754.86 878.56C754.86 878.56 751.34 884.85 752.94 898.8C754.79 914.98 761.26 922.5 762.64 924.2C764.02 925.9 765.71 920.54 765.71 920.54C765.71 920.54 758.3 902.41 760.93 895.1C763.56 887.79 764.46 884.62 763.09 884.04C761.72 883.46 754.86 878.57 754.86 878.57V878.56Z" fill="#30923A"/>
|
||||
<path d="M786.41 879.99C791.77 884.16 793.64 892.93 795.12 902.19C796.6 911.45 794.53 922.6 789.9 924.52C785.27 926.44 772.35 926.44 766.76 916.12C761.17 905.8 766.56 923.12 775.81 927.67C785.06 932.22 795.38 929.71 801.76 924.77C808.14 919.83 813.06 916.87 812.08 909.78C811.09 902.68 808.43 899.63 805.77 899.23C803.11 898.84 800.15 898.44 800.15 898.44C800.15 898.44 799.95 893.4 795.12 886.9C790.29 880.4 786.41 879.98 786.41 879.98V879.99Z" fill="#30923A"/>
|
||||
<path d="M522.9 233.47C522.9 233.47 523.44 241.77 525.72 249.33C528 256.89 533.86 264.69 533.86 264.69H547.69C547.69 264.69 538.95 257.68 533.28 250.04C527.61 242.4 522.9 233.48 522.9 233.48V233.47Z" fill="#30923A"/>
|
||||
<path d="M527.4 261.91C527.4 261.91 531.32 273.73 545.2 284.29C559.08 294.85 576.23 298.17 586.46 299.04C596.69 299.91 607.24 297.45 616.07 304.54C624.89 311.63 633.99 320.67 640.16 327.03C646.33 333.39 654.89 345.36 654.89 345.36L651.59 327.93C651.59 327.93 625.2 302.49 616.63 295.11C608.06 287.73 594.61 290.68 587.22 289.94C579.83 289.2 560.62 287.28 551.75 281.66C542.88 276.04 533.42 268.06 533.42 268.06L527.41 261.92L527.4 261.91Z" fill="#30923A"/>
|
||||
<path d="M644.36 344.13C648.99 350.77 655.83 359.79 662.72 365.2C669.61 370.61 679.08 373.57 679.08 373.57L678.4 379.47C678.4 379.47 668.7 376.46 661.82 370.39C654.94 364.31 648.64 352.23 648.64 352.23L644.36 344.13V344.13Z" fill="#30923A"/>
|
||||
<path d="M650.79 339.82C651.95 342.54 652.49 347.18 659.18 355.23C665.87 363.28 678.41 366.67 678.41 366.67L679.88 360.98C679.88 360.98 665.32 358.69 660 350.89C654.68 343.09 650.79 339.82 650.79 339.82V339.82Z" fill="#30923A"/>
|
||||
<path d="M627.36 321.65C622.51 316.16 614.77 308.16 614.22 306.71C613.67 305.26 615.62 303.77 621.63 309.22C627.63 314.67 621.49 304.5 610.48 299.32C599.47 294.14 580.16 298.28 580.16 298.28C580.16 298.28 586.69 298.99 587.13 301.85C587.57 304.71 572.92 305.83 560.3 304.79C547.68 303.74 538.79 295.9 530.03 284.52C521.27 273.14 504.5 248.3 504.5 248.3C504.5 248.3 520.25 278.12 527.41 290.96C534.57 303.8 548 313.72 563.24 312.81C578.48 311.9 595.02 307.89 602.01 308.17C609 308.45 610.96 309.44 616.45 313.65C621.94 317.86 627.36 321.65 627.36 321.65V321.65Z" fill="#30923A"/>
|
||||
<path d="M551.95 187.15C551.95 187.15 550.33 165.92 549.74 160.48C549.15 155.04 552.86 151.49 557.97 149.97C563.08 148.45 554.17 149.42 548.97 151.52C543.77 153.62 541.73 152.25 543.54 156.79C545.35 161.33 551.95 187.15 551.95 187.15V187.15Z" fill="#30923A"/>
|
||||
<path d="M504.49 295.48C507.08 291.49 512.44 290.76 516.95 296.49C521.46 302.22 530.95 311.48 542.35 317.57C553.75 323.66 570.58 324.8 575.74 324.19C575.74 324.19 581.29 316.56 578.17 316.45C575.05 316.34 547.62 313.83 537.17 303.14C526.72 292.45 519.29 280.08 517.11 273.57C514.93 267.05 510.75 263.75 510.75 263.75C501.82 273.19 493.3 289.62 493.3 289.62L504.5 295.49L504.49 295.48Z" fill="#AAAE11"/>
|
||||
<path d="M504.49 295.48C499.84 302.19 484.54 326.83 474.6 369.22C464.66 411.61 462.21 451.03 465.69 503.92C469.17 556.81 479.28 593.49 490.85 632.6C502.42 671.71 513.75 699.54 524.59 737.04C535.43 774.54 544.3 816.94 546.7 846.92L539.2 857.09C539.2 857.09 532.58 793.72 518.67 749.44C504.76 705.16 486.18 653.43 474.83 608.21C463.49 562.99 456.65 521.97 456.16 496.48C455.67 470.99 452.7 425.11 458.53 398.13C464.36 371.15 477.85 324.75 484.79 310.43C491.73 296.11 500.83 283.27 500.83 283.27L504.48 295.48H504.49Z" fill="#AAAE11"/>
|
||||
<path d="M565.74 323.95C568.64 324.19 568.77 327.85 567.63 330.74C566.48 333.62 554.51 358.42 551.39 410.82C548.28 463.22 559.08 508.09 568.8 532.38C578.51 556.66 590.99 583.66 601.24 605.61C611.49 627.56 616.71 641.47 616.71 641.47L620.21 629.91C620.21 629.91 591.4 570.9 582.16 548.6C572.92 526.3 563.27 493.75 560.06 465.82C556.85 437.89 556.52 404.54 559.09 384.05C561.66 363.56 570.69 337.28 573.21 330.73C575.74 324.18 581.56 315.72 581.56 315.72L565.75 323.94L565.74 323.95Z" fill="#AAAE11"/>
|
||||
<path d="M482.56 341.48C481.36 345.06 483.72 344.39 489.7 342.3C495.68 340.21 507.93 336.42 521.38 335.82C534.83 335.22 544.59 336.42 551.27 339.11C557.95 341.8 560.37 344.69 561.11 344.93C561.85 345.17 562.37 344.85 562.83 343.46C563.29 342.06 565.01 342.57 563.75 346.23C562.49 349.89 561.23 352.26 561.23 352.26C561.23 352.26 557.15 343.47 539.11 341.64C521.07 339.8 510.95 341.59 497.3 346.29C483.65 350.99 473.18 357.83 471.23 355.77C469.27 353.72 473.63 342.77 473.63 342.77L482.56 341.5V341.48Z" fill="#AAAE11"/>
|
||||
<path d="M465.44 427.89C465.27 430.17 467.25 431.89 471.85 429.59C476.45 427.29 484.04 423.14 501.3 419.18C518.55 415.22 527.07 415.33 535.12 416.25C543.17 417.17 546.87 418.77 548.25 419.18C549.63 419.59 550.73 419.16 551.08 417.12C551.43 415.08 552.57 415.84 552.85 420.25C553.13 424.66 553.13 426.04 553.13 426.04C553.13 426.04 537.49 418.31 522.89 420.2C508.29 422.1 492.4 425.71 479.95 431.5C467.5 437.29 460.16 444.55 458.86 441.96C457.56 439.37 465.44 427.89 465.44 427.89Z" fill="#AAAE11"/>
|
||||
<path d="M467.56 525.59C467.87 528.42 469.77 530.74 473.07 529.24C476.37 527.74 490.04 520.72 509.33 517.63C528.62 514.54 543.98 513.88 552.69 515.19C561.4 516.5 562.93 515.49 562.09 512.49C561.26 509.49 566.46 514.12 567.58 518.26C568.7 522.4 568.42 523.04 568.42 523.04C568.42 523.04 547.06 515.82 520.47 521.09C493.88 526.36 482.46 531.62 475.8 535.04C469.13 538.47 465.03 542.91 464.65 540.57C464.28 538.23 467.56 525.58 467.56 525.58V525.59Z" fill="#AAAE11"/>
|
||||
<path d="M494.03 643.12C495.13 646.63 498.12 648.35 503.78 645.03C509.44 641.72 520.66 634.67 548.43 627.94C576.2 621.21 594.68 620.57 600.34 619.93C606 619.29 607.23 618.75 606.16 616.38C605.09 614.01 608.88 617.58 610.59 620.46C612.3 623.34 610.59 624.09 610.59 624.09C610.59 624.09 576.48 623.59 548.85 632.6C521.22 641.61 503.3 653.15 499.74 655.6C496.19 658.06 492.02 662.08 491.92 659.21C491.81 656.34 494.03 643.11 494.03 643.11V643.12Z" fill="#AAAE11"/>
|
||||
<path d="M535.37 778.84C536.11 782.15 537.78 785.68 543.01 784.54C548.24 783.4 565.29 778.84 565.29 778.84V784.76L532.14 793.08L535.37 778.84Z" fill="#AAAE11"/>
|
||||
<path d="M607.45 942.59C608.4 943.02 628.41 937.5 655.09 933.68C681.77 929.87 706.13 930.06 706.13 930.06L707.6 936.34C707.6 936.34 682.95 935.91 646.92 942.62C610.89 949.32 597.1 954.2 597.1 954.2L607.44 942.6L607.45 942.59Z" fill="#AAAE11"/>
|
||||
<path d="M645.63 883.13C645.63 883.13 641.5 918.53 616.56 959C591.62 999.46 548.32 1027.1 548.32 1027.1L526.23 1031.87L536.75 1000.55L554.88 986.23L550.49 999.24C550.49 999.24 581.75 976.49 595.99 956.69C610.23 936.89 620.49 911.84 623.54 898.91C626.59 885.98 625.28 878.98 625.28 878.98L645.62 883.13H645.63Z" fill="#AAAE11"/>
|
||||
<path d="M632.68 893.94C633.29 893.94 702.89 901.54 702.89 901.54L699.73 886.57L633.51 880.67L632.68 893.95V893.94Z" fill="#AAAE11"/>
|
||||
<path d="M691.94 896.22C691.94 896.22 699.61 922.27 700.25 955.43C700.89 988.59 694.17 1036.54 694.17 1036.54L701.57 1029.14C701.57 1029.14 708.52 991.24 707.37 957.73C706.22 924.22 701.57 892.49 701.57 892.49L691.95 896.21L691.94 896.22Z" fill="#AAAE11"/>
|
||||
<path d="M591.78 990.55C585.5 996.88 587.84 1006.88 609.74 1005.51C631.64 1004.14 675.17 996.31 685.84 993.52C696.52 990.73 699.58 986.59 699.92 975.38C700.26 964.17 702.78 976.49 701.57 999.22C700.35 1021.94 694.17 1036.54 694.17 1036.54L524.59 1036.62L543.76 1013.99L591.78 990.54V990.55Z" fill="#AAAE11"/>
|
||||
<path d="M597.34 678.8C600.23 675.95 601.21 683.05 598.56 707.26C595.91 731.47 588.26 795.38 588.26 795.38L613.27 662.37C613.27 662.37 599.21 670.44 598.05 673.08C596.89 675.72 597.34 678.81 597.34 678.81V678.8Z" fill="#30923A"/>
|
||||
<path d="M564.41 654.58C563.91 657.61 564.03 661.02 566.69 661.66C569.35 662.3 574.54 664.95 577.08 673.69C579.61 682.43 579.36 698.64 578.85 701.56C578.34 704.48 587.62 690.27 587.36 682.32C587.1 674.37 589.44 668.63 582.78 662.37C576.12 656.11 569.56 655.64 568.09 654.58C566.62 653.52 565.3 654.54 566.66 651.34C568.02 648.13 568.94 646.25 568.94 646.25L564.41 654.58Z" fill="#30923A"/>
|
||||
<path d="M363.28 579.33C363.28 579.33 359.9 631.33 355.28 692.16C350.66 752.99 328.13 824.45 295.76 873.44C263.39 922.43 237.94 946.48 209.97 984.01C182 1021.54 177.04 1040.66 177.04 1040.66L157.75 1036.63C157.75 1036.63 190.49 984.8 219.53 951.48C248.56 918.16 287.96 872.13 303.67 843.1C319.38 814.07 336.37 759.54 342.39 718.83C348.41 678.11 349.98 634.21 350.72 611.73C351.46 589.26 353.69 575.88 353.69 575.88L363.28 579.34V579.33Z" fill="#30923A"/>
|
||||
<path d="M668.37 327.92C668.37 327.92 663.5 322.63 664.18 316.23C664.86 309.84 668.35 305.82 675.45 303.63C682.54 301.44 690.49 301.59 700.2 308.11C700.2 308.11 696.87 300.6 683.24 298.05C669.61 295.5 661.93 301.17 660.05 305.86C658.17 310.54 657.87 311.64 658.87 316.24C659.87 320.84 660.6 322.03 661.73 323.13C662.86 324.23 668.37 327.92 668.37 327.92V327.92Z" fill="#E4FFA4"/>
|
||||
<path d="M743.72 294.21C740.61 290.71 734.24 290.42 729.09 293.42C723.24 296.83 719.52 304.3 718.33 306.99C718.33 306.99 724.11 301.75 728.03 298.38C731.95 295.01 733.8 294.15 736.61 293.49C740.81 292.5 743.71 294.21 743.71 294.21H743.72Z" fill="#E4FFA4"/>
|
||||
<path d="M672.18 335.03C674.81 337.7 680.02 340.02 685.83 339.82C691.64 339.62 697.45 336.1 697.45 336.1C697.45 336.1 692.08 343.76 685.51 343.76C675.03 343.76 672.18 335.03 672.18 335.03Z" fill="#E4FFA4"/>
|
||||
<path d="M522.9 233.47C522.9 233.47 528.73 241.19 540.99 250C553.25 258.81 575.53 265.91 591.15 268.36C591.15 268.36 580.34 271.89 562.86 271.34C545.38 270.79 536.1 267.55 536.1 267.55C536.1 267.55 543.29 266.45 546.1 265.73C548.91 265.01 546.62 261.83 543.46 260.29C540.3 258.75 532.15 249.88 529.25 245.62C526.35 241.36 522.91 233.47 522.91 233.47H522.9Z" fill="#E4FFA4"/>
|
||||
<path d="M650.94 276.3C650.94 276.3 655.95 286.67 658.3 292.23C660.65 297.79 660.78 300.46 659.4 303.03C658.02 305.6 665.29 301.09 665.75 298.34C666.21 295.59 656.82 284.31 656.09 283.67C655.36 283.03 650.94 276.3 650.94 276.3V276.3Z" fill="#E4FFA4"/>
|
||||
<path d="M608.56 203.46C600.04 198.45 585.83 196.11 574.85 197.11C574.85 197.11 577.71 195.18 582.01 193.72C586.31 192.26 587.48 188.69 586.17 185.91C584.86 183.14 579.6 176.57 573.62 172.12C573.62 172.12 582.09 176.39 587.26 180.58C597.95 189.24 608.56 203.45 608.56 203.45V203.46Z" fill="#E4FFA4"/>
|
||||
<path d="M381.62 55.95C381.62 55.95 395.62 73.93 428.75 98.1C461.88 122.27 487.95 133.73 507.21 139.15C526.47 144.57 535 143.23 538.24 151.44C541.48 159.66 542.83 147.48 531.89 140.38C520.96 133.28 469.58 113.73 448.75 102.9C427.92 92.07 398.96 69.56 391.87 63.1C384.79 56.64 381.62 55.96 381.62 55.96V55.95Z" fill="#E4FFA4"/>
|
||||
<path d="M403.21 436.53C405.98 415.71 415.42 376.47 427.56 350.24C439.69 324.01 451.41 311.46 451.41 311.46C451.41 311.46 435.35 341.91 424.28 377.45C413.2 412.99 404.12 457.7 402.54 478.6C402.54 478.6 400.96 453.49 403.22 436.52L403.21 436.53Z" fill="#E4FFA4"/>
|
||||
<path d="M380.03 704.74C380.03 704.74 398.01 718.42 407.49 734.48C416.97 750.54 415.2 753.12 413.7 754.48C412.2 755.84 410.16 753.39 402.81 753.66C395.46 753.93 391.52 757.33 389.47 763.73C389.47 763.73 392.02 756.83 399.53 758.42C407.05 760 411.78 759.11 420.63 769.58C429.47 780.06 436.43 791.9 436.43 791.9C436.43 791.9 432.63 762.5 419.89 743.66C407.16 724.82 396.36 716.68 392.94 714.04C389.52 711.4 380.01 704.74 380.01 704.74H380.03Z" fill="#E4FFA4"/>
|
||||
<path d="M388.99 770.33C390.01 777.81 394.66 788.58 398.96 796.51C403.26 804.44 404.63 808.64 403.61 810.11C402.59 811.58 399.53 810.59 396.69 809.39C393.85 808.19 386.6 807.75 382.57 809.66C382.57 809.66 389.46 806.94 399.08 814.74C408.69 822.53 417.86 834.42 418.02 834.37C418.18 834.32 417.44 816.31 405.44 797.95C393.44 779.6 388.99 770.33 388.99 770.33Z" fill="#E4FFA4"/>
|
||||
<path d="M459.24 906.97C459.24 906.97 494.13 909.53 523.86 889.62C553.59 869.71 563.4 848.01 569.81 822.35C576.7 794.77 570.37 763.62 563.1 739.16C555.82 714.7 568.65 764.75 565.28 795.38C561.92 826.01 550.81 853.47 532.13 869.16C513.45 884.85 502.1 895.41 489.1 898.66C476.09 901.91 459.24 906.97 459.24 906.97Z" fill="#E4FFA4"/>
|
||||
<path d="M567.24 645.6C567.24 645.6 558.62 660.9 557.39 679.03C556.16 697.17 557.25 718.37 561.85 734.82C566.44 751.27 553.82 720.79 551.63 708.85C549.44 696.9 547.26 684.05 551.68 669.92C556.1 655.79 563.49 645.6 563.49 645.6H567.25H567.24Z" fill="#E4FFA4"/>
|
||||
<path d="M773.86 913.54C771.37 910.13 768.96 905.22 768.59 897.5C768.22 889.78 769.93 884.21 772.1 882.04C773.86 880.28 774.41 879.99 774.41 879.99C774.41 879.99 774.41 883.19 774.78 886.91C774.78 886.91 773.67 890.35 772.93 896.22C772.19 902.09 773.85 913.55 773.85 913.55L773.86 913.54Z" fill="#E4FFA4"/>
|
||||
<path d="M742.94 858.55C742.94 858.55 739.29 869.25 739.06 883.15C738.83 897.06 741 907.58 741 907.58C741 907.58 732.3 897.73 732.79 884.03C733.28 870.33 736.52 863.55 739.73 861.05L742.94 858.55Z" fill="#E4FFA4"/>
|
||||
<path d="M34.51 544.94C43 520.25 61.42 470.18 88.97 421.11C116.52 372.04 144.41 338 173.74 303.83C203.07 269.65 229.04 251.99 240.02 245.24C251 238.49 257.88 240.88 261.28 243.74C264.68 246.6 268.77 249.97 270.76 264.92C272.75 279.87 274 252.27 270.09 241.67C266.18 231.07 256.09 228.62 247.18 232.28C238.27 235.94 207.57 258.27 187.39 280.39C167.21 302.5 136.64 340.87 116.69 368.98C96.74 397.08 84.5 418.88 73.62 441.11C62.74 463.34 57.38 484.41 48.93 500.45C40.47 516.49 34.51 544.94 34.51 544.94Z" fill="#E4FFA4"/>
|
||||
<path d="M187.24 370.01C171.98 383.96 140.56 428.52 130.24 454.92C119.92 481.32 111.84 514.59 111.84 514.59C111.84 514.59 124.19 483.37 133.69 463.57C143.19 443.77 154.69 418.83 162.45 407.63C170.21 396.43 180.24 382.38 180.24 382.38L187.24 370.01Z" fill="#E4FFA4"/>
|
||||
<path d="M194.05 474.61C194.05 474.61 196.53 439.2 200.76 415.26C204.99 391.32 218.11 348.76 228.38 332.15C228.38 332.15 224.11 355.48 218.52 370.95C212.94 386.42 201.6 432.94 201.6 432.94L194.04 474.61H194.05Z" fill="#E4FFA4"/>
|
||||
<path d="M325.97 926.16C325.34 933.67 323.98 945.23 322.17 949.77C320.36 954.31 317.29 951.47 314.91 949.09C312.53 946.71 303.69 944.78 298.47 953.4C293.25 962.02 297.16 961.56 297.16 961.56C297.16 961.56 301.53 949.66 306.18 950.79C310.83 951.92 313.21 955.12 316.5 963.44C319.79 971.76 320.43 976.79 320.43 976.79C320.43 976.79 325.73 966.46 327.01 955.67C328.29 944.89 327.01 940.85 327.01 937.18C327.01 933.51 325.97 926.15 325.97 926.15V926.16Z" fill="#E4FFA4"/>
|
||||
<path d="M425.18 614.18C436.28 622.92 445.31 633.47 452.26 647.12C459.21 660.77 460.07 675.46 460.07 675.46C460.07 675.46 454.8 661.27 450.51 655.11C446.22 648.95 444.43 650.5 443.59 651.8C442.75 653.1 444.24 654.99 445.42 657.47C446.6 659.95 449.79 664.9 453.11 675.01C456.44 685.12 456.7 693.27 456.7 693.27C456.7 693.27 450.84 674.04 442.01 662.22C433.18 650.4 420.91 639.56 411.76 634.78C411.76 634.78 417.69 635.61 425.18 638.73C432.67 641.85 436.1 644.5 438.39 643.95C440.68 643.41 439.26 638.65 437.04 633.53C434.02 626.56 425.19 614.17 425.19 614.17L425.18 614.18Z" fill="#E4FFA4"/>
|
||||
<path d="M388.34 956.57C413.9 960.1 446.2 957.69 467.03 952.37C489.39 946.65 504.8 933.78 504.8 933.78C504.8 933.78 493.12 938.23 477.91 939.71C462.71 941.19 453.62 939.71 453.62 939.71C453.62 939.71 469.38 937.67 484.4 931.92C499.42 926.17 514.55 913.57 514.55 913.57C514.55 913.57 504.32 918.54 476.84 924.39C449.36 930.24 418.08 926.14 404.05 922.64C404.05 922.64 414.25 928.11 423.31 931.52C432.37 934.94 437.46 937.07 437.6 940.09C437.83 945 430.05 948.64 416.26 951.68C403.38 954.52 388.34 956.57 388.34 956.57V956.57Z" fill="#E4FFA4"/>
|
||||
<path d="M391.97 564.33C358.09 550.8 337.88 506.45 322.9 475.16C299.15 422.8 285.14 366.6 277.07 309.82C273.99 289.78 271.38 269.67 268.78 249.62C268.43 246.64 267.71 243.25 266.13 240.76C261.75 233.47 252.1 233.67 244.84 236.82C240.66 238.52 237.09 241.11 233.42 243.83C158.8 302.24 102.08 382.29 63.7801 468.54C36.9901 529.31 16.93 593.48 8.01004 659.39C6.81004 668.81 5.85002 678.25 5.21002 687.72C5.21002 687.72 1.68003 687.13 1.69003 687.13C9.35003 660.21 18.7601 633.85 30.3401 608.37C39.0801 589.3 48.7101 570.17 62.1501 553.8C66.8401 548.33 71.6 542.93 77.39 538.56C83.83 533.61 93.34 531.69 100.1 537.5C105.19 541.81 107.45 547.83 108.46 554.06C110.33 567.82 111.76 585.76 113.16 599.69C115.56 627.36 119.44 654.92 125.87 681.95L121.69 682.24C122.82 670.08 124.24 658.01 125.94 645.94C131.16 609.68 138.43 573.3 154.55 540.07C168.83 512.2 193.4 507.63 197.95 543.89C201.42 571.08 203.17 598.44 205.29 625.7L201.76 625.54C203.63 614.11 205.89 602.8 208.51 591.53C212.52 574.68 217.25 557.68 224.99 542.07C228.88 534.22 233.1 526.18 239.42 519.85C246.18 512.81 255.84 510.73 265.24 512.79C271.29 514.1 276.48 517.55 281.24 521.21C294.81 532 306.79 544.38 320.55 554.87C334.21 565.11 350.01 572.3 365.87 578.56C371.21 580.63 376.57 582.63 382.09 584.37C376.29 583.96 370.61 582.6 365.04 580.99C348.34 575.94 332.56 568 318.29 557.97C304.34 547.79 291.95 535.46 278.57 524.67C271.9 519.28 264.81 515.74 256.34 516.39C247.98 516.72 241.79 522.41 237.11 529.19C224.35 548.14 217.71 570.4 212.31 592.44C208.43 608.11 205.28 625.21 202.8 641.21C200.8 612.04 198.68 582.47 195.1 553.49C194.56 549.34 193.82 544.25 192.95 540.19C191.22 532.08 186.32 518.96 175.86 523.79C167.99 527.3 162.96 534.78 158.91 542.37C153.42 552.97 149.48 564.46 145.79 575.85C138.23 598.84 133.84 622.66 130.32 646.58C127.88 662.77 126.17 679.84 124.55 696.15C119.16 671.33 114.13 646.12 111.43 620.9C108.89 599.36 107.46 577.58 103.53 556.25C103.14 553.88 102.48 551.45 101.58 549.26C97.59 539.33 89.39 536.75 80.7 543.21C75.4 547.25 70.7701 552.34 66.3401 557.35C52.9801 573.15 43.23 591.55 34.37 610.25C20.23 640.47 9.07004 672.49 0.540039 704.75L1.62003 687.52C3.41003 658.87 8.01003 630.47 14.34 602.49C44.5 472.47 107.67 347.46 207.1 256.79C214.23 250.35 221.59 244.17 229.34 238.36C233.3 235.4 237.53 232.37 242.22 230.48C252.25 226.11 265.85 226.57 272.02 237.22C274.86 241.78 275.33 247.15 275.96 252.29C277.14 261.77 278.17 271.23 279.28 280.69C286.1 346.78 299.12 412.7 326.55 473.53C338.26 499.3 351.2 525.02 370.54 545.92C376.96 552.83 383.87 559.37 391.97 564.33V564.33Z" fill="#0E1016"/>
|
||||
<path d="M339.66 524.44C326.32 504.71 315.55 483.01 305.86 461.26C280.66 403.67 264.92 342.46 253.68 280.76C251.74 274.35 249.14 264.77 241.09 265.55C229.81 266.62 220.11 274.06 211.55 281.16C166.59 320 130.66 368.53 99.9199 419.13C87.6899 439.56 76.32 460.53 65.7 481.91C84.46 437.97 107.73 395.75 136.08 357.22C157.61 328.45 181.22 301.04 208.33 277.33C214.76 272.03 221.36 266.43 228.85 262.44C246.84 253.07 256.79 260.23 259.69 279.46C262.21 294.1 264.64 309.04 267.62 323.52C274.79 358.47 284.81 392.83 296.38 426.56C308.29 460.18 322.16 493.32 339.67 524.44H339.66Z" fill="#0E1016"/>
|
||||
<path d="M224.69 474.61C220.42 443.69 220.09 412.36 221.11 381.2C221.43 370.79 221.94 360.43 222.8 349.99L226.51 350.77C216.52 381.18 207.28 411.89 200.49 443.19C198.21 453.62 196 464.08 194.06 474.6C195.3 463.97 196.96 453.39 198.91 442.87C205.78 405.39 216.74 368.41 228.39 332.14C223.41 379.34 221.88 427.21 224.7 474.6L224.69 474.61Z" fill="#0E1016"/>
|
||||
<path d="M139.12 500.39C143.78 479.77 149.73 459.54 155.82 439.29C159.01 429.19 162.76 419.3 166.71 409.48C170.65 399.66 174.91 390 179.34 380.38L181.68 381.83C151.37 421.92 131.04 468.43 111.84 514.59C115.75 502.64 120.43 490.95 125.31 479.37C139.97 444.58 156.63 410.33 179.51 380.12L187.25 370.02L181.86 381.57C173.11 400.68 164.85 420.13 158.07 440.03C151.51 460.05 145.86 480.41 139.13 500.4L139.12 500.39Z" fill="#0E1016"/>
|
||||
<path d="M640.16 251.83C645.12 249.62 650.99 248.55 656.27 247.37C657.61 247.05 660.17 246.6 661.34 246.11C662.68 245.55 663.82 244.53 664.62 243.19C667.14 238.68 666.57 233.17 665.76 228.1C665.11 224.57 664.23 220.95 663.28 217.46C658.54 199.8 649.45 183.66 638.3 169.23C624.97 151.76 608.65 136.51 590.69 123.91L593.11 121.61C599.81 134.2 605.65 147.11 610.24 160.61C611.77 165.12 613.14 169.7 614.45 174.37C614.86 175.84 614.21 175.09 612.74 175.51C611.08 176 611.23 175.8 611.12 174.08C611.01 172.91 608.78 172.98 608.61 171.82C606.83 159.23 602.01 147.15 596.65 135.67C593.78 129.44 589.98 123.01 586.45 117.15C604.43 128.25 620.78 141.98 634.65 157.89C651.99 177.94 665.74 200.87 669.47 227.52C670.49 234.91 670.8 245.68 662.59 249.25C660.55 249.97 658.86 250.14 656.86 250.52C651.31 251.39 645.83 252.16 640.15 251.83H640.16Z" fill="#0E1016"/>
|
||||
<path d="M539.99 148.37C557.57 141.03 588.78 140.7 605.8 149.57C606.18 149.75 606.01 150 605.88 150.59C605.68 151.42 604.96 152.54 604.36 152.97C604.24 153.03 604.19 152.94 604.11 152.92C603.3 152.57 601.74 151.95 600.9 151.6C587.24 145.84 571.66 145.2 557.18 147.82C551.99 148.83 546.71 150.38 542.03 152.74C539.09 154.14 536.99 149.76 539.99 148.37V148.37Z" fill="#0E1016"/>
|
||||
<path d="M566.15 155.12C589.73 156.54 616.43 167.04 626.15 190.17C622.1 185.66 618.24 181.27 613.64 177.63C611.6 176.02 608.68 173.94 606.59 172.48C594.43 164.02 580.51 158.77 566.16 155.12H566.15Z" fill="#0E1016"/>
|
||||
<path d="M515.04 130.01C526.18 129.04 537.44 130.14 548.43 132.11C559.34 134.13 570.17 137.8 579.9 143.2C582.64 144.71 581.7 146.45 578.86 145.06C578.84 145.05 577.45 145.83 577.43 145.82L576.76 145.23L574 145.35C561.02 138.26 546.35 134.66 531.7 133.1C526.44 132.56 521.13 132.31 515.85 132.45L514.98 132.22C513.84 131.95 513.88 130.22 515.04 130.01Z" fill="#0E1016"/>
|
||||
<path d="M623.55 147.6C644.23 160.3 663.2 176.56 676.62 196.97C680.57 203.28 684.87 210.34 684.53 218.26C684.25 223.93 679.79 228.03 675.54 231.14C673.5 232.65 671.43 233.97 669.11 235.17C668.17 235.65 667.02 235.29 666.53 234.35C666.04 233.41 666.42 232.27 667.33 231.78C671.21 229.48 675.18 226.52 678.14 223.33C687.98 212.61 663.62 184.57 655.77 176.46C646.08 166.6 635.28 157.81 623.77 150.13L623.01 149.63L622.63 149.38C622.45 149.26 622.23 149.1 622.05 148.95C621.36 148.41 621.95 147.2 622.8 147.44L623.54 147.61L623.55 147.6Z" fill="#0E1016"/>
|
||||
<path d="M543.45 219.94C556.98 210.44 568.7 211.97 583.25 218.05C591.54 221.55 599.43 225.78 607.57 229.44C612.87 231.7 618.29 234.12 624.06 234.37C631.21 234.61 635.57 229.36 636.19 221.99C640.09 233.78 629.44 241.17 618.65 238.8C609.6 236.97 601.43 232.51 593.1 228.86C584.12 224.91 575.33 220.21 565.79 218.05C558.24 216.28 550.98 217.97 543.45 219.94V219.94Z" fill="#0E1016"/>
|
||||
<path d="M590.59 225.6C581.19 225.14 571.67 227.34 562.96 230.7L564.36 227.36C569.67 238.95 579.57 249.07 590.77 255.04C593.65 256.52 596.71 257.56 599.85 258.17C609.18 260.5 623.21 254.82 630 263.33C630.05 263.35 626.34 264.57 626.35 264.58C625.87 254.66 622.91 244.32 616.5 236.54C616.12 236.11 616.17 235.45 616.62 235.09C617.03 234.76 617.63 234.81 617.98 235.2C619.79 237.18 621.5 239.27 622.87 241.58C627.08 248.53 629.49 256.31 630.37 264.41L631.04 271.35C631.04 271.35 626.71 265.65 626.7 265.65C625.37 263.45 622.55 262.38 619.42 262.01C612.88 261.26 605.92 262.49 599.3 261.05C585.66 258.13 574.75 248.32 565.94 238.04C563.1 234.64 560.57 230.9 558.55 226.91C561.83 225.87 565.34 224.94 568.63 224.36C575.93 223.02 583.46 222.47 590.82 223.62C592.08 223.82 591.85 225.71 590.59 225.61V225.6Z" fill="#0E1016"/>
|
||||
<path d="M596.15 258.76C596.15 258.76 597.41 253.82 600.82 248.85C604.23 243.88 609.94 240.66 609.94 240.66C609.94 240.66 610.23 247.82 609.21 252.94C608.19 258.06 607.16 260.29 607.16 260.29C607.16 260.29 603.28 260.3 600.82 259.82C598.36 259.34 596.15 258.76 596.15 258.76V258.76Z" fill="#0E1016"/>
|
||||
<path d="M718.72 296.83C700.05 281.07 680.8 264.11 662.53 247.85C661.61 247.03 662.8 245.53 663.81 246.31C677.96 257.3 693.7 269.78 707.67 280.99C712.5 284.89 717.32 288.81 722.11 292.76C723.55 294 721.68 293.86 720.65 294.84C719.79 295.66 718.71 296.83 718.71 296.83H718.72Z" fill="#0E1016"/>
|
||||
<path d="M695.01 360.47C695.01 360.47 697.57 360.62 700.74 362.74C703.91 364.86 711.47 364.74 714.72 360.72C719.65 354.63 717.24 349.04 724.64 346.37" stroke="#0E1016" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M697.72 361.16C697.72 361.16 704.39 365.86 709.78 363.66C715.17 361.46 716.98 357.71 717.69 354.64C718.4 351.57 718.71 350.13 719.43 349.38C720.15 348.63 725.46 346.95 723.22 345.59C719.69 343.45 716.14 338.12 716.14 338.12C716.14 338.12 712.93 347.58 707.65 352.31C702.37 357.04 697.46 359.67 697.46 359.67L697.72 361.16Z" fill="#0E1016"/>
|
||||
<path d="M731.18 319.57C733.29 315.24 735.97 309.19 739.07 306.6C742.17 304.02 744.35 304.85 745.13 306.6C746 308.58 744.57 310.3 741.95 311.47C739.8 312.43 738.27 313.52 736.27 315.01C734.55 316.29 731.18 319.56 731.18 319.56V319.57Z" fill="#0E1016"/>
|
||||
<path d="M691.49 322.64C689.09 319.24 684.72 314.79 679.85 312.61C674.98 310.43 672.41 312.79 672.19 315.3C672.01 317.32 674.75 319.55 678.48 319.37C682.21 319.19 685.57 320.59 688 321.42C690.43 322.25 691.49 322.64 691.49 322.64V322.64Z" fill="#0E1016"/>
|
||||
<path d="M668.37 327.92C653.27 322.99 652.79 303.72 666.54 296.68C678.34 291.11 694.47 296.6 700.2 308.12C692.68 299.62 678.9 296.5 668.92 301.44C662.92 304.24 659.08 311.07 660.68 317.68C661.73 321.95 664.76 325.28 668.37 327.93V327.92Z" fill="#0E1016"/>
|
||||
<path d="M666.99 300.14L658.84 288.31C656.5 284.81 653.07 279.88 650.95 276.3C652.52 278.1 654.18 279.82 655.83 281.54C658.97 284.81 667.62 293.5 670.94 296.83C672.34 298.32 671.27 298.95 670.03 299.74C669.01 300.39 668.01 301.49 667 300.14H666.99Z" fill="#0E1016"/>
|
||||
<path d="M522.9 233.47C527.55 241.95 533.98 249.28 541.12 255.7C545.1 259.24 549.54 262.5 553.67 265.95C546.18 266.95 542.36 268.37 531.08 267.14L533.46 264.05C533.79 264.54 534.13 265.15 534.54 265.65C541.08 274.14 550.88 279.47 560.98 282.7C569.24 285.22 577.96 286.8 586.61 287.24C594.12 287.71 601.51 286.86 609.09 287.77C613.87 289.09 617.45 292.64 621.14 295.59C627.93 301.48 634.29 307.83 640.57 314.23C644.91 318.69 649.23 323.15 653.45 327.74C653.45 329.73 653.72 331.89 654.14 333.88C656.7 347.6 667.98 356.84 681.4 359.16C681.79 367.38 681.1 375.6 678.73 383.49L676.55 382.25C677.97 380.73 679.35 379.09 680.64 377.44C684.64 372.52 687.04 366.65 689.19 360.74C689.19 360.74 689.53 359.75 689.54 359.74C703.15 359.41 712.39 346.77 716.24 334.82L717.31 337.15C720.09 343 726.73 346.72 733.15 346.25C734.74 353.12 732.95 360.06 731.49 366.76C731.52 366.86 729.29 365.6 729.28 365.63C735.48 358.7 738.94 349.68 740.34 340.58C746.65 329.2 753.08 312.43 747.92 299.73C745.87 294.69 741.03 291.48 735.5 291.31C728.65 290.9 722.83 294.7 717.1 298.03C720.64 291.42 728.1 287.52 735.57 287.28C749.08 287.12 755.39 299.63 754.34 311.73C753.41 322.6 749.37 332.94 743.6 342.12L743.85 341.46C742.85 346.29 741.45 350.88 739.39 355.35C736.79 361.13 732.56 366.07 728.14 370.58L728.94 366.23C729.88 360.4 731.18 354.28 729.95 348.44L731.71 349.79H731.67C726.15 349.8 721.02 346.84 717.81 342.5C716.76 341.05 715.94 339.56 715.39 337.76H717.29C713.27 349.71 704.16 361.58 690.76 362.86L692.18 361.8C689.8 367.93 687.05 374.1 682.75 379.13C680.6 381.89 677.56 384.81 675.13 387.36C675.87 384.25 676.78 380.45 677.24 377.33C678.14 371.9 678.07 366.28 677.65 360.82L679.18 362.54C671.14 360.94 663.65 357.14 658.07 351.04C652.52 344.89 649.53 336.75 649.32 328.51L649.85 329.91C639.9 319 629.7 308.21 618.51 298.59C615.31 296.08 612.2 293.03 608.38 291.82C604.16 291.35 599.79 292.02 595.42 292.22C586.36 292.69 577.12 291.69 568.26 289.74C553.38 286.48 537.84 279.49 529.6 266.03L527.36 261.9L531.99 262.94C537.9 264.28 544.28 263.94 550.33 263.71L549.82 266.2C546.39 264.83 543.27 262.81 540.42 260.49C534.8 255.81 530.31 249.88 526.8 243.52C525.12 240.32 523.6 236.98 522.86 233.45L522.9 233.47Z" fill="#0E1016"/>
|
||||
<path d="M680.73 378.53C688.47 380.17 696.45 379.18 704.03 377.17C711.43 375.35 719.09 372.4 723.2 365.62C725.92 361.19 728.32 356.19 730.98 351.64C731.31 351.02 732.09 350.79 732.71 351.13C733.29 351.45 733.51 352.17 733.23 352.76C732.09 355.21 730.93 357.65 729.74 360.07C728.62 362.29 727.28 365.28 725.92 367.35C719.97 377.42 707.4 380.14 696.73 381.92C691.23 382.7 685.56 382.47 680.15 381.2C679.41 381.03 678.96 380.29 679.13 379.56C679.3 378.84 680.01 378.39 680.73 378.53V378.53Z" fill="#0E1016"/>
|
||||
<path d="M504.49 248.3C513.12 260.4 520.21 273.87 527.3 286.78C536.33 301.79 550.26 310.86 568.07 309.39C577.76 309.07 587.85 307.62 597.51 306.33C607.28 304.73 612.59 306.32 619.76 313.28C633.81 327.68 644.56 344.74 656.15 361.01C661.78 368.78 669.45 375.21 678.76 377.86C680.56 378.36 680.05 381.31 677.94 380.84C661.2 376.31 653.7 364.28 644.42 351.08C638.66 343.02 632.74 335.07 626.44 327.43C621.16 321.44 613.54 310.88 605.15 310.57C601.3 310.47 594.79 311.75 590.78 312.27C583.35 313.36 575.82 314.35 568.28 314.72C560.48 315.31 552.28 314.36 545.18 310.87C535.61 306.4 528.11 298.32 523.04 289.26C521.18 285.96 519.69 282.39 518.08 279.05C513.45 268.87 508.75 258.72 504.48 248.31L504.49 248.3Z" fill="#0E1016"/>
|
||||
<path d="M693.26 368.72C696.91 369.54 700.47 370.16 704.13 370.36C707.79 370.66 711.15 370.85 714.86 370.01C709.1 376 698.94 373.68 693.26 368.72V368.72Z" fill="#0E1016"/>
|
||||
<path d="M551.95 187.15C546.13 173.19 541.66 158.74 535.51 145.06C535.16 144.32 534.57 143.27 534.12 142.68C534.14 142.68 533.98 142.56 533.86 142.46C533.76 142.36 533.51 142.23 533.35 142.11C527.4 138.87 520.26 136.81 513.75 134.38C509.18 132.75 504.6 131.09 500.03 129.36C481.71 122.57 464.07 113.62 447.2 103.8C421.88 89.04 397.44 72.34 376.9 51.21L380.22 49.6C381.07 54.73 382.78 60.11 384.51 65.06C395.87 95.63 416.53 122.15 441.03 143.42C442.63 144.72 446.94 148.24 448.6 149.59C450.32 150.85 454.79 154.16 456.44 155.4C458.26 156.72 462.61 159.6 464.5 160.91C482.85 172.74 502.45 182.39 522.9 190.16C514.1 189.02 505.53 186.52 497.19 183.49C445.55 164.26 398.93 118.62 380.46 66.47C378.75 61.11 377.16 55.8 376.34 50.09L375.49 44.3C390.26 59.55 407.19 72.6001 425.03 84.1701C449.39 99.8301 475.02 113.43 502.37 123.06C511.44 126.44 520.58 129.47 529.77 133.26C532.35 134.4 535.24 135.57 537.79 137.52C539.87 139.15 540.82 141.89 541.83 144.23C546.96 158.15 550.06 172.49 551.95 187.14V187.15Z" fill="#0E1016"/>
|
||||
<path d="M448.75 130.95C460.21 141.49 473.47 149.97 487.52 156.54C497.11 161.16 506.69 165.5 517.17 167.97C515.5 168.14 513.82 168.1 512.14 168C487.47 166.09 463.95 149.84 448.74 130.95H448.75Z" fill="#0E1016"/>
|
||||
<path d="M641.11 161.28C641.65 155.48 642.37 149.75 644.03 144.13C646.65 136 648.09 128.37 640.05 123.19C618.88 108.96 599.25 92.19 583.29 72.21C578.61 66.13 573.47 58.55 569.3 52.12C560.47 38.43 552.21 24.44 544.56 10.02L548.22 9.02C549.69 33.74 555.65 58.27 564.35 81.38C568.54 92.98 573.6 104.39 579.13 115.47C584.67 126.39 590.59 137.37 598.39 146.83C599.12 147.57 599.09 148.78 598.3 149.46C597.58 150.1 596.48 150.05 595.82 149.37C594.6 148.12 593.59 146.95 592.57 145.7C589.56 142 586.86 138.07 584.27 134.08C577.51 123.41 571.77 112.17 566.64 100.66C564.16 94.98 561.53 88.98 559.51 83.13C552.37 62.23 548.26 40.5 545.37 18.67C544.56 13.17 544.1 5.6 543.54 0L548.01 8.24C559.28 29.56 571.63 50.67 586.14 69.98C601.12 88.88 619.84 104.72 639.67 118.32C641.17 119.31 643.51 120.77 644.84 122.04C651.93 128.12 650.45 137.38 648.01 145.31C647.34 147.62 646.99 150.04 646.69 152.47C646.22 156.39 646.04 160.45 646.1 164.37C646.03 167.95 641.02 162.25 641.11 161.27V161.28Z" fill="#0E1016"/>
|
||||
<path d="M586.56 312.01C567.05 338.8 561.04 378.45 559.94 411C558.38 460.61 566.29 508.56 587.13 553.81C598.13 578.29 610.32 602.42 620.94 627.14C621.42 628.25 618.44 633.65 617.91 632.41C612.39 619.5 606.66 606.67 600.75 593.94C582.29 555.97 564.6 516.66 559.16 474.35C556.09 453.32 555.18 432.09 555.63 410.87C556.26 389.61 558.83 368.35 564.91 347.88C568.49 335.93 573.16 324.15 579.92 313.59C580.97 312.02 581.93 310.57 583.18 309.07C583.97 308.12 585.38 307.99 586.33 308.78C587.31 309.59 587.41 311.06 586.56 312V312.01Z" fill="#0E1016"/>
|
||||
<path d="M514.11 266.45C485.87 308.03 468.64 356.86 461.51 406.48C459.76 419.24 458.73 432.11 458.49 444.99C457.69 483.8 459.86 522.76 466.61 561C473.26 599.33 484.58 636.65 497.13 673.43C519.52 734.28 536.42 798.59 542.35 863.24C542.35 863.24 541.3 866.42 539.62 866.58C537.94 866.75 536.44 865.53 536.28 863.85C534.18 838.03 530.32 812.35 525.11 786.96C512.8 722.96 485.08 663.45 469.55 600.25C459.88 562.26 454.54 523.19 453.01 484.05C449.74 423.11 456.99 367.39 481.52 311.09C489.46 292.95 498.49 275.31 509.93 259.07C509.93 259.07 514.76 265.52 514.12 266.45H514.11Z" fill="#0E1016"/>
|
||||
<path d="M459.04 904.98C482.04 900.38 505.07 891.22 522.31 874.99C545.17 852 562.25 818.34 563.59 785.63C564 768.64 560.5 751.84 555.68 735.58C553.79 728.9 551.27 722.39 549.66 715.61C546.07 699.71 544.47 678.15 550.75 662.82C555.25 653.12 561.57 643.58 571.39 638.66L576.35 636.19C572.85 641.66 568.83 647.43 567.8 653.92L566.11 652.06C567.4 651.94 568.45 652 569.6 652.13C587.72 654.95 591.79 673.13 588.57 688.61C588.33 689.74 583.16 693.52 583.73 690.93C585.67 681.73 586.36 670.89 580.58 663C577.27 658.55 571.9 655.52 566.36 655.24L564.43 654.59L564.66 653.38C565.49 648.19 568.2 643.62 570.83 639.21L572.85 641.28C570.03 643.01 567.53 645.16 565.32 647.6C560.97 652.55 557.72 658.62 555.23 664.69C551.91 674.03 551.94 684.34 552.6 694.23C553.31 702.61 554.56 710.96 557.03 718.97C558.59 723.78 560.26 729.01 561.64 733.85C565.53 747.31 568.42 761.12 569.4 775.15C571.77 810.58 559.4 846.95 534.85 872.8C525.29 883.39 513.09 891.62 500 897.08C493.48 899.82 486.79 902.03 479.97 903.67C473.15 905.33 466.28 906.52 459.25 906.97C458.7 907 458.22 906.58 458.19 906.03C458.16 905.5 458.54 905.05 459.05 904.98H459.04Z" fill="#0E1016"/>
|
||||
<path d="M573.75 729.21C573.63 722.22 574.18 715.26 575.58 708.38C577.12 701.4 579.86 694.91 583.01 688.53C588.63 677.02 597.32 667.47 608.84 661.63C610.44 660.79 612 660.03 613.75 659.32C615.22 658.72 612.67 660.91 613.27 662.37C613.83 663.74 614.19 665.72 613.18 666.35C608.59 669.23 604.08 672.39 600.22 675.95C591.79 683.71 585.5 694.17 580.62 704.61C577.2 712.41 575.38 720.75 573.74 729.21H573.75Z" fill="#0E1016"/>
|
||||
<path d="M912.2 547.03C825.76 532.1 738.08 525.18 650.65 518.85L644.68 518.42C643.62 518.2 642.81 518.73 642.58 519.53L640.45 531.08C620.73 640.81 597.82 757.12 577.17 866.31L576.61 869.25L576.33 870.72C576.16 871.45 576.52 872.09 577.13 872.43C577.23 872.48 577.33 872.52 577.44 872.55C577.57 872.58 577.59 872.59 577.9 872.62L579.39 872.78C582.99 873.14 605 875.57 609.14 876L704.36 886.2C705.3 886.3 705.98 887.15 705.88 888.09C705.78 889.03 704.94 889.71 704 889.61C680.78 887.19 599.32 878.65 577.5 876.41C574.42 876.25 571.84 873.1 572.56 870.02C572.73 869.1 573.2 866.56 573.38 865.61C594.07 753.38 615.42 633.7 637.22 521.35L637.5 519.88L637.64 519.15C637.91 517.24 639.03 515.44 640.7 514.45C641.8 513.56 643.91 513.42 645.05 513.55C670.24 515.57 697.54 517.7 722.61 520.08C770.33 524.36 818.01 529.39 865.32 537.07C881.1 539.56 896.84 542.18 912.56 545.07C913.1 545.17 913.46 545.69 913.36 546.23C913.26 546.77 912.74 547.13 912.2 547.03V547.03Z" fill="#0E1016"/>
|
||||
<path d="M953.48 463.55C939.98 430.93 924.04 399.22 912.58 365.76C911.4 362.5 908.33 353.97 907.19 350.81L906.97 350.19C907.13 350.7 907.88 350.82 908.2 350.31C908.23 350.25 908.26 350.21 908.27 350.15C908.27 350.15 908.26 350.19 908.25 350.21C908.12 350.77 907.82 351.88 907.68 352.46L897.28 393.51C897.06 394.48 896.08 395.08 895.12 394.83C894.17 394.59 893.59 393.61 893.84 392.66L904.63 351.64L904.97 350.36L905.14 349.72L905.22 349.4C905.75 347.05 909.14 346.88 909.9 349.15L910.12 349.77C911.22 352.91 914.23 361.51 915.37 364.77C926.52 398.31 942.12 430.04 955.33 462.79C955.54 463.3 955.29 463.88 954.78 464.09C954.27 464.3 953.69 464.05 953.48 463.54V463.55Z" fill="#0E1016"/>
|
||||
<path d="M949.97 467.69C940.93 450.23 933.4 431.96 926.24 413.64C919.21 395.28 912.05 376.84 907.38 357.72C916.41 385.77 928.48 412.76 940.16 439.78C944.07 448.79 948.11 457.71 951.79 466.87C952.24 468.01 950.55 468.85 949.96 467.69H949.97Z" fill="#0E1016"/>
|
||||
<path d="M912.51 545.05C924.16 546.1 940.02 547.76 950.06 540.9C954.47 537.77 957.93 533.4 960.51 528.64C964.04 522.2 965.64 515.05 964.16 507.73C963.59 504.09 962.66 500.48 961.61 496.9C961.45 496.37 961.75 495.81 962.29 495.65C962.81 495.5 963.35 495.79 963.52 496.29C964.73 499.88 965.84 503.53 966.57 507.31C969.27 518.93 964.2 531.03 956.18 539.42C950.75 545.29 942.83 547.95 935.01 548.33C927.34 548.9 919.74 548.13 912.26 547.05C911 546.84 911.21 544.95 912.52 545.07L912.51 545.05Z" fill="#0E1016"/>
|
||||
<path d="M937.06 550.47C928.1 551.42 918.83 551.01 909.99 549.27C909.99 549.28 909.99 549.29 909.98 549.31C859.75 541.95 809.49 534.16 758.87 529.75C727.22 526.95 694.93 524.37 663.21 521.9C660.14 521.66 651.79 521.02 648.86 520.8L646.47 520.62L646.17 520.6C645.27 520.46 644.41 521.15 644.31 522.04C641.77 535.01 639.42 548 638.49 561.21C642.15 548.66 644.44 535.85 646.55 522.98C648.41 523.12 652.07 523.41 653.48 523.52C688.36 526.25 723.87 529.09 758.67 532.2C806.84 536.53 854.65 544 902.48 551.21C906 551.74 908.23 555.25 907.22 558.67C902.16 575.75 897.23 592.87 892.37 610.01C875.69 669.3 858.77 729.41 845.06 789.5C867.02 713.97 889.43 635.43 910.14 559.54C911.19 555.69 914.91 553.27 918.88 553.72C927.32 554.69 935.93 554.34 944.11 552C950.66 550.17 956.63 546.09 960.29 540.33C953.68 545.96 945.76 549.67 937.07 550.49L937.06 550.47Z" fill="#0E1016"/>
|
||||
<path d="M739.86 922.63C722.13 924.3 705.57 910.66 702.69 893.22C701.71 887.48 702.29 881.44 703.61 875.88C704.47 872.21 705.42 868.38 707.04 864.92C709.24 859.78 713.05 855.12 716.91 851.05C716.59 853.58 715.92 857.75 715.79 860.24C715.44 864.01 715.94 867.71 716.81 871.36L715.3 869.9C716.95 870.3 718.43 870.75 719.97 871.25C724.54 872.77 728.98 874.9 732.84 877.8C734.66 879.12 732.75 881.87 730.87 880.61C729.76 879.82 728.29 878.72 727.09 878.09C722.89 875.73 718.09 874.16 713.34 873.46C711.91 867.7 711.77 861.74 712.87 855.92L715.76 857.3C711.25 862.84 708.68 869.71 707.37 876.71C705.76 883.65 705.57 890.86 707.92 897.59C712.07 910.19 724.28 919.71 737.63 918.94C738.54 918.89 743.46 922.16 740.85 922.49C740.85 922.49 740.17 922.57 739.84 922.6L739.86 922.63Z" fill="#0E1016"/>
|
||||
<path d="M768.68 922.84C757.24 932.65 738.74 927.91 732.68 914.35C727.79 902.74 727.99 886.66 731.04 874.56C733.6 865.85 738.33 857.79 745.56 852.16L749.3 849.28C747.77 853.3 745.91 857.81 744.77 861.89C744.04 864.59 743.65 867.51 743.41 870.31L741.98 868.76C747.03 869.04 751.56 871.59 755.54 874.5C758.95 877.07 761.85 880.34 764.12 883.96C764.44 884.48 764.31 886.36 763.32 886.92C762.33 887.48 762.02 888.55 761.47 887.56C757.63 880.03 750.53 873.1 741.99 871.63L740.43 871.51L740.56 870.08C740.99 864.06 742.83 858.24 745.14 852.72L747.17 854.27C743.98 856.87 741.4 860.3 739.26 863.9C733.67 873.04 732.4 883.88 732.7 894.39C733.14 904.29 734.23 916.3 743.74 921.73C750.56 925.87 760.31 925.28 765.61 919.5C767.9 917.17 771.2 920.77 768.68 922.86V922.84Z" fill="#0E1016"/>
|
||||
<path d="M967.59 491.45C967.23 490.53 966.08 490.14 965.2 490.76C964.61 491.18 964.43 491.98 964.69 492.65C967.81 500.63 970.57 508.99 968.87 517.55C967.65 524.38 963.27 536.21 961.27 542.71C927.6 636.55 887.35 802.17 861.38 900.43C860.82 901.31 859.57 902.5 858.65 903.19C849.68 910.02 837.2 910.05 827.13 905.71C824.63 904.74 820.94 901.88 818.91 899.23C818.35 898.5 818.14 897.56 818.37 896.66C818.37 896.66 818.37 896.66 818.37 896.65C822.55 879.6 825.49 862.28 828.51 844.99C827 849.44 825.64 853.93 824.36 858.44C821.16 869.53 818.27 880.72 815.99 892.04C815.64 893.79 813.4 897.84 808.21 897.4C805.83 897.2 803.48 896.76 801.11 896.47C800.41 894.25 799.58 892.06 798.68 889.94C795.72 883.53 791.18 876.36 783.82 874.77C783.34 873.24 779.59 861.17 779.59 861.17L778.03 856.17L776.19 861.1C774.76 865.13 773.65 869.08 772.5 873.14C767.58 875.14 763.45 878.75 761.21 883.65C758.06 891.07 757.51 899.33 758.84 907.21C759.29 909.81 759.91 912.4 760.71 914.92C765.3 928.46 781.34 935.41 794.58 930.87C805.27 927.37 812.48 915.91 817.15 905.56C822.38 909.89 829.17 911.6 835.68 912.89C844.02 914.1 852.83 912.55 859.93 907.86C862.21 906.24 864.16 904.8 865.63 902.19C884.62 831.06 926.57 662.18 948.86 594.49C951.52 586 966.54 538.39 968.74 531.15C974.11 514.94 974.14 507.45 967.59 491.45V491.45ZM804.35 918.72C799.96 924.27 793.61 927.86 786.46 927.67C777.41 927.86 768.43 921.82 765.52 913.28C763.45 906.49 762.41 899.23 763.59 892.2C764.69 884.23 767.99 879.01 775.44 875.64C776.24 872.9 777.05 870.1 777.92 867.34C779.39 871.57 781.14 876.63 781.14 876.63L781.35 877.22C789.67 880.17 793.99 888.62 797.7 896.19C801.11 903.15 801.43 907.12 802.94 911.05C803.34 907.32 802.94 903.59 802.09 899.95C802.26 899.99 802.45 900.03 802.65 900.07C808.78 901.32 811.74 908.35 808.26 913.55C807.05 915.35 805.76 917.08 804.36 918.73L804.35 918.72Z" fill="#0E1016"/>
|
||||
<path d="M865.83 386.9C866.77 369.54 871.27 352.64 875.87 335.95L877.47 340.53C887.95 370.54 899.4 400.33 913.11 429.03C917.33 437.83 923.17 448.56 927.95 457.15C937.15 473.89 946.81 490.42 956.97 506.6C960.69 511.95 962.08 519.49 958.9 525.31C960.18 520.14 959.33 514.86 956.83 510.39C943.44 489.94 930.74 468.98 919.01 447.52C908.79 428.81 900.01 409.27 892.12 389.51C885.8 373.67 879.84 357.72 874.59 341.48C874.59 341.48 877.5 341.42 877.49 341.43C874.32 352.62 871.52 364 869.7 375.49C869.12 379.32 868.64 383.17 868.48 386.99C868.39 388.73 865.8 388.61 865.83 386.89V386.9Z" fill="#0E1016"/>
|
||||
<path d="M846.49 380.06C847.15 377.3 853.86 349.76 854.34 347.78C857.04 357.03 860.24 366.21 863.71 375.22C869.92 391.32 877.2 407.04 885.61 422.11C897.18 442.43 911.13 461.29 925.92 479.39C933.63 488.86 941.73 498.01 950.05 506.94C952.89 509.83 955.71 513.31 956.31 517.45C954.82 513.64 951.9 510.7 948.93 508.03C940.26 499.35 932.13 490.17 924.19 480.82C903.15 455.82 884.18 428.7 870.7 398.82C863.88 383.91 857.87 368.68 853.24 352.92L855.68 352.89L848.78 381.97C848.44 383.24 846.18 381.32 846.48 380.06H846.49Z" fill="#0E1016"/>
|
||||
<path d="M650.82 515.66C650.94 514.76 653.32 494.57 653.34 494.49C660.22 494.02 667.03 494.11 673.8 494.25C716.72 495.48 759.4 501.24 801.6 508.82C837.76 515.39 873.71 523.15 909.11 533.07C915.05 534.72 920.75 536.84 926.68 537.85C932.68 539.09 938.65 538.74 944.77 538.61C938.94 540.87 932.46 540.61 926.35 539.91C921.13 539.34 913.56 536.91 908.42 535.46C873.11 525.62 837.18 518.01 801.14 511.39C759.1 503.95 716.51 498.5 673.76 498.39C667.7 498.42 661.54 498.55 655.62 499.01L657.76 496.94L655.73 516.2C655.33 519.42 650.52 518.94 650.83 515.65L650.82 515.66Z" fill="#0E1016"/>
|
||||
<path d="M660.09 496.26C660.19 495 661.14 485.47 661.21 484.55C674.48 485.1 687.67 486.07 700.86 487.25C738.83 490.62 776.8 496.24 814.01 504.61C848.16 512.11 882.34 519.92 915.74 530.38C920.91 532.11 926.1 533.94 931.59 534.25C942.81 535.32 951.02 532.55 956.31 521.93C953.87 534.36 942.72 537.82 931.38 536.87C925.67 536.59 920.26 534.79 914.91 532.95C881.62 522.44 847.5 514.84 813.4 507.41C776.29 499.15 738.44 493.63 700.57 490.41C687.93 489.32 675.26 488.42 662.62 487.91L664.31 486.41L663.61 496.57C663.43 498.9 659.9 498.66 660.08 496.28L660.09 496.26Z" fill="#0E1016"/>
|
||||
<path d="M665.99 486.24C666.1 485.35 667.53 476.13 667.62 475.45C692.02 476.86 716.3 479.72 740.51 482.88C799.98 490.39 858.46 504.93 914.72 525.54C920.3 527.52 925.83 529.55 931.76 530.1C937.25 530.64 943.72 530.29 947.53 525.95C947.88 525.52 948.53 525.47 948.95 525.83C949.37 526.19 949.41 526.81 949.06 527.23C944.73 532.26 937.64 532.76 931.54 532.21C925.48 531.7 919.66 529.64 913.97 527.66C846.5 503.19 775.77 489.16 704.48 481.81C692.6 480.54 680.7 479.37 668.81 478.47L670.41 477.19L668.93 486.68C668.64 488.59 665.71 488.19 665.98 486.23L665.99 486.24Z" fill="#0E1016"/>
|
||||
<path d="M954.27 504.94C958.67 498.89 963.2 492.98 967.97 487.21C972.84 481.44 977.81 475.14 984.95 472.03C999.68 464.91 1017.31 472.87 1024.3 487.01C1024.86 488.17 1023.15 489.07 1022.53 487.93C1020.85 484.76 1018.51 481.94 1015.82 479.56C1004.45 469.18 988.27 469.83 977.65 480.91C972.61 486.23 968.03 492.12 963.55 497.95C961.29 500.89 959.05 503.85 956.86 506.84C956.18 507.75 955.68 507.15 955.04 506.57C954.5 506.08 953.69 505.73 954.26 504.94H954.27Z" fill="#0E1016"/>
|
||||
<path d="M997.61 469.75C1007.64 471.62 1005.3 489.77 1004.05 497.11C1002.42 505.32 999.38 517.1 997.38 525.35C992.85 544.03 988.2 562.77 986.07 581.87C981.76 613.91 989.97 636.79 1017.42 654.93C1025.32 660.22 1033.83 664.62 1042.46 668.89C1043.44 669.38 1043.85 670.57 1043.36 671.55C1042.87 672.54 1041.67 672.94 1040.68 672.45C1032.03 668.15 1023.36 663.65 1015.23 658.18C998.04 646.95 985.8 631.48 982.37 610.81C979.64 591.22 983.78 571.83 987.64 552.8C990.61 539.12 994.5 524.19 997.98 510.58C1000.27 501.36 1003.06 492.06 1002.67 482.52C1002.38 478.48 1001.93 472.73 997.28 471.71C996.03 471.52 996.28 469.55 997.6 469.74L997.61 469.75Z" fill="#0E1016"/>
|
||||
<path d="M1046.37 672.02C1049.19 673.55 1053.37 673.93 1055.36 676.16C1056.62 677.57 1057.75 679.07 1058.78 680.59C1066.7 692.42 1068.77 706.89 1067.87 720.83C1066.48 739.32 1056.5 755.59 1048.27 771.72L1046.08 770.01C1050.58 765.85 1055.07 761.67 1059.52 757.47C1063.05 754.35 1067.82 748.94 1071.9 746.66C1073.53 745.69 1075.74 745.39 1077.57 746.23C1080.22 747.67 1080.72 750.84 1081.13 753.33C1082.05 762.67 1081.27 771.85 1081.15 781.16L1078.11 780.52C1083 767.16 1087.67 753.53 1090.51 739.59C1093.65 725.89 1089.36 711.54 1081.49 700.14C1077.52 694.45 1072.57 689.43 1066.71 685.72C1056.46 679.53 1045.27 675.57 1034.94 668.97C1020.2 659.91 1009.65 644.64 1005.89 627.79C1001.16 606.3 1004.51 584.13 1008.63 562.88C1010.52 553.45 1012.69 544.09 1014.95 534.76C1017.66 523.11 1021.08 511.67 1022.7 499.9C1023.09 496.49 1023.3 493.1 1022.47 489.83C1021.44 486.54 1019.55 483.63 1017.89 480.58C1013.11 472.37 1007.91 464.33 1002.38 456.59C999.67 452.96 996.97 448.93 993.55 446.04C989.81 443.79 985.15 443.07 980.85 443.63C971.31 444.78 965.55 453.36 959.39 459.9C953.07 467.06 946.92 474.4 940.87 481.79C940.21 482.6 939.7 481.96 939.09 481.46C938.48 480.96 937.76 480.61 938.41 479.79C947.61 468.64 956.66 457.45 966.86 447.15C973.87 440.01 985.58 437.98 994.49 442.75C996.02 443.6 997.29 444.87 998.31 446.02C1000.76 448.75 1002.94 451.69 1005.08 454.64C1010.7 462.49 1015.93 470.55 1020.81 478.89C1022.59 482.11 1024.66 485.35 1025.74 488.94C1026.71 492.65 1026.51 496.6 1026.08 500.29C1024.46 512.27 1021.01 523.9 1018.31 535.56C1016.07 544.86 1013.92 554.18 1012.06 563.55C1007.02 590.7 1002.06 621.51 1017.39 646.24C1029.55 666.52 1049.65 671.34 1068.78 682.46C1083.42 691.83 1093.12 708.24 1095.02 725.45C1096.04 735.27 1093.08 745.02 1090.49 754.35C1087.76 763.61 1084.54 772.66 1081.05 781.65L1077.55 790.67C1077.92 781.54 1078.57 771.7 1078.64 762.68C1078.54 759.83 1079.33 748.96 1075.67 748.51C1072.77 748.33 1070.32 751.37 1068.14 753.19C1061.66 759.25 1054.53 766.12 1047.99 772.05L1041.71 777.83L1045.8 770.34C1052.07 758.47 1058.68 746.52 1062.5 733.63C1066.82 717.39 1065.37 696.89 1055.14 683.1C1052.61 679.67 1049.44 676.67 1045.89 675.12C1042.59 673.53 1042.27 670.03 1045.51 671.66L1046.37 671.99V672.02Z" fill="#0E1016"/>
|
||||
<path d="M356.7 532.55C364.18 473.86 377.64 415.86 398.09 360.3C420.06 303.5 451.46 250.78 487.25 201.68C492.08 195.14 496.95 188.7 502.12 182.33C503.2 181 503.42 183.37 504.75 184.44C506.1 185.54 508.01 184.98 506.87 186.3C501.77 192.4 496.79 198.78 491.93 205.18C448.48 262.97 411.35 326.6 390.32 396.09C375.75 442.22 366.08 489.81 361.56 537.97C361.41 539.48 356.5 534.16 356.71 532.55H356.7Z" fill="#0E1016"/>
|
||||
<path d="M357.9 576.12C357.13 587.42 355.17 614.17 354.42 624.96C353.19 641.28 352.76 657.56 351.5 673.92C348.47 717.63 340.21 761.15 323.69 801.86C311.41 832.41 297 862.29 276.84 888.56C250.47 922.35 218.67 954.52 192.68 988.46C179.49 1005.59 167.12 1023.37 157.58 1042.86C157.34 1043.36 156.74 1043.57 156.24 1043.32C155.78 1043.1 155.57 1042.57 155.73 1042.09C159.3 1031.7 164.1 1021.74 169.53 1012.18C180.3 993.18 193.72 975.42 207.62 958.6C228.31 933.37 251.12 909.86 271.51 884.45C291.64 858.98 306.51 829.84 318.76 799.87C331.16 769.82 339.19 738.15 343.26 705.91C347.6 673.74 347.9 640.71 350.26 608.31L352.29 575.67C352.39 574.12 353.77 575.84 355.32 575.93C356.9 576.03 358.04 574.53 357.89 576.11L357.9 576.12Z" fill="#0E1016"/>
|
||||
<path d="M339.82 568.03C337.92 582.56 336.3 597.12 335.47 611.75C333.86 645.84 330.29 682.43 310.15 711.33C300.13 725.21 285.11 734.69 276.77 749.77C270.82 760.07 266.36 771.51 263.84 783.11L260.35 782.09C264.05 774.45 268.15 767.18 273.69 760.69C278.01 755.85 283.27 751.53 289.61 749.57C297.07 747.25 304.47 749.38 307.82 756.85C311.76 765.15 310.48 774.14 308.65 782.69C298.76 827.7 272.69 867.15 244.12 902.49C239.75 907.83 234.63 913.94 230.09 919.11C218.26 933.01 201.31 940.62 185.01 947.56C170.15 953.67 154.36 959.11 142.44 970.19C136.56 975.51 131.69 981.96 127.38 988.8C123.18 995.53 119.4 1002.81 117.19 1010.4L113.3 1008.24C119.55 1002.37 125.99 996.92 132.98 991.97C136.57 989.49 140.23 987.08 144.34 985.45C153.14 981.66 165.56 982.72 169.33 992.95C172.14 1001.86 169.63 1011.12 167.11 1019.69C165.74 1023.9 164.22 1027.94 161.86 1032.14C160.68 1034.28 157.97 1035.06 155.83 1033.86C153.47 1032.56 152.84 1029.35 154.53 1027.26C156.82 1024.3 158.7 1020.64 160.25 1017.13C163.04 1010.34 168.08 996.55 161.52 990.8C157.23 987.3 151.16 987.89 145.99 989.76C142.23 991.12 138.82 993.37 135.51 995.62C126.45 1001.75 118.21 1009.65 110.64 1017.52C111.71 1013.27 113.1 1007.02 114.65 1003.15C120.07 989.35 127.9 976.52 138.73 966.18C148.01 957.42 159.65 951.6 171.35 946.9C189.72 939.09 209.74 932.02 224.11 917.84C229.64 911.83 235.02 905.61 240.19 899.29C265.8 867.67 288.77 832.64 300.46 793.41C302.58 785.7 304.66 777.85 305.2 769.86C305.3 763.92 304.48 756.21 298.66 753.51C294.28 752.07 289.65 753.3 285.55 755.73C275.2 761.95 268.89 772.95 263.69 783.62L258.52 796.1L260.2 782.6C262.41 767.71 267.78 753.23 276.08 740.66C284.38 728.44 297.01 719.35 305.81 708.07C326.83 680.77 330.02 644.88 331.56 611.55C332.39 596.83 333.8 582.15 335.51 567.52C335.65 566.33 336.76 567.23 337.95 567.37C339.15 567.51 339.98 566.85 339.82 568.05V568.03Z" fill="#0E1016"/>
|
||||
<path d="M334.31 490.92C341.11 485.9 348.18 487.71 348.85 496.77C349.42 502.08 348.12 508.92 347.67 514.14C347.27 517.1 342.91 516.63 343.16 513.65C343.74 508.87 345.35 501.75 345.08 497.09C344.9 490.31 340.7 490.36 335.75 493.21C334.26 494.03 332.94 491.91 334.32 490.93L334.31 490.92Z" fill="#0E1016"/>
|
||||
<path d="M497.03 182.91C489.52 187.07 481.24 189.2 473.29 192.22C449.52 201.24 429.57 215.49 416.36 237.5L414.02 235.46C417.91 232.41 421.87 229.54 425.97 226.8C432.13 222.72 438.67 218.73 445.87 216.54C451.99 214.62 462.39 212.86 464.4 221.19C465.77 228.1 460.66 234.48 457.54 239.38L449.63 251.65C444.41 259.87 439.17 268.09 434.09 276.37C431.6 280.58 428.74 284.9 425.34 288.52C420.28 294.07 414.11 298.48 407.7 302.31C401.65 305.81 395 309.81 389.68 314.29C382.28 320.35 375.93 327.64 370.91 335.79C365.95 343.91 361.58 352.74 358.81 361.8L355.92 360.4C358.58 356.87 361.37 353.56 364.34 350.33C370.4 343.86 377.17 337.68 385.9 334.88C389.37 333.87 393.96 333.32 396.98 336.39C399.63 339.69 398.38 344.05 397.6 347.47C395.84 353.82 393.24 359.83 390.95 365.93C383.53 385.29 376.16 404.71 370.38 424.61C368.35 429.74 364.5 433.71 361.02 437.79C355.18 444.28 349.01 450.37 342.8 456.38C336.66 462.19 330.78 468.4 326.72 475.83C326.27 476.67 325.22 476.98 324.39 476.52C323.57 476.07 323.26 475.05 323.69 474.23C325.74 470.26 328.28 466.59 331.05 463.16C339.46 453.3 349.4 444.74 357.82 434.96C360.93 431.21 364.58 427.48 366.36 422.96C369.69 411.37 373.67 399.94 377.86 388.67C380.88 380.56 384.02 372.5 387.2 364.47C388.78 360.45 390.43 356.44 391.93 352.46C393.24 348.78 394.92 344.53 394.83 340.73C394.75 336.85 389.75 337.49 386.9 338.28C372.67 343.02 362.3 357.57 353.44 369.06C358.8 347.19 369.96 326.08 387.42 311.56C394.73 305.19 403.88 300.68 411.81 295.22C419.76 289.8 426.05 282.71 430.71 274.32C435.77 265.96 440.92 257.68 446.15 249.44L454.05 237.11C456.42 233.44 459.57 229.23 460.31 224.95C462.2 214.6 445.31 220.26 440.33 222.58C429.35 227.74 419.13 235.51 409.48 242.88L413.63 235.91C422.45 221.33 434.43 208.58 449.05 199.78C459.81 193.02 471.52 187.92 483.49 183.76C487.24 182.27 491.05 180.61 494.49 178.57C496.06 177.68 495.51 179.49 496.05 180.75C496.48 181.76 498.35 182.07 497.03 182.91V182.91Z" fill="#0E1016"/>
|
||||
<path d="M467.53 361.95C467.15 361.95 466.78 361.78 466.54 361.46C466.12 360.91 466.22 360.13 466.77 359.71C475.67 352.88 491.73 344.04 517.7 340.65C530.58 338.97 541.95 339.86 550.62 343.22C558.56 346.3 563.91 351.37 565.7 357.48C565.89 358.14 565.51 358.84 564.85 359.03C564.19 359.22 563.49 358.84 563.3 358.18C560.37 348.13 545.24 339.58 518.03 343.12C492.61 346.44 476.95 355.05 468.3 361.69C468.07 361.86 467.81 361.95 467.54 361.95H467.53Z" fill="#0E1016"/>
|
||||
<path d="M455.46 446.85C455.08 446.85 454.71 446.68 454.47 446.36C454.05 445.81 454.16 445.03 454.7 444.61C454.91 444.45 476.47 428.15 505.18 421.86C533.01 415.76 555.16 420.33 559.03 432.97C559.23 433.63 558.86 434.33 558.2 434.53C557.54 434.73 556.84 434.36 556.64 433.7C553.71 424.13 535.27 417.82 505.72 424.3C477.55 430.47 456.43 446.44 456.22 446.6C455.99 446.77 455.73 446.86 455.46 446.86V446.85Z" fill="#0E1016"/>
|
||||
<path d="M461.32 546.49C460.99 546.49 460.66 546.36 460.42 546.11C459.94 545.61 459.96 544.82 460.45 544.34C461.1 543.71 476.85 528.84 510.51 521.95C546.14 514.66 568.74 519.19 575.33 526.92C575.78 527.44 575.71 528.23 575.19 528.68C574.66 529.13 573.87 529.06 573.43 528.54C567.51 521.6 545.46 517.35 511.01 524.4C478.05 531.15 462.34 545.99 462.18 546.14C461.94 546.37 461.63 546.49 461.31 546.49H461.32Z" fill="#0E1016"/>
|
||||
<path d="M491.16 665.06C490.64 665.06 490.15 664.73 489.97 664.2C489.78 663.63 490.04 663.14 490.13 662.98C491.69 660.07 507.85 643.09 546.93 632.56C589.6 621.06 611.66 621.97 617.97 624.33C618.62 624.57 618.95 625.29 618.7 625.94C618.46 626.59 617.73 626.92 617.09 626.67C611.47 624.57 589.72 623.61 547.58 634.97C509.65 645.2 493.77 661.6 492.37 664.08C492.28 664.49 491.98 664.84 491.55 664.99C491.42 665.03 491.29 665.05 491.16 665.05V665.06Z" fill="#0E1016"/>
|
||||
<path d="M529.56 796.56C529.05 796.56 528.57 796.24 528.38 795.73C528.15 795.08 528.49 794.36 529.13 794.13C529.34 794.05 550.45 786.51 566.36 782.83C567.04 782.67 567.7 783.09 567.86 783.77C568.02 784.44 567.6 785.11 566.92 785.27C551.15 788.92 530.18 796.41 529.97 796.49C529.83 796.54 529.69 796.56 529.55 796.56H529.56Z" fill="#0E1016"/>
|
||||
<path d="M701.99 887.03C707.21 911.57 710.87 936.49 711.32 961.61C711.35 986.74 708.48 1011.96 700.97 1035.98C700.8 1036.51 700.24 1036.8 699.71 1036.63C699.21 1036.47 698.93 1035.95 699.04 1035.45C700.39 1029.39 701.49 1023.29 702.44 1017.16C706.21 992.69 708.11 967.84 705.79 943.15C704.26 924.62 701.48 906.13 698.02 887.88C697.78 886.61 699.11 887.99 700.08 887.77C701.05 887.55 701.65 885.72 701.98 887.03H701.99Z" fill="#0E1016"/>
|
||||
<path d="M598.21 954.87C597.65 954.87 597.14 954.49 597 953.93C596.83 953.26 597.23 952.58 597.9 952.41C598.57 952.24 664.69 935.45 705.99 935.45C706.52 935.45 707.04 935.45 707.56 935.45C708.25 935.45 708.8 936.02 708.8 936.71C708.8 937.4 708.23 937.95 707.55 937.95H707.54C707.03 937.95 706.51 937.95 705.99 937.95C665.01 937.95 599.2 954.66 598.53 954.83C598.43 954.86 598.32 954.87 598.22 954.87H598.21Z" fill="#0E1016"/>
|
||||
<path d="M667.69 249.39C674.37 244.19 677.29 234.94 676.75 226.67C676.7 226.12 677.11 225.64 677.66 225.59C678.21 225.54 678.69 225.95 678.74 226.5C678.78 227.72 678.78 228.86 678.73 230.02C678.55 237.61 676.52 245.3 672.09 251.57C671.52 252.41 671.31 252.68 670.52 253.41C669.45 254.45 670.27 253.3 669.24 252.23C667.68 250.72 665.55 249.59 667.68 249.37L667.69 249.39Z" fill="#0E1016"/>
|
||||
<path d="M671.67 236.19C671.67 236.19 668.92 241.33 666.61 244.87C665.75 246.2 664.68 246.3 664.36 247.08C663.83 248.39 664.79 248.83 664.79 248.83C664.79 248.83 667.49 252.78 668.64 251.83C671.85 249.18 672.56 244.63 672.59 242.17C672.63 238.97 671.66 236.19 671.66 236.19H671.67Z" fill="#0E1016"/>
|
||||
<path d="M680.33 262.88C677.33 260.43 675.97 258.81 676.8 255.71C678.78 248.31 681.04 238.92 678.29 232.12L676.86 227.94C676.44 226.73 678.24 226.03 678.73 227.24L680.36 231.41C684.2 239.79 682 249.42 681.38 258.16C681.38 259.35 681.7 260.34 682.71 261.14C684.54 262.6 682.1 264.43 680.32 262.89L680.33 262.88Z" fill="#0E1016"/>
|
||||
<path d="M277.7 520.62C271.91 553.39 267.14 586.36 264.03 619.5L261.22 618.8C269.87 598.75 278.88 578.87 288.9 559.46C292.28 553 295.74 546.57 299.64 540.35C300.83 538.51 303.66 540.24 302.48 542.15C287.42 566.86 275.66 593.54 263.91 619.96L260.37 628.12C263.09 591.88 267.56 555.76 273.42 519.91C273.94 517.12 278.11 517.76 277.69 520.64L277.7 520.62Z" fill="#0E1016"/>
|
||||
<path d="M423.65 225.06C408.45 218.8 396.44 224.8 385.17 235.38C374.49 245.46 365.94 257.58 358.22 270.06C332.96 312.3 314.61 358.61 302.05 406.15C301.77 407.21 300.68 407.84 299.63 407.56C298.57 407.28 297.94 406.19 298.22 405.14C303.53 386.06 309.41 367.09 316.7 348.61C331.73 311.03 360.29 245.36 395.04 223.86C403.97 218.38 415.37 217.75 425.04 221.76C427.21 222.73 425.87 225.95 423.65 225.06V225.06Z" fill="#0E1016"/>
|
||||
<path d="M447.79 249.51C442.87 241.24 436.94 233.09 428.81 227.86C426.82 226.55 428.73 223.53 430.77 224.79C435.1 227.74 438.75 231.47 441.91 235.47C445.05 239.47 447.73 243.77 450.15 248.17C450.51 248.83 450.27 249.65 449.62 250.01C448.97 250.37 448.16 250.14 447.79 249.51Z" fill="#0E1016"/>
|
||||
<path d="M316.48 391.19C324.31 365.58 333.05 340.23 343.6 315.59C350.72 299.02 358.38 282.51 370.16 268.61C382.95 253.18 405.55 236.67 425.31 250.32C431.14 254.29 435.78 259.92 439.21 265.94C439.76 266.9 439.43 268.12 438.47 268.67C437.56 269.19 436.42 268.92 435.84 268.08C433.34 264.55 430.55 261.27 427.44 258.45C408.8 241.16 387.7 256.15 374.17 272C362.72 285.4 354.86 301.33 347.46 317.3C336.59 340.64 326.18 367.18 316.49 391.18L316.48 391.19Z" fill="#0E1016"/>
|
||||
<path d="M708.68 958.41C728.49 955.79 747.63 946.21 760.45 930.71C762.74 927.83 764.89 924.75 766.76 921.6C767.45 920.33 768.35 921.15 769.59 921.92C770.88 922.71 771.91 923.22 770.95 924.39C768.97 926.81 767.11 929.3 765.01 931.6C758.88 938.56 751.86 944.86 743.94 949.73C733.39 956.27 721.55 960.49 709.36 962.72C706.47 963.17 705.75 958.89 708.68 958.42V958.41Z" fill="#0E1016"/>
|
||||
<path d="M947.59 525.89C950.71 522.68 950.52 517.62 947.47 514.42C946.9 513.77 944.89 511.72 944.23 511.06C924.69 491.83 906.12 471.54 889.27 449.89C871.17 426.64 853.77 401.93 843.59 374.07C842.14 370.05 840.84 366.04 839.94 361.76H842.32C835.71 401.81 830.93 442.32 828.77 482.88C828.42 488.68 828.43 495.09 828.66 500.07C828.75 502.13 824.79 502.34 824.81 500.19C826.15 453.58 831.83 407.2 839.93 361.31L841.01 355.35C841.01 355.36 842.31 361.3 842.31 361.31C846.56 377.46 854.23 392.56 862.56 406.97C875.44 428.5 891.19 448.62 907.23 467.86C919.4 482.33 932.37 496.18 945.74 509.56L947.95 511.85L949.05 513.05C952.75 517 952.92 523.41 949 527.3C948.03 528.23 946.65 526.85 947.58 525.89H947.59Z" fill="#0E1016"/>
|
||||
<path d="M590.49 817.26C587.92 833.69 585.16 851.23 582.14 867.62L582.08 867.96C582.06 868.07 582.09 867.93 582.08 867.89C582.06 867.71 581.92 867.56 581.74 867.54L581.9 867.56C583.07 867.68 593.49 868.88 594.92 869.04C609.18 870.71 688.57 879.74 704.54 881.56C705.22 881.64 705.71 882.25 705.63 882.93C705.55 883.61 704.94 884.1 704.26 884.02L616.55 874.1C609.26 873.29 590.7 871.21 583.66 870.41C583.15 870.35 581.94 870.22 581.42 870.15C580.16 870.03 579.22 868.68 579.53 867.47C582.81 851.01 586.76 833.6 590.5 817.26H590.49Z" fill="#0E1016"/>
|
||||
<path d="M626.09 879.21C624.59 879.01 622.81 877.17 622.49 879.14C619.12 903.19 612.26 927.31 598.86 947.73C587.03 965.38 571.95 980.85 554.88 993.52C555.24 992.86 555.61 992.19 555.96 991.52C561.44 980.9 566.22 969.66 567.85 957.73C566.55 960.4 565.23 963.07 563.9 965.74C561.03 971.5 557.18 976.72 552.47 981.1C546.56 986.62 540.2 991.71 533.67 996.4C504.48 1017.6 468.84 1029.1 432.74 1029.47C432.22 1029.47 431.79 1029.88 431.75 1030.4C431.71 1030.95 432.13 1031.43 432.68 1031.47C444.9 1032.32 457.26 1031.79 469.47 1029.92C499.06 1025.03 528.27 1011.95 550.67 991.62C548.05 996.62 545.41 1001.6 542.75 1006.51C555.65 998.7 568.07 989.94 579.01 979.53C588.05 970.75 596.44 961.31 603.55 950.84C617.66 929.74 625.14 904.93 628.58 880C628.81 877.95 627.58 879.4 626.08 879.2L626.09 879.21Z" fill="#0E1016"/>
|
||||
<path d="M683.19 613.34L695.73 618.04L693.12 631.17C690.9 638.76 682.94 643.12 675.35 640.9C667.76 638.68 663.4 630.72 665.62 623.13C667.82 615.61 675.65 611.25 683.2 613.35L683.19 613.34ZM696.76 612.86L684.82 608.38L684.72 608.35C674.34 605.39 663.54 611.41 660.58 621.79C657.62 632.17 663.64 642.97 674.02 645.93C684.39 648.88 695.2 642.87 698.16 632.5L698.19 632.4L700.68 619.9L712.62 624.38L712.72 624.41C723.1 627.37 733.9 621.35 736.86 610.97C739.82 600.59 733.8 589.79 723.42 586.83C713.05 583.88 702.24 589.89 699.28 600.26L699.25 600.36L696.76 612.86ZM701.7 614.71L704.31 601.58C706.53 593.99 714.49 589.63 722.08 591.85C729.67 594.07 734.03 602.03 731.81 609.62C729.61 617.14 721.78 621.5 714.23 619.4L701.7 614.7V614.71Z" fill="black"/>
|
||||
<path d="M823.98 622.99L828.19 610.28L841.41 612.39C849.08 614.32 853.74 622.11 851.81 629.78C849.88 637.45 842.09 642.11 834.42 640.18C826.82 638.27 822.17 630.61 823.97 622.98L823.98 622.99ZM822.99 609.46L818.97 621.56L818.94 621.66C816.38 632.14 822.81 642.71 833.29 645.27C843.77 647.83 854.34 641.4 856.9 630.92C859.46 620.44 853.04 609.87 842.56 607.31L842.46 607.29L829.87 605.28L833.89 593.18L833.92 593.08C836.48 582.6 830.05 572.03 819.57 569.47C809.09 566.91 798.52 573.34 795.96 583.82C793.4 594.3 799.82 604.87 810.3 607.43L810.4 607.45L822.99 609.46ZM824.65 604.45L811.43 602.34C803.76 600.41 799.1 592.62 801.03 584.95C802.96 577.28 810.75 572.62 818.42 574.55C826.02 576.46 830.67 584.12 828.87 591.75L824.65 604.45V604.45Z" fill="black"/>
|
||||
<path d="M650.09 767.54L663.41 766.19L666.89 779.12C668.27 786.91 663.06 794.34 655.27 795.72C647.48 797.1 640.05 791.89 638.67 784.1C637.31 776.38 642.4 769.01 650.09 767.54V767.54ZM662.04 761.1L649.36 762.38L649.25 762.4C638.64 764.35 631.62 774.54 633.57 785.15C635.52 795.76 645.71 802.78 656.32 800.83C666.93 798.88 673.95 788.7 672 778.09L671.98 777.99L668.67 765.68L681.35 764.4L681.46 764.38C692.07 762.43 699.09 752.24 697.14 741.63C695.19 731.02 685 724 674.39 725.95C663.78 727.9 656.76 738.08 658.71 748.69L658.73 748.79L662.04 761.1ZM667.29 760.57L663.81 747.65C662.43 739.86 667.64 732.43 675.43 731.05C683.22 729.67 690.65 734.88 692.03 742.67C693.39 750.39 688.3 757.76 680.61 759.23L667.29 760.57V760.57Z" fill="black"/>
|
||||
<path d="M774.66 828.41L777.35 815.29L790.73 815.83C798.58 816.84 804.12 824.03 803.1 831.88C802.09 839.73 794.9 845.27 787.05 844.25C779.28 843.25 773.76 836.19 774.66 828.4V828.41ZM772.08 815.09L769.51 827.58L769.49 827.69C768.18 838.4 775.8 848.14 786.51 849.45C797.22 850.76 806.96 843.14 808.27 832.43C809.58 821.72 801.96 811.98 791.25 810.67H791.14L778.4 810.15L780.97 797.66L780.99 797.55C782.3 786.84 774.68 777.1 763.97 775.79C753.26 774.48 743.52 782.1 742.21 792.81C740.9 803.52 748.52 813.26 759.23 814.57H759.34L772.08 815.09ZM773.14 809.92L759.77 809.38C751.92 808.37 746.38 801.18 747.4 793.33C748.41 785.48 755.6 779.94 763.45 780.96C771.22 781.96 776.74 789.02 775.84 796.81L773.14 809.92V809.92Z" fill="black"/>
|
||||
<path d="M787.92 711.27L784.58 698.3L796.84 692.92C804.34 690.39 812.47 694.41 815 701.91C817.53 709.41 813.51 717.54 806.01 720.07C798.59 722.58 790.53 718.66 787.92 711.27V711.27ZM779.75 700.43L782.92 712.78L782.95 712.88C786.48 723.08 797.6 728.49 807.8 724.96C818 721.43 823.41 710.31 819.88 700.11C816.35 689.91 805.23 684.51 795.04 688.03L794.94 688.07L783.27 693.2L780.1 680.85L780.07 680.75C776.54 670.55 765.42 665.14 755.22 668.67C745.02 672.2 739.61 683.32 743.14 693.52C746.67 703.72 757.79 709.12 767.98 705.6L768.08 705.56L779.75 700.43ZM778.44 695.32L766.19 700.7C758.69 703.23 750.56 699.21 748.03 691.71C745.5 684.21 749.52 676.08 757.02 673.55C764.44 671.04 772.5 674.96 775.11 682.35L778.44 695.31V695.32Z" fill="black"/>
|
||||
<path d="M558.54 226.9C562.88 225.64 569.37 223.9 571.83 222.58C573.97 221.43 571.97 219.48 566.75 218.28C561.53 217.08 570.02 217.88 577.15 219.92C584.28 221.96 585.53 223.77 585.53 223.77L562.32 227.79L558.54 226.9V226.9Z" fill="#0E1016"/>
|
||||
<path d="M624.42 238.21C624.42 238.21 628.55 244.98 630.13 262.5C630.91 271.15 627.12 256.56 625.63 251.91C624.14 247.26 620.04 237.59 620.04 237.59L624.41 238.21H624.42Z" fill="#0E1016"/>
|
||||
<path d="M653.73 251C659.19 250.57 662.92 248.14 671.49 255.81C680.06 263.48 666.19 247.79 664.36 247.08C662.53 246.37 655.93 248.9 655.05 249.17C654.17 249.44 653.72 251 653.72 251H653.73Z" fill="#0E1016"/>
|
||||
<path d="M462.61 534.24C463.12 536.37 465.48 539.98 469.78 537.46C474.08 534.94 467.37 540.41 465.46 541.76C463.55 543.11 461.62 544.4 461.62 544.4L462.61 534.24V534.24Z" fill="#0E1016"/>
|
||||
<path d="M465.26 553.07C464.81 550.23 464.25 543.93 470.74 539.87C477.23 535.81 468.41 540.42 465.46 541.77C462.51 543.12 461.62 544.41 461.62 544.41L463.43 550.58L465.26 553.07V553.07Z" fill="#0E1016"/>
|
||||
<path d="M458.38 451.18C458.41 448.94 459.16 444.22 464.27 441.26C469.38 438.3 462.75 441.55 460.32 442.59C457.89 443.63 456.52 444.75 456.52 444.75L457.16 450.95L458.38 451.19V451.18Z" fill="#0E1016"/>
|
||||
<path d="M458.88 433.86C458.76 436.22 459.6 441.02 464.05 438.48C468.5 435.94 462.79 441.37 459.49 442.96C456.19 444.55 455.03 445.11 455.03 445.11L457.55 436.43L458.88 433.86Z" fill="#0E1016"/>
|
||||
<path d="M469.45 366.82C470.21 363.93 471.59 358.71 477.34 355.77C483.09 352.83 469.52 359.16 469.09 359.48C468.66 359.8 468.5 364.4 468.5 364.4L469.45 366.82Z" fill="#0E1016"/>
|
||||
<path d="M474.85 348.11C474.03 350.71 473.83 354.62 478.17 352.48C482.51 350.35 476.36 355.04 473.64 356.8C470.92 358.56 469.53 358.46 469.53 358.46L472.47 351.77L474.86 348.11H474.85Z" fill="#0E1016"/>
|
||||
<path d="M490.49 653.53C491.04 655.23 493.71 658.29 498.56 654.81C503.42 651.33 491.76 662.01 491.55 662.22C491.34 662.43 490.49 653.53 490.49 653.53V653.53Z" fill="#0E1016"/>
|
||||
<path d="M495.93 669.92C494.98 667.1 493.09 661.87 499.18 657.49C505.27 653.11 495.03 659.89 492.93 660.95C490.83 662.01 490.71 663.81 490.94 664.44C491.17 665.07 495.94 669.92 495.94 669.92H495.93Z" fill="#0E1016"/>
|
||||
<path d="M530.6 787.37C531.01 789.29 532.79 792.81 537.64 791.22C542.49 789.63 532.42 794.22 530.85 794.72C529.28 795.23 529.89 791.04 529.89 791.04L530.6 787.36V787.37Z" fill="#0E1016"/>
|
||||
<path d="M533.56 801.92C532.98 798.93 533.94 795.06 538.56 793.55C543.18 792.04 532.28 794.64 530.9 795.28C529.52 795.92 530.16 799.06 530.16 799.06L533.56 801.92Z" fill="#0E1016"/>
|
||||
<path d="M462.77 230.77C467.46 222.37 470.13 218.32 468.48 214.66C466.56 210.41 456.16 213.52 445.87 216.54C435.58 219.56 452.63 215.94 456.84 216.87C461.05 217.8 463.27 219.17 463.27 222.98C463.27 226.79 462.78 230.76 462.78 230.76L462.77 230.77Z" fill="#0E1016"/>
|
||||
<path d="M377.98 338.57C386.82 332.9 394.36 329.15 398.09 330.12C402.51 331.26 399.79 340.64 397.58 347.48C395.37 354.32 395.88 346.6 396.45 344.03C397.02 341.46 397.09 336.82 393.81 336.04C390.53 335.26 383.32 337.47 383.32 337.47L377.97 338.58L377.98 338.57Z" fill="#0E1016"/>
|
||||
<path d="M332.23 490.25C334.67 486.11 340.65 478.95 345.39 479.53C350.59 480.17 350.59 486.67 348.85 503.23C347.16 519.25 346.95 503.94 346.87 501.09C346.79 498.24 346.71 491.11 343.31 490.08C339.91 489.05 336.03 490.79 336.03 490.79L332.24 490.25H332.23Z" fill="#0E1016"/>
|
||||
<path d="M355.13 528.76C356.27 530.26 357.54 531.93 358.77 533.56C359.46 526.67 360.16 520.63 360.87 516.15C364.14 495.35 369.2 459.83 369.2 459.83C364.3 479.9 359.33 501.48 355.13 528.77V528.76Z" fill="#0E1016"/>
|
||||
<path d="M316.69 805.25C312.98 813.82 310.38 819.27 310.38 819.27C310.38 819.27 313.02 814.18 316.69 805.25Z" fill="#0E1016"/>
|
||||
<path d="M349.64 573.09C348.49 585.05 347.48 597.98 346.66 612.06C342.39 685.38 342.89 693.99 335.26 740.04C330.61 768.09 322.4 791.34 316.69 805.25C324.42 787.42 337.02 756.06 341.25 732.14C347.52 696.72 351.06 625.6 352.69 608.43C353.33 601.7 354.22 589.24 355.27 575.33L349.63 573.08L349.64 573.09Z" fill="#0E1016"/>
|
||||
<path d="M629.15 238C623.94 240.21 617.4 240.51 610.49 238.46C603.58 236.41 592.48 230.33 583.43 225.39C574.39 220.45 572.36 217.19 577.93 220.15C583.5 223.11 598.47 228.58 605.69 231.93C612.91 235.27 617.27 236.41 621.74 236.87C626.21 237.33 629.14 238.01 629.14 238.01L629.15 238Z" fill="#0E1016"/>
|
||||
<path d="M661.27 300.63C662.74 299.22 664.31 296.25 661.83 292.65C659.35 289.05 666.12 295.42 666.98 296.83C667.84 298.24 663.56 301.4 663.56 301.4L661.28 300.63H661.27Z" fill="#0E1016"/>
|
||||
<path d="M662.35 288.2C664.24 290.11 666.92 292.73 669.7 293.53C672.48 294.33 674.51 294.43 676.44 294.48C678.37 294.53 671.41 297.3 669.16 297.59C666.91 297.88 663.92 292.03 663.92 292.03L662.35 288.2Z" fill="#0E1016"/>
|
||||
<path d="M538.18 306.71C549.02 314.97 556.61 317.04 564.21 318.72C571.8 320.4 578.81 320.59 578.81 320.59L584.33 310.83C584.33 310.83 567.26 314.87 555.53 311.42C543.8 307.97 538.19 306.71 538.19 306.71H538.18Z" fill="#0E1016"/>
|
||||
<path d="M489.73 186.33C491.84 185.45 493.42 185.49 494.86 186.33C496.3 187.17 497.26 188.4 495.5 190.7C493.74 192.99 499.13 189.49 500.57 187.05C502.01 184.6 500.07 182.59 500.07 182.59L494.11 181.93L489.73 186.33Z" fill="#0E1016"/>
|
||||
<path d="M597.96 162.84C600.84 164.24 603.13 164.22 604.65 163.81C606.17 163.4 606.71 162.07 606.04 159.79C605.36 157.51 608.78 162.68 609.55 166.43C610.32 170.19 609.48 171.4 609.55 171.19C609.62 170.98 597.96 162.84 597.96 162.84V162.84Z" fill="#0E1016"/>
|
||||
<path d="M198.66 292.78C207.5 284.7 216.43 279.9 225.64 277.76C234.85 275.62 242.29 277.26 247.71 284.45C253.13 291.64 257.08 299.75 260.29 314.33C263.49 328.91 258.18 290.76 256.04 281.05C253.9 271.34 251.88 265.41 245.19 263.39C238.5 261.37 236.99 260.74 226.14 267.8C215.29 274.86 211.76 277.01 206.34 282.18C200.92 287.35 198.66 292.79 198.66 292.79V292.78Z" fill="#0E1016"/>
|
||||
<path d="M358.29 295.48C365.2 282.88 371.51 274.5 382.77 266.79C394.03 259.08 407.34 259.76 415.29 262.21C423.24 264.66 432.66 272.61 432.66 272.61L437.19 267.23C437.19 267.23 431.19 256.34 419.57 251.94C407.95 247.54 402.07 247.54 387.64 257.45C373.21 267.36 368.31 274.58 365.25 279.72C362.19 284.86 358.29 295.5 358.29 295.5V295.48Z" fill="#0E1016"/>
|
||||
<path d="M277.18 523.58C277.18 523.58 284.25 532.15 288.84 536.33C293.43 540.51 298.59 544.93 298.59 544.93L300.72 542.14L277.32 522.78L277.18 523.57V523.58Z" fill="#0E1016"/>
|
||||
<path d="M283.58 752.27C291.67 747.3 297.07 745.28 302.75 745.1C308.43 744.92 313.55 749.04 313.46 757.27C313.37 765.51 307.92 785.86 307.92 785.86L307.05 781.34C307.05 781.34 309.25 768.8 305.59 760.65C301.93 752.5 302.66 751.86 296.16 751.5C289.66 751.14 283.58 752.26 283.58 752.26V752.27Z" fill="#0E1016"/>
|
||||
<path d="M138.58 988.32C151.33 980.28 158.34 976.67 167.23 978.29C178 980.25 177.51 996.2 168.68 1014.08C159.85 1031.96 166.95 1011.66 167.51 1006.4C168.08 1001.15 168.36 992.91 162.68 988.94C157 984.96 150.47 984.68 146.35 986.53C142.23 988.38 138.57 988.33 138.57 988.33L138.58 988.32Z" fill="#0E1016"/>
|
||||
<path d="M541.13 996.16C541.13 996.16 535.65 1019.15 530.19 1031.87C524.73 1044.6 520.63 1036.67 520.63 1036.67C520.63 1036.67 528.89 1016.72 533.19 1002.31C535.75 993.72 541.14 996.16 541.14 996.16H541.13Z" fill="#0E1016"/>
|
||||
<path d="M161.88 411.02C163.1 409.03 167.1 408.36 165.1 413.5C163.09 418.65 166.36 411.56 169.89 403.85C173.42 396.15 176.72 386.1 176.72 386.1L161.88 411.01V411.02Z" fill="#0E1016"/>
|
||||
<path d="M216.07 383.78C217.18 380.12 221.2 377.18 221.02 383.78C220.84 390.38 222.84 376.66 223.01 368.8C223.18 360.95 223.01 356.37 223.01 356.37L216.06 383.78H216.07Z" fill="#0E1016"/>
|
||||
<path d="M958.73 504.33C958.73 504.33 960.72 514.28 960.67 518.23C960.62 522.18 960.32 516.56 959 512.67C957.68 508.78 955.65 505.67 955.65 505.67L958.73 504.33Z" fill="#0E1016"/>
|
||||
<path d="M380.03 704.74C399.67 716.2 417.35 732.33 426.95 753.29C434.12 768.64 438.08 785.47 439.99 802.29L434.58 794.34C429.59 787.06 424.45 779.79 418.89 772.93C415.32 768.5 411.4 764.36 406.27 762.01C398.1 758.07 390.72 762.34 393.49 771.83C394.93 777.34 398.45 782.14 401.4 787.03C404.55 792.1 407.55 797.37 410.17 802.78C416.44 815.33 420.75 828.99 421.03 843.13C417.32 838.16 413.36 832.88 409.48 828.07C403.97 821.59 398.54 814.17 390.28 811.24C387.33 810.24 383.91 809.83 381.04 811.24C380.07 811.72 379.23 812.48 378.54 813.31C380.63 808.88 386.51 808.18 390.84 809.25C399.99 811.46 406.33 819.05 412.2 825.81C415.1 829.25 417.81 832.79 420.36 836.52L417.25 837.6C416.71 831.93 415.24 826.12 413.47 820.65C409.76 809.56 404.18 799.09 397.9 789.19C393.89 782.66 388.91 776.01 388.7 767.95C388.57 764.43 390.15 760.49 393.18 758.44C403.69 751.51 416.09 762.08 422.29 770.1C428 777.08 433.2 784.36 438.32 791.77L434.22 793.31C433.66 788.99 432.79 784.54 431.81 780.2C423.98 745.09 408.77 725.65 380.04 704.69L380.03 704.74Z" fill="#0E1016"/>
|
||||
<path d="M325.97 926.16C331.13 946.61 330.13 965.27 318.92 983.56C317.99 978.1 316.91 972.19 315.46 966.87C314.48 963.36 313.37 959.48 311.46 956.47C308.42 951.82 303.3 951.44 300.78 956.81C299.94 958.44 299.48 960.27 298.96 962.04C294.77 977 289.6 993.59 276.79 1003.55C284.27 994.29 288.7 983.53 292.13 972.33C293.58 967.66 294.77 962.88 296.2 958.14C296.53 957.23 296.93 955.96 297.41 955.11C299.53 950.55 304.37 947.51 309.45 949.79C313.76 951.64 315.95 956.04 317.4 960.06C319.44 965.78 320.72 971.63 321.74 977.6L318.18 976.99C326.54 960.8 327.68 944.05 325.96 926.17L325.97 926.16Z" fill="#0E1016"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_121_754" x1="656.82" y1="655.74" x2="908.24" y2="717.18" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#78E382"/>
|
||||
<stop offset="1" stop-color="#3685DB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_121_754" x1="884.03" y1="340.97" x2="946.58" y2="486.33" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#78E382"/>
|
||||
<stop offset="1" stop-color="#3685DB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 101 KiB |
28
frontend/public/locales/en/mfa.json
Normal file
28
frontend/public/locales/en/mfa.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"title": "Sign Up",
|
||||
"og-title": "Replace .env files with 1 line of code. Sign Up for Infisical in 3 minutes.",
|
||||
"og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage API-keys and environemntal variables. Works with Node.js, Next.js, Gatsby, Nest.js...",
|
||||
"signup": "Sign Up",
|
||||
"already-have-account": "Have an account? Log in",
|
||||
"forgot-password": "Forgot your password?",
|
||||
"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.",
|
||||
"step1-submit": "Get Started",
|
||||
"step2-message": "We've sent a verification code to",
|
||||
"step2-code-error": "Oops. Your code is wrong. Tries left:",
|
||||
"step2-resend-alert": "Don't see the code?",
|
||||
"step2-resend-submit": "Resend",
|
||||
"step2-resend-progress": "Resending...",
|
||||
"step2-spam-alert": "Make sure to check your spam inbox.",
|
||||
"step3-message": "Almost there!",
|
||||
"step4-message": "Save your Emergency Kit",
|
||||
"step4-description1": "If you get locked out of your account, your Emergency Kit is the only way to sign in.",
|
||||
"step4-description2": "We recommend you download it and keep it somewhere safe.",
|
||||
"step4-description3": "It contains your Secret Key which we cannot access or recover for you if you lose it.",
|
||||
"step4-download": "Download PDF",
|
||||
"step5-send-invites": "Send Invites",
|
||||
"step5-invite-team": "Invite your team",
|
||||
"step5-subtitle": "Infisical is meant to be used with your teammates. Invite them to test it out.",
|
||||
"step5-skip": "Skip"
|
||||
}
|
||||
@@ -9,9 +9,9 @@
|
||||
"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.",
|
||||
"step1-submit": "Get Started",
|
||||
"step2-message": "We've sent a verification email to",
|
||||
"step2-message": "We've sent a verification code to",
|
||||
"step2-code-error": "Oops. Your code is wrong. Please try again.",
|
||||
"step2-resend-alert": "Don't see the email?",
|
||||
"step2-resend-alert": "Don't see the code?",
|
||||
"step2-resend-submit": "Resend",
|
||||
"step2-resend-progress": "Resending...",
|
||||
"step2-spam-alert": "Make sure to check your spam inbox.",
|
||||
|
||||
28
frontend/public/locales/fr/mfa.json
Normal file
28
frontend/public/locales/fr/mfa.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"title": "S'inscrire",
|
||||
"og-title": "Remplacez les fichiers .env par 1 ligne de code. Inscrivez-vous à Infisical en 3 minutes.",
|
||||
"og-description": "Infisical, une plate-forme simple et chiffré de bout en bout qui permet aux équipes de synchroniser et de gérer des clefs API et des variables d'environnement. Fonctionne avec Node.js, Next.js, Gatsby, Nest.js ...",
|
||||
"signup": "S'inscrire",
|
||||
"already-have-account": "Déjà inscris? Se connecter",
|
||||
"forgot-password": "Mot de passe oublié?",
|
||||
"verify": "Vérifier",
|
||||
"step1-start": "Bon, on commence!",
|
||||
"step1-privacy": "En créant votre compte, vous acceptez nos conditions et avez lu et reconnu notre politique de confidentialité.",
|
||||
"step1-submit": "C'est parti",
|
||||
"step2-message": "Nous avons envoyé un email de vérification à",
|
||||
"step2-code-error": "Oops. Votre code est faux. Essais restants:",
|
||||
"step2-resend-alert": "Vous ne voyez pas le code?",
|
||||
"step2-resend-submit": "Renvoyer",
|
||||
"step2-resend-progress": "Envoie en cours...",
|
||||
"step2-spam-alert": "Assurez-vous de vérifier vos spams.",
|
||||
"step3-message": "Nous y sommes presque!",
|
||||
"step4-message": "Enregistrez votre kit d'urgence",
|
||||
"step4-description1": "Si vous n'arrivez plus à vous connecter à votre compte, votre kit d'urgence est le seul moyen d'y arriver.",
|
||||
"step4-description2": "Nous vous recommandons de le télécharger et de le garder en sécurité.",
|
||||
"step4-description3": "Il contient votre clef secrète que nous ne pouvons pas récupérer pour vous si vous la perdez.",
|
||||
"step4-download": "Téléchargez le PDF",
|
||||
"step5-send-invites": "Envoyer les invitations",
|
||||
"step5-invite-team": "Invitez votre équipe",
|
||||
"step5-subtitle": "Infisical a pour but d'être utilisé avec vos coéquipiers. Invitez-les à le tester.",
|
||||
"step5-skip": "Passer"
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"step1-submit": "C'est parti",
|
||||
"step2-message": "Nous avons envoyé un email de vérification à",
|
||||
"step2-code-error": "Oops. Votre code est faux. Veuillez réessayer.",
|
||||
"step2-resend-alert": "Vous ne voyez pas l'email?",
|
||||
"step2-resend-alert": "Vous ne voyez pas le code?",
|
||||
"step2-resend-submit": "Renvoyer",
|
||||
"step2-resend-progress": "Envoie en cours...",
|
||||
"step2-spam-alert": "Assurez-vous de vérifier vos spams.",
|
||||
|
||||
21
frontend/public/locales/ko/mfa.json
Normal file
21
frontend/public/locales/ko/mfa.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "회원가입",
|
||||
"og-title": "한줄의 코드르 .env파일을 교체하세요. 3분이면 가입할 수 있어요.",
|
||||
"og-description": "Infisical은 팀원과 .env파일을 공유하고 연동할 수 있는 심플한 end-to-end 암호화 플렛폼입니다. Node.js, Next.js, Gatsby, Nest.js 와 같은 다양한 플렛폼에서 작동해요.",
|
||||
"signup": "회원가입",
|
||||
"already-have-account": "이미 계정이 있나요? 로그인하기",
|
||||
"forgot-password": "비밀번호를 잊으셨나요?",
|
||||
"verify": "인증",
|
||||
"step1-start": "시작하기",
|
||||
"step1-privacy": "회원가입시 약관과 개인 정보 보호 정책을 읽고 동의한 것으로 간주합니다.",
|
||||
"step1-submit": "시작하기",
|
||||
"step2-message": "<email>{{email}}</email><wrapper>로 인증 메일을 전송하였습니다</wrapper><email>{{email}}</email>",
|
||||
"step2-code-error": "코드가 잘못된 것 같아요. 남은 시도:",
|
||||
"step2-spam-alert": "스팸함에 메일이 있지는 않은지 확인하세요",
|
||||
"step3-message": "거의다 끝났어요!",
|
||||
"step4-message": "긴급복구 키트 저장하기",
|
||||
"step4-description1": "계정이 잠겼을 경우 비상 키트를 사용하여 로그인할 수 있어요.",
|
||||
"step4-description2": "다운로드 후 안전한 곳에 보관하는 것을 추천합니다.",
|
||||
"step4-description3": "분실시 접근하거나 복구할 수 없는 시크릿 키가 포함되어 있어요.",
|
||||
"step4-download": "PDF 다운로드"
|
||||
}
|
||||
21
frontend/public/locales/pt-BR/mfa.json
Normal file
21
frontend/public/locales/pt-BR/mfa.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "Inscrever-se",
|
||||
"og-title": "Substitua os arquivos .env por 1 linha de código. Cadastre-se no Infisical em 3 minutos.",
|
||||
"og-description": "Infisical é uma plataforma criptografada de ponta a ponta simples que permite que as equipes sincronizem e gerenciem chaves de API e variáveis ambientais. Funciona com Node.js, Next.js, Gatsby, Nest.js...",
|
||||
"signup": "Inscrever-se",
|
||||
"already-have-account": "Possui uma conta? Conecte-se",
|
||||
"forgot-password": "Esqueceu sua senha?",
|
||||
"verify": "Verificar",
|
||||
"step1-start": "Vamos começar",
|
||||
"step1-privacy": "Ao criar uma conta, você concorda com nossos Termos e leu e reconheceu a Política de Privacidade.",
|
||||
"step1-submit": "Iniciar",
|
||||
"step2-message": "<wrapper>Enviamos um e-mail de verificação para</wrapper><email>{{email}}</email>",
|
||||
"step2-code-error": "Ops. Seu código está errado. Tentativas restantes:",
|
||||
"step2-spam-alert": "Certifique-se de verificar sua caixa de entrada de spam.",
|
||||
"step3-message": "Quase lá!",
|
||||
"step4-message": "Guarde o seu Kit de Emergência",
|
||||
"step4-description1": "Se sua conta for bloqueada, seu Kit de emergência é a única maneira de fazer login.",
|
||||
"step4-description2": "Recomendamos que você faça o download e guarde-o em algum lugar seguro.",
|
||||
"step4-description3": "Ele contém sua chave secreta que não podemos acessar ou recuperar para você se você a perder.",
|
||||
"step4-download": "Baixar PDF"
|
||||
}
|
||||
28
frontend/public/locales/tr/mfa.json
Normal file
28
frontend/public/locales/tr/mfa.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"title": "Kayıt olun",
|
||||
"og-title": "Tek satır kodla .env dosyalarını değiştirin. 3 dakika içerinde Infisical'a kayıt olun.",
|
||||
"og-description": "Infisical takımların API anahtarlarını ve ortam değişkenlerini yönetmelerini ve senkronize etmelerini sağlayan basit, uçtan uca şifrelenmiş bir platformdur. Node.js, Next.js, Gatsby, Nest.js ve daha fazlası ile çalışır.",
|
||||
"signup": "Kayıt ol",
|
||||
"already-have-account": "Hesabın var mı? Giriş yap",
|
||||
"forgot-password": "Şifreni mi unuttun?",
|
||||
"verify": "Doğrula",
|
||||
"step1-start": "Hadi başlayalım",
|
||||
"step1-privacy": "Hesap oluşturarak, Şartlarımızı ve Gizlilik Politikasını okuyup kabul etmiş olursunuz.",
|
||||
"step1-submit": "Başla",
|
||||
"step2-message": "Şu adrese bir doğrulama maili yolladık",
|
||||
"step2-code-error": "Tüh. Kodun hatalı. Kalan denemeler:",
|
||||
"step2-resend-alert": "Kodu ulaşamadı mı?",
|
||||
"step2-resend-submit": "Tekrar Yolla",
|
||||
"step2-resend-progress": "Tekrar yollanıyor...",
|
||||
"step2-spam-alert": "Spam kutunuzu kontrol ettiğinizden emin olun.",
|
||||
"step3-message": "Çok az kaldı!",
|
||||
"step4-message": "Acil Durum Kitinizi kayıt edin",
|
||||
"step4-description1": "Eğer hesabınıza erişemezseniz, Acil Durum Kitiniz giriş yapmanın tek yoludur.",
|
||||
"step4-description2": "Bunu indirmenizi ve güvenli bir ortamda saklamanızı öneriyoruz.",
|
||||
"step4-description3": "Kaybetmeniz durumunda bizim dahi erişemeyeceğimiz veya kurtaramayacağımız Gizli Anahtarınızı barındırır.",
|
||||
"step4-download": "PDF'yi indir",
|
||||
"step5-send-invites": "Davetleri yolla",
|
||||
"step5-invite-team": "Takımını davet et",
|
||||
"step5-subtitle": "Infisical takım arkadaşlarınız ile kullanılmak üzere yapılmıştır. Birlikte test etmek için onları davet edin.",
|
||||
"step5-skip": "Atla"
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"step1-submit": "Başla",
|
||||
"step2-message": "Şu adrese bir doğrulama maili yolladık",
|
||||
"step2-code-error": "Tüh. Kodun hatalı. Lütfen tekrar dene.",
|
||||
"step2-resend-alert": "Mail ulaşamadı mı?",
|
||||
"step2-resend-alert": "Kodu ulaşamadı mı?",
|
||||
"step2-resend-submit": "Tekrar Yolla",
|
||||
"step2-resend-progress": "Tekrar yollanıyor...",
|
||||
"step2-spam-alert": "Spam kutunuzu kontrol ettiğinizden emin olun.",
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Switch } from '@headlessui/react';
|
||||
interface ToggleProps {
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
addOverride: (value: string | undefined, pos: number) => void;
|
||||
pos: number;
|
||||
addOverride: (value: string | undefined, id: string) => void;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -13,18 +13,18 @@ interface ToggleProps {
|
||||
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||
* @param {function} obj.setEnabled - change the state of the toggle
|
||||
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||
* @param {number} obj.pos - position of a certain secret
|
||||
* @param {number} obj.id - id of a certain secret
|
||||
* @returns
|
||||
*/
|
||||
const Toggle = ({ enabled, setEnabled, addOverride, pos }: ToggleProps): JSX.Element => {
|
||||
const Toggle = ({ enabled, setEnabled, addOverride, id }: ToggleProps): JSX.Element => {
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled === false) {
|
||||
addOverride('', pos);
|
||||
addOverride('', id);
|
||||
} else {
|
||||
addOverride(undefined, pos);
|
||||
addOverride(undefined, id);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { plans } from 'public/data/frequentConstants';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
@@ -12,7 +10,6 @@ type Props = {
|
||||
submitModal: (email: string) => void;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
currentPlan: string;
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
@@ -22,13 +19,11 @@ const AddUserDialog = ({
|
||||
submitModal,
|
||||
email,
|
||||
setEmail,
|
||||
currentPlan,
|
||||
orgName,
|
||||
}: Props) => {
|
||||
const submit = () => {
|
||||
submitModal(email);
|
||||
};
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className='z-50'>
|
||||
@@ -81,28 +76,6 @@ const AddUserDialog = ({
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
{currentPlan === plans.starter && (
|
||||
<div className='flex flex-row'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={() =>
|
||||
router.push(`/settings/billing/${ router.query.id}`)
|
||||
}
|
||||
>
|
||||
You can add up to 5 members on the Starter plan.
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='ml-1 inline-flex justify-center rounded-md py-1 underline underline-offset-2 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={() =>
|
||||
router.push(`/settings/billing/${ router.query.id}`)
|
||||
}
|
||||
>
|
||||
Upgrade now.
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className='mt-4 max-w-max'>
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
|
||||
@@ -45,19 +45,19 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-mineshaft-600 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-bunker-200">
|
||||
{t('dashboard:sidebar.delete-key-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-bunker-300">
|
||||
{t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-start">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-red-700 hover:bg-red-600 px-4 py-2 text-sm font-medium text-bunker-200 hover:text-white text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-red-500 opacity-80 hover:opacity-100 px-4 py-2 text-sm font-medium text-bunker-100 text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Delete
|
||||
|
||||
@@ -30,7 +30,7 @@ type Props = {
|
||||
type EnvironmentProps = {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the component that shows the users of a certin project
|
||||
@@ -91,26 +91,38 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handlePermissionUpdate = (index: number, val: string, membershipId: string, slug: string ) => {
|
||||
let denials: { ability: string; environmentSlug: string; }[];
|
||||
if (val === "Read Only") {
|
||||
denials = [{
|
||||
ability: "write",
|
||||
environmentSlug: slug
|
||||
}];
|
||||
} else if (val === "No Access") {
|
||||
denials = [{
|
||||
ability: "write",
|
||||
environmentSlug: slug
|
||||
}, {
|
||||
ability: "read",
|
||||
environmentSlug: slug
|
||||
}];
|
||||
} else if (val === "Add Only") {
|
||||
denials = [{
|
||||
ability: "read",
|
||||
environmentSlug: slug
|
||||
}];
|
||||
const handlePermissionUpdate = (
|
||||
index: number,
|
||||
val: string,
|
||||
membershipId: string,
|
||||
slug: string
|
||||
) => {
|
||||
let denials: { ability: string; environmentSlug: string }[];
|
||||
if (val === 'Read Only') {
|
||||
denials = [
|
||||
{
|
||||
ability: 'write',
|
||||
environmentSlug: slug
|
||||
}
|
||||
];
|
||||
} else if (val === 'No Access') {
|
||||
denials = [
|
||||
{
|
||||
ability: 'write',
|
||||
environmentSlug: slug
|
||||
},
|
||||
{
|
||||
ability: 'read',
|
||||
environmentSlug: slug
|
||||
}
|
||||
];
|
||||
} else if (val === 'Add Only') {
|
||||
denials = [
|
||||
{
|
||||
ability: 'read',
|
||||
environmentSlug: slug
|
||||
}
|
||||
];
|
||||
} else {
|
||||
denials = [];
|
||||
}
|
||||
@@ -118,8 +130,12 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
|
||||
setIsUpgradeModalOpen(true);
|
||||
} else {
|
||||
const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
|
||||
updateUserProjectPermission({ membershipId, denials: allDenials});
|
||||
const allDenials = userData[index].deniedPermissions
|
||||
.filter(
|
||||
(perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
|
||||
)
|
||||
.concat(denials);
|
||||
updateUserProjectPermission({ membershipId, denials: allDenials });
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
@@ -156,7 +172,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
orgId
|
||||
});
|
||||
if (subscriptions) {
|
||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
||||
setCurrentPlan(subscriptions.data[0].plan.product);
|
||||
}
|
||||
})();
|
||||
}, [userData, myUser]);
|
||||
@@ -186,25 +202,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
|
||||
const closeUpgradeModal = () => {
|
||||
setIsUpgradeModalOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
|
||||
<div className="absolute rounded-t-md w-full h-[3.1rem] bg-white/5" />
|
||||
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-700 bg-bunker">
|
||||
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
|
||||
<UpgradePlanModal
|
||||
isOpen={isUpgradeModalOpen}
|
||||
onClose={closeUpgradeModal}
|
||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
||||
/>
|
||||
<table className="w-full my-0.5">
|
||||
<thead className="text-gray-400 text-xs font-light">
|
||||
<table className="my-0.5 w-full">
|
||||
<thead className="text-xs font-light text-gray-400">
|
||||
<tr>
|
||||
<th className="text-left pl-4 py-3.5">NAME</th>
|
||||
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
||||
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
||||
{workspaceEnvs.map(env => (
|
||||
<th key={guidGenerator()} className="text-left pl-2 py-1 max-w-min break-normal">
|
||||
<span>{env.slug.toUpperCase()}<br/></span>
|
||||
<th className="py-3.5 pl-4 text-left">NAME</th>
|
||||
<th className="py-3.5 pl-4 text-left">EMAIL</th>
|
||||
<th className="py-3.5 pl-6 pr-10 text-left">ROLE</th>
|
||||
{workspaceEnvs.map((env) => (
|
||||
<th key={guidGenerator()} className="max-w-min break-normal py-1 pl-2 text-left">
|
||||
<span>
|
||||
{env.slug.toUpperCase()}
|
||||
<br />
|
||||
</span>
|
||||
{/* <span>PERMISSION</span> */}
|
||||
</th>
|
||||
))}
|
||||
@@ -227,28 +246,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
)
|
||||
.map((row, index) => (
|
||||
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500">
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
|
||||
{row.firstName} {row.lastName}
|
||||
</td>
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
|
||||
{row.email}
|
||||
</td>
|
||||
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<div className="justify-start h-full flex flex-row items-center">
|
||||
<Select
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-6 pr-10 text-gray-300">
|
||||
<div className="flex h-full flex-row items-center justify-start">
|
||||
<Select
|
||||
className="w-36 bg-mineshaft-700"
|
||||
dropdownContainerClassName="bg-mineshaft-700"
|
||||
// open={isOpen}
|
||||
onValueChange={(e) => handleRoleUpdate(index, e)}
|
||||
value={row.role}
|
||||
disabled={myRole !== 'admin' || myUser === row.email}
|
||||
isDisabled={myRole !== 'admin' || myUser === row.email}
|
||||
// onOpenChange={(open) => setIsOpen(open)}
|
||||
>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
</Select>
|
||||
{row.status === 'completed' && myUser !== row.email && (
|
||||
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
|
||||
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
|
||||
<Button
|
||||
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
||||
color="mineshaft"
|
||||
@@ -259,43 +278,106 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-2 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<Select
|
||||
className="w-16 bg-mineshaft-700"
|
||||
dropdownContainerClassName="bg-mineshaft-700"
|
||||
position="item-aligned"
|
||||
// open={isOpen}
|
||||
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
|
||||
value={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
|
||||
? "No Access"
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Read Only"
|
||||
: !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Add Only" : "Read & Write")
|
||||
}
|
||||
icon={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
|
||||
? faEyeSlash
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faEye
|
||||
: !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faPlus : faPenToSquare)
|
||||
}
|
||||
disabled={myRole !== 'admin'}
|
||||
// onOpenChange={(open) => setIsOpen(open)}
|
||||
{workspaceEnvs.map((env) => (
|
||||
<td
|
||||
key={guidGenerator()}
|
||||
className="border-t border-mineshaft-700 py-2 pl-2 text-gray-300"
|
||||
>
|
||||
<SelectItem value="No Access" customIcon={faEyeSlash}>No Access</SelectItem>
|
||||
<SelectItem value="Read Only" customIcon={faEye}>Read Only</SelectItem>
|
||||
<SelectItem value="Add Only" customIcon={faPlus}>Add Only</SelectItem>
|
||||
<SelectItem value="Read & Write" customIcon={faPenToSquare}>Read & Write</SelectItem>
|
||||
</Select>
|
||||
</td>)}
|
||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||
<Select
|
||||
className="w-16 bg-mineshaft-700"
|
||||
dropdownContainerClassName="bg-mineshaft-700"
|
||||
position="item-aligned"
|
||||
// open={isOpen}
|
||||
onValueChange={(val) =>
|
||||
handlePermissionUpdate(index, val, row.membershipId, env.slug)
|
||||
}
|
||||
value={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('write') &&
|
||||
row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('read')
|
||||
? 'No Access'
|
||||
: // eslint-disable-next-line no-nested-ternary
|
||||
row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('write') &&
|
||||
!row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('read')
|
||||
? 'Read Only'
|
||||
: !row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('write') &&
|
||||
row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('read')
|
||||
? 'Add Only'
|
||||
: 'Read & Write'
|
||||
}
|
||||
icon={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('write') &&
|
||||
row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('read')
|
||||
? faEyeSlash
|
||||
: // eslint-disable-next-line no-nested-ternary
|
||||
row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('write') &&
|
||||
!row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('read')
|
||||
? faEye
|
||||
: !row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('write') &&
|
||||
row.deniedPermissions
|
||||
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||
.map((perm: { ability: string }) => perm.ability)
|
||||
.includes('read')
|
||||
? faPlus
|
||||
: faPenToSquare
|
||||
}
|
||||
isDisabled={myRole !== 'admin'}
|
||||
// onOpenChange={(open) => setIsOpen(open)}
|
||||
>
|
||||
<SelectItem value="No Access" customIcon={faEyeSlash}>
|
||||
No Access
|
||||
</SelectItem>
|
||||
<SelectItem value="Read Only" customIcon={faEye}>
|
||||
Read Only
|
||||
</SelectItem>
|
||||
<SelectItem value="Add Only" customIcon={faPlus}>
|
||||
Add Only
|
||||
</SelectItem>
|
||||
<SelectItem value="Read & Write" customIcon={faPenToSquare}>
|
||||
Read & Write
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</td>
|
||||
))}
|
||||
<td className="border-0.5 flex flex-row justify-end border-t border-mineshaft-700 py-2 pl-8 pr-8">
|
||||
{myUser !== row.email &&
|
||||
// row.role !== "admin" &&
|
||||
myRole !== 'member' ? (
|
||||
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
|
||||
<div className="mt-0.5 flex items-center opacity-50 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||
color="red"
|
||||
@@ -304,7 +386,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-9 h-9" />
|
||||
<div className="h-9 w-9" />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -36,7 +36,7 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full flex items-center justify-between px-6 py-6 rounded-md border border-bunker-500 pointer-events-auto bg-mineshaft-700 mb-3 right-3"
|
||||
className="relative w-full flex items-center justify-between px-6 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-mineshaft-700 mb-3 right-3"
|
||||
role="alert"
|
||||
>
|
||||
{notification.type === 'error' && (
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Tag } from 'public/data/frequentInterfaces';
|
||||
* @param {function} obj.modifyTags - modify tags for a certain secret
|
||||
* @param {Tag[]} obj.position - currently selected tags for a certain secret
|
||||
*/
|
||||
const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => {
|
||||
const AddTagsMenu = ({ allTags, currentTags, modifyTags, id }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], id: string) => void; id: string; }) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Menu as="div" className="ml-2 relative inline-block text-left">
|
||||
@@ -41,7 +41,7 @@ const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags:
|
||||
<button
|
||||
type="button"
|
||||
className={`${currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`}
|
||||
onClick={() => {if (!currentTags?.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), position)}}}
|
||||
onClick={() => {if (!currentTags?.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), id)}}}
|
||||
>
|
||||
{currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? <FontAwesomeIcon icon={faCheckSquare} className="text-xs mr-2 text-primary"/> : <FontAwesomeIcon icon={faSquare} className="text-xs mr-2"/>} {tag.name}
|
||||
</button>
|
||||
|
||||
@@ -6,11 +6,11 @@ import { useTranslation } from 'next-i18next';
|
||||
const CommentField = ({
|
||||
comment,
|
||||
modifyComment,
|
||||
position
|
||||
id
|
||||
}: {
|
||||
comment: string;
|
||||
modifyComment: (value: string, posistion: number) => void;
|
||||
position: number;
|
||||
modifyComment: (value: string, id: string) => void;
|
||||
id: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -20,7 +20,7 @@ const CommentField = ({
|
||||
<textarea
|
||||
className="placeholder:text-bunker-400 dark:[color-scheme:dark] h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, position)}
|
||||
onChange={(e) => modifyComment(e.target.value, id)}
|
||||
placeholder="Leave any comments here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,15 +9,15 @@ import { PopoverObject } from '../v2/Popover/Popover';
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
|
||||
interface DashboardInputFieldProps {
|
||||
position: number;
|
||||
onChangeHandler: (value: string, position: number) => void;
|
||||
id: string;
|
||||
onChangeHandler: (value: string, id: string) => void;
|
||||
value: string | undefined;
|
||||
type: 'varName' | 'value' | 'comment';
|
||||
blurred?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
isCapitalized?: boolean;
|
||||
overrideEnabled?: boolean;
|
||||
modifyValueOverride?: (value: string | undefined, position: number) => void;
|
||||
modifyValueOverride?: (value: string | undefined, id: string) => void;
|
||||
isSideBarOpen?: boolean;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ interface DashboardInputFieldProps {
|
||||
*/
|
||||
|
||||
const DashboardInputField = ({
|
||||
position,
|
||||
id,
|
||||
onChangeHandler,
|
||||
type,
|
||||
value,
|
||||
@@ -72,7 +72,7 @@ const DashboardInputField = ({
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, position)}
|
||||
onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)}
|
||||
type={type}
|
||||
value={value}
|
||||
className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${
|
||||
@@ -99,15 +99,15 @@ const DashboardInputField = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!error && <div className={`absolute right-0 top-0 text-red z-50 ${
|
||||
overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700'
|
||||
{!error && <div className={`absolute right-0 top-0 text-red z-50 bg-mineshaft-800 group-hover:bg-mineshaft-700 ${
|
||||
overrideEnabled ? 'visible' : 'invisible group-hover:visible'
|
||||
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
|
||||
<button type="button" onClick={() => {
|
||||
if (modifyValueOverride) {
|
||||
if (overrideEnabled === false) {
|
||||
modifyValueOverride('', position);
|
||||
modifyValueOverride('', id);
|
||||
} else {
|
||||
modifyValueOverride(undefined, position);
|
||||
modifyValueOverride(undefined, id);
|
||||
}
|
||||
}
|
||||
}}>
|
||||
@@ -126,7 +126,7 @@ const DashboardInputField = ({
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<PopoverObject text={value || ''} onChangeHandler={onChangeHandler} position={position}>
|
||||
<PopoverObject text={value || ''} onChangeHandler={onChangeHandler} id={id}>
|
||||
<div title={value} className={`relative flex-col w-full h-10 overflow-hidden ${
|
||||
isSideBarOpen && 'bg-mineshaft-700 duration-200'
|
||||
}`}>
|
||||
@@ -157,7 +157,7 @@ const DashboardInputField = ({
|
||||
)}
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
onChange={(e) => onChangeHandler(e.target.value, id)}
|
||||
onScroll={syncScroll}
|
||||
className={`${
|
||||
blurred
|
||||
@@ -175,10 +175,10 @@ const DashboardInputField = ({
|
||||
} ${overrideEnabled ? 'text-primary-300' : 'text-gray-400'}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ${blurred ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{value?.split(REGEX).map((word, id) => {
|
||||
{value?.split(REGEX).map((word) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={`${word}.${id + 1}`}>
|
||||
<span className="ph-no-capture text-yellow" key={id}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">
|
||||
{word.slice(2, word.length - 1)}
|
||||
@@ -231,7 +231,7 @@ function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInput
|
||||
return (
|
||||
prev.value === next.value &&
|
||||
prev.type === next.type &&
|
||||
prev.position === next.position &&
|
||||
prev.id === next.id &&
|
||||
prev.blurred === next.blurred &&
|
||||
prev.isCapitalized === next.isCapitalized &&
|
||||
prev.overrideEnabled === next.overrideEnabled &&
|
||||
|
||||
@@ -8,6 +8,7 @@ import { parseDocument, Scalar, YAMLMap } from 'yaml';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Error from '../basic/Error';
|
||||
import { useNotificationContext } from '../context/Notifications/NotificationProvider';
|
||||
import { parseDotEnv } from '../utilities/parseDotEnv';
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
|
||||
@@ -32,6 +33,7 @@ const DropZone = ({
|
||||
numCurrentRows
|
||||
}: DropZoneProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -110,6 +112,15 @@ const DropZone = ({
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
const reader = new FileReader();
|
||||
if (file === undefined) {
|
||||
createNotification({
|
||||
text: `You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.`,
|
||||
type: 'error',
|
||||
timeoutMs: 10000
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const fileType = file.name.split('.')[1];
|
||||
|
||||
reader.onload = (event) => {
|
||||
@@ -164,7 +175,7 @@ const DropZone = ({
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="opacity-0 absolute w-full h-full"
|
||||
accept=".txt,.env,.yml"
|
||||
accept=""
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
|
||||
|
||||
@@ -10,10 +10,10 @@ import { Menu, Transition } from '@headlessui/react';
|
||||
*/
|
||||
const GenerateSecretMenu = ({
|
||||
modifyValue,
|
||||
position
|
||||
id
|
||||
}: {
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
position: number;
|
||||
modifyValue: (value: string, id: string) => void;
|
||||
id: string;
|
||||
}) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
const { t } = useTranslation();
|
||||
@@ -51,7 +51,7 @@ const GenerateSecretMenu = ({
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
position
|
||||
id
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -8,11 +8,11 @@ import { DeleteActionButton } from './DeleteActionButton';
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyValueOverride: (value: string | undefined, position: number) => void;
|
||||
modifyComment: (value: string, position: number) => void;
|
||||
modifyTags: (value: Tag[], position: number) => void;
|
||||
modifyKey: (value: string, id: string) => void;
|
||||
modifyValue: (value: string, id: string) => void;
|
||||
modifyValueOverride: (value: string | undefined, id: string) => void;
|
||||
modifyComment: (value: string, id: string) => void;
|
||||
modifyTags: (value: Tag[], id: string) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
@@ -55,7 +55,7 @@ const colorsText = [
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {SecretDataProps[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
|
||||
@@ -108,7 +108,7 @@ const KeyPair = ({
|
||||
isCapitalized = {isCapitalized}
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
|
||||
value={keyPair.key}
|
||||
isDuplicate={isDuplicate}
|
||||
overrideEnabled={keyPair.valueOverride !== undefined}
|
||||
@@ -124,7 +124,7 @@ const KeyPair = ({
|
||||
<DashboardInputField
|
||||
onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
|
||||
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
|
||||
blurred={isBlurred}
|
||||
overrideEnabled={keyPair.valueOverride !== undefined}
|
||||
@@ -137,7 +137,7 @@ const KeyPair = ({
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyComment}
|
||||
type="comment"
|
||||
position={keyPair.pos}
|
||||
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
|
||||
value={keyPair.comment}
|
||||
isDuplicate={isDuplicate}
|
||||
isSideBarOpen={keyPair.id === sidebarSecretId}
|
||||
@@ -149,11 +149,11 @@ const KeyPair = ({
|
||||
{keyPair.tags?.map((tag, index) => (
|
||||
index < 2 && <div key={keyPair.pos} className={`ml-2 px-1.5 ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
|
||||
<span className='mb-0.5 cursor-default'>{tag.name}</span>
|
||||
<FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
|
||||
<FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.id)}/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} position={keyPair.pos} />
|
||||
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -33,10 +33,10 @@ export interface DeleteRowFunctionProps {
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
data: SecretProps[];
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyValueOverride: (value: string | undefined, position: number) => void;
|
||||
modifyComment: (value: string, position: number) => void;
|
||||
modifyKey: (value: string, id: string) => void;
|
||||
modifyValue: (value: string, id: string) => void;
|
||||
modifyValueOverride: (value: string | undefined, id: string) => void;
|
||||
modifyComment: (value: string, id: string) => void;
|
||||
buttonReady: boolean;
|
||||
savePush: () => void;
|
||||
sharedToHide: string[];
|
||||
@@ -110,7 +110,7 @@ const SideBar = ({
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
id={data[0]?.id}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
@@ -128,14 +128,14 @@ const SideBar = ({
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data[0].pos}
|
||||
id={data[0].id}
|
||||
value={data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
|
||||
<GenerateSecretMenu modifyValue={modifyValue} id={data[0]?.id} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -154,7 +154,7 @@ const SideBar = ({
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={modifyValueOverride}
|
||||
pos={data[0]?.pos}
|
||||
id={data[0]?.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -167,14 +167,14 @@ const SideBar = ({
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValueOverride}
|
||||
type="value"
|
||||
position={data[0]?.pos}
|
||||
id={data[0]?.id}
|
||||
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute right-[0.57rem] top-[0.3rem] z-50">
|
||||
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
|
||||
<GenerateSecretMenu modifyValue={modifyValueOverride} id={data[0]?.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,7 +182,7 @@ const SideBar = ({
|
||||
<CommentField
|
||||
comment={data[0]?.comment}
|
||||
modifyComment={modifyComment}
|
||||
position={data[0]?.pos}
|
||||
id={data[0]?.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
147
frontend/src/components/login/LoginStep.tsx
Normal file
147
frontend/src/components/login/LoginStep.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faWarning } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import Error from '@app/components/basic/Error';
|
||||
import InputField from '@app/components/basic/InputField';
|
||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
|
||||
/**
|
||||
* 1st step of login - user enters their username and password
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.email - email of user
|
||||
* @param {Function} obj.setEmail - function to set the email of user
|
||||
* @param {String} obj.password - password of user
|
||||
* @param {String} obj.setPassword - function to set the password of user
|
||||
* @param {Function} obj.setStep - function to set the login flow step
|
||||
* @returns
|
||||
*/
|
||||
export default function LoginStep ({
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
setStep
|
||||
}: {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
password: string;
|
||||
setPassword: (password: string) => void;
|
||||
setStep: (step: number) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const isLoginSuccessful = await attemptLogin(email, password);
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
// case: login was successful
|
||||
|
||||
if (isLoginSuccessful.mfaEnabled) {
|
||||
// case: login requires MFA step
|
||||
setStep(2);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||
{t('login:login')}
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label={t('common:email')}
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label={t('common:password')}
|
||||
onChangeHandler={setPassword}
|
||||
type="password"
|
||||
value={password}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||
<Link href="/verify-email">
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
||||
>
|
||||
{t('login:forgot-password')}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t('login:error-login') ?? ''} />}
|
||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text={t('login:login') ?? ''}
|
||||
onButtonPressed={async () => handleLogin()}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
{t('common:maintenance-alert')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||
{t('login:need-account')}
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
||||
>
|
||||
{t('login:create-account')}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
|
||||
162
frontend/src/components/login/MFAStep.tsx
Normal file
162
frontend/src/components/login/MFAStep.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React, { useState } from 'react';
|
||||
import ReactCodeInput from 'react-code-input';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa';
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { useSendMfaToken } from '@app/hooks/api/auth';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Error from '../basic/Error';
|
||||
|
||||
// The style for the verification code input
|
||||
const props = {
|
||||
inputStyle: {
|
||||
fontFamily: 'monospace',
|
||||
margin: '4px',
|
||||
MozAppearance: 'textfield',
|
||||
width: '55px',
|
||||
borderRadius: '5px',
|
||||
fontSize: '24px',
|
||||
height: '55px',
|
||||
paddingLeft: '7',
|
||||
backgroundColor: '#0d1117',
|
||||
color: 'white',
|
||||
border: '1px solid #2d2f33',
|
||||
textAlign: 'center',
|
||||
outlineColor: '#8ca542',
|
||||
borderColor: '#2d2f33'
|
||||
}
|
||||
} as const;
|
||||
|
||||
interface VerifyMfaTokenError {
|
||||
response: {
|
||||
data: {
|
||||
context: {
|
||||
code: string;
|
||||
triesLeft: number;
|
||||
}
|
||||
},
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 2nd step of login - users enter their MFA code
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.email - email of user
|
||||
* @param {String} obj.password - password of user
|
||||
* @param {Function} obj.setStep - function to set the login flow step
|
||||
* @returns
|
||||
*/
|
||||
export default function MFAStep({
|
||||
email,
|
||||
password
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
}): JSX.Element {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||
const [mfaCode, setMfaCode] = useState('');
|
||||
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sendMfaToken = useSendMfaToken();
|
||||
|
||||
const handleLoginMfa = async () => {
|
||||
try {
|
||||
if (mfaCode.length !== 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const isLoginSuccessful = await attemptLoginMfa({
|
||||
email,
|
||||
password,
|
||||
mfaToken: mfaCode
|
||||
});
|
||||
|
||||
if (isLoginSuccessful) {
|
||||
setIsLoading(false);
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
const error = err as VerifyMfaTokenError;
|
||||
|
||||
if (error?.response?.status === 500) {
|
||||
window.location.reload();
|
||||
} else if (error?.response?.data?.context?.triesLeft) {
|
||||
setTriesLeft(error?.response?.data?.context?.triesLeft);
|
||||
if (error.response.data.context.triesLeft === 0) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleResendMfaCode = async () => {
|
||||
try {
|
||||
setIsLoadingResend(true);
|
||||
await sendMfaToken.mutateAsync({ email });
|
||||
setIsLoadingResend(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setIsLoadingResend(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
||||
<p className="text-l flex justify-center text-bunker-300">{t('mfa:step2-message')}</p>
|
||||
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
|
||||
<div className="hidden md:block">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setMfaCode}
|
||||
{...props}
|
||||
className="mt-6 mb-2"
|
||||
/>
|
||||
</div>
|
||||
{typeof triesLeft === 'number' && <Error text={`${t('mfa:step2-code-error')} ${triesLeft}`} />}
|
||||
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<Button
|
||||
text={t('mfa:verify') ?? ''}
|
||||
onButtonPressed={() => handleLoginMfa()}
|
||||
size="lg"
|
||||
/>
|
||||
</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('mfa:step2-resend-alert')}</span>
|
||||
<u
|
||||
className={`font-normal ${
|
||||
isLoadingResend
|
||||
? 'text-bunker-400'
|
||||
: 'text-primary-700 hover:text-primary duration-200'
|
||||
}`}
|
||||
>
|
||||
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
|
||||
{isLoadingResend
|
||||
? t('mfa:step2-resend-progress')
|
||||
: t('mfa:step2-resend-submit')}
|
||||
</button>
|
||||
</u>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-400 pb-2">{t('mfa:step2-spam-alert')}</p>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = getTranslatedStaticProps(['auth', 'mfa']);
|
||||
@@ -87,13 +87,12 @@ export default function Navbar() {
|
||||
}, []);
|
||||
|
||||
const closeApp = async () => {
|
||||
console.log('Logging out...');
|
||||
await logout();
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[61]">
|
||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[71]">
|
||||
<div className="m-auto flex justify-start items-center mx-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
@@ -166,7 +165,7 @@ export default function Navbar() {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[65] ring-opacity-5 focus:outline-none">
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[999] ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 z-[100]">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t('nav:user.signed-in-as')}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function DonwloadBackupPDFStep({
|
||||
|
||||
return (
|
||||
<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">
|
||||
<p className="text-4xl text-center font-semibold flex justify-center text-primary">
|
||||
{t('signup:step4-message')}
|
||||
</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">
|
||||
@@ -42,7 +42,7 @@ export default function DonwloadBackupPDFStep({
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
|
||||
{t('signup:step4-description3')}
|
||||
</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 () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -8,18 +9,21 @@ import nacl from 'tweetnacl';
|
||||
import { encodeBase64 } from 'tweetnacl-util';
|
||||
|
||||
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
|
||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||
import ProjectService from '@app/services/ProjectService';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import InputField from '../basic/InputField';
|
||||
import attemptLogin from '../utilities/attemptLogin';
|
||||
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';
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
interface UserInfoStepProps {
|
||||
verificationToken: string;
|
||||
incrementStep: () => void;
|
||||
email: string;
|
||||
password: string;
|
||||
@@ -44,7 +48,6 @@ interface UserInfoStepProps {
|
||||
* @param {string} obj.setLastName - function managing the state of user's last name
|
||||
*/
|
||||
export default function UserInfoStep({
|
||||
verificationToken,
|
||||
incrementStep,
|
||||
email,
|
||||
password,
|
||||
@@ -62,7 +65,6 @@ export default function UserInfoStep({
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
// Verifies if the information that the users entered (name, workspace)
|
||||
// is there, and if the password matches the criteria.
|
||||
@@ -94,17 +96,9 @@ export default function UserInfoStep({
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
|
||||
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: PRIVATE_KEY,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||
}) as { ciphertext: string; iv: string; tag: string };
|
||||
|
||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
localStorage.setItem('PRIVATE_KEY', privateKey);
|
||||
|
||||
client.init(
|
||||
{
|
||||
@@ -113,35 +107,90 @@ export default function UserInfoStep({
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||
const response = await completeAccountInformationSignup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: `${firstName}'s organization`,
|
||||
publicKey: PUBLIC_KEY,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
token: verificationToken
|
||||
});
|
||||
try {
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
if (response.status === 200) {
|
||||
// response = await response.json();
|
||||
// TODO: moduralize into KeyService
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password,
|
||||
salt: result.salt,
|
||||
mem: 65536,
|
||||
time: 3,
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||
|
||||
localStorage.setItem('publicKey', PUBLIC_KEY);
|
||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||
localStorage.setItem('iv', iv);
|
||||
localStorage.setItem('tag', tag);
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: privateKey,
|
||||
secret: key
|
||||
});
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString('hex'),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
const response = await completeAccountInformationSignup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
organizationName: `${firstName}'s organization`
|
||||
});
|
||||
|
||||
// unset signup JWT token and set JWT token
|
||||
SecurityClient.setSignupToken('');
|
||||
SecurityClient.setToken(response.token);
|
||||
|
||||
try {
|
||||
await attemptLogin(email, password, () => {}, router, true, false);
|
||||
incrementStep();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
saveTokenToLocalStorage({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const orgId = userOrgs[0]?._id;
|
||||
const project = await ProjectService.initProject({
|
||||
organizationId: orgId,
|
||||
projectName: 'Example Project'
|
||||
});
|
||||
|
||||
localStorage.setItem('orgData.id', orgId);
|
||||
localStorage.setItem('projectData.id', project._id);
|
||||
|
||||
incrementStep();
|
||||
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -153,7 +202,7 @@ export default function UserInfoStep({
|
||||
|
||||
return (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 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">
|
||||
<p className="text-4xl font-bold flex justify-center mb-6 mx-8 md:mx-16 text-primary">
|
||||
{t('signup:step3-message')}
|
||||
</p>
|
||||
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
|
||||
@@ -258,4 +307,4 @@ export default function UserInfoStep({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,19 @@
|
||||
import { getAuthToken, setAuthToken } from '@app/reactQuery';
|
||||
import {
|
||||
getAuthToken,
|
||||
setAuthToken,
|
||||
setMfaTempToken,
|
||||
setSignupTempToken} from '@app/reactQuery';
|
||||
|
||||
// depreciated: go for apiRequest module in config/api
|
||||
export default class SecurityClient {
|
||||
static setSignupToken(tokenStr: string) {
|
||||
setSignupTempToken(tokenStr);
|
||||
}
|
||||
|
||||
static setMfaToken(tokenStr: string) {
|
||||
setMfaTempToken(tokenStr);
|
||||
}
|
||||
|
||||
static setToken(tokenStr: string) {
|
||||
setAuthToken(tokenStr);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import crypto from 'crypto';
|
||||
|
||||
import jsrp from 'jsrp';
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
|
||||
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||
import login1 from '@app/pages/api/auth/Login1';
|
||||
import login2 from '@app/pages/api/auth/Login2';
|
||||
import addSecrets from '@app/pages/api/files/AddSecrets';
|
||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||
import getUser from '@app/pages/api/user/getUser';
|
||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||
import KeyService from '@app/services/KeyService';
|
||||
|
||||
import { encryptAssymmetric } from './cryptography/crypto';
|
||||
import encryptSecrets from './secrets/encryptSecrets';
|
||||
import Telemetry from './telemetry/Telemetry';
|
||||
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
||||
import SecurityClient from './SecurityClient';
|
||||
@@ -22,212 +14,130 @@ import SecurityClient from './SecurityClient';
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
interface IsLoginSuccessful {
|
||||
mfaEnabled: boolean;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function logs in the user (whether it's right after signup, or a normal login)
|
||||
* @param {string} email - email of the user logging in
|
||||
* @param {string} password - password of the user logging in
|
||||
* @param {function} setErrorLogin - function that visually dispay an error is something is wrong
|
||||
* @param {*} router
|
||||
* @param {boolean} isSignUp - whether this log in is a part of signup
|
||||
* @param {boolean} isLogin - ?
|
||||
* @returns
|
||||
* Return whether or not login is successful for user with email [email]
|
||||
* and password [password]
|
||||
* @param {string} email - email of user to log in
|
||||
* @param {string} password - password of user to log in
|
||||
*/
|
||||
const attemptLogin = async (
|
||||
email: string,
|
||||
password: string,
|
||||
setErrorLogin: (value: boolean) => void,
|
||||
router: any,
|
||||
isSignUp: boolean,
|
||||
isLogin: boolean
|
||||
) => {
|
||||
try {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
|
||||
password: string
|
||||
): Promise<IsLoginSuccessful> => {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
return new Promise((resolve, reject) => {
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
|
||||
try {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
|
||||
|
||||
client.setSalt(salt);
|
||||
client.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = client.getProof(); // called M1
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
const { token, publicKey, encryptedPrivateKey, iv, tag } = await login2(
|
||||
|
||||
const {
|
||||
mfaEnabled,
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
token,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag
|
||||
} = await login2(
|
||||
email,
|
||||
clientProof
|
||||
);
|
||||
|
||||
if (mfaEnabled) {
|
||||
// case: MFA is enabled
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
// set temporary (MFA) JWT token
|
||||
SecurityClient.setMfaToken(token);
|
||||
|
||||
const privateKey = Aes256Gcm.decrypt({
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
|
||||
|
||||
let orgToLogin;
|
||||
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
||||
orgToLogin = localStorage.getItem('orgData.id');
|
||||
} else {
|
||||
orgToLogin = userOrgsData[0];
|
||||
localStorage.setItem('orgData.id', orgToLogin);
|
||||
}
|
||||
|
||||
let orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: orgToLogin
|
||||
});
|
||||
|
||||
orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
|
||||
let projectToLogin;
|
||||
if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
|
||||
projectToLogin = localStorage.getItem('projectData.id');
|
||||
} else {
|
||||
try {
|
||||
projectToLogin = orgUserProjects[0];
|
||||
localStorage.setItem('projectData.id', projectToLogin);
|
||||
} catch (error) {
|
||||
console.log('ERROR: User likely has no projects. ', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (email) {
|
||||
telemetry.identify(email);
|
||||
telemetry.capture('User Logged In');
|
||||
}
|
||||
|
||||
if (isSignUp) {
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
|
||||
await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
|
||||
|
||||
const secretsToBeAdded: SecretDataProps[] = [
|
||||
{
|
||||
pos: 0,
|
||||
key: 'DATABASE_URL',
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
|
||||
valueOverride: undefined,
|
||||
comment: 'Secret referencing example',
|
||||
id: '',
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
pos: 1,
|
||||
key: 'DB_USERNAME',
|
||||
value: 'OVERRIDE_THIS',
|
||||
valueOverride: undefined,
|
||||
comment:
|
||||
'Override secrets with personal value',
|
||||
id: '',
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
pos: 2,
|
||||
key: 'DB_PASSWORD',
|
||||
value: 'OVERRIDE_THIS',
|
||||
valueOverride: undefined,
|
||||
comment:
|
||||
'Another secret override',
|
||||
id: '',
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
pos: 3,
|
||||
key: 'DB_USERNAME',
|
||||
value: 'user1234',
|
||||
valueOverride: 'user1234',
|
||||
comment: '',
|
||||
id: '',
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
pos: 4,
|
||||
key: 'DB_PASSWORD',
|
||||
value: 'example_password',
|
||||
valueOverride: 'example_password',
|
||||
comment: '',
|
||||
id: '',
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
pos: 5,
|
||||
key: 'TWILIO_AUTH_TOKEN',
|
||||
value: 'example_twillio_token',
|
||||
valueOverride: undefined,
|
||||
comment: '',
|
||||
id: '',
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
pos: 6,
|
||||
key: 'WEBSITE_URL',
|
||||
value: 'http://localhost:3000',
|
||||
valueOverride: undefined,
|
||||
comment: '',
|
||||
id: '',
|
||||
tags: []
|
||||
}
|
||||
];
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeAdded,
|
||||
workspaceId: String(localStorage.getItem('projectData.id')),
|
||||
env: 'dev'
|
||||
resolve({
|
||||
mfaEnabled,
|
||||
success: true
|
||||
});
|
||||
await addSecrets({
|
||||
secrets: secrets ?? [],
|
||||
env: 'dev',
|
||||
workspaceId: String(localStorage.getItem('projectData.id'))
|
||||
} else if (
|
||||
!mfaEnabled &&
|
||||
encryptionVersion &&
|
||||
encryptedPrivateKey &&
|
||||
iv &&
|
||||
tag &&
|
||||
token
|
||||
) {
|
||||
// case: MFA is not enabled
|
||||
|
||||
// set JWT token
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
password,
|
||||
salt,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
// TODO: in the future - move this logic elsewhere
|
||||
// because this function is about logging the user in
|
||||
// and not initializing the login details
|
||||
const userOrgs = await getOrganizations();
|
||||
const orgId = userOrgs[0]._id;
|
||||
localStorage.setItem('orgData.id', orgId);
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId
|
||||
});
|
||||
|
||||
if (orgUserProjects.length > 0) {
|
||||
localStorage.setItem('projectData.id', orgUserProjects[0]._id);
|
||||
}
|
||||
|
||||
if (email) {
|
||||
telemetry.identify(email);
|
||||
telemetry.capture('User Logged In');
|
||||
}
|
||||
|
||||
resolve({
|
||||
mfaEnabled: false,
|
||||
success: true
|
||||
});
|
||||
}
|
||||
|
||||
if (isLogin) {
|
||||
if (localStorage.getItem('projectData.id') !== "undefined") {
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
} else {
|
||||
router.push("/noprojects");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setErrorLogin(true);
|
||||
console.log('Login response not available');
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during authentication');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export default attemptLogin;
|
||||
export default attemptLogin;
|
||||
103
frontend/src/components/utilities/attemptLoginMfa.ts
Normal file
103
frontend/src/components/utilities/attemptLoginMfa.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from 'jsrp';
|
||||
|
||||
import login1 from '@app/pages/api/auth/Login1';
|
||||
import verifyMfaToken from '@app/pages/api/auth/verifyMfaToken';
|
||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||
import KeyService from '@app/services/KeyService';
|
||||
|
||||
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
||||
import SecurityClient from './SecurityClient';
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
/**
|
||||
* Return whether or not MFA-login is successful for user with email [email]
|
||||
* and MFA token [mfaToken]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.email - email of user
|
||||
* @param {String} obj.mfaToken - MFA code/token
|
||||
*/
|
||||
const attemptLoginMfa = async ({
|
||||
email,
|
||||
password,
|
||||
mfaToken
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
mfaToken: string;
|
||||
}): Promise<Boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.init({
|
||||
username: email,
|
||||
password
|
||||
}, async () => {
|
||||
try {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
const { salt } = await login1(email, clientPublicKey);
|
||||
|
||||
const {
|
||||
encryptionVersion,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
token,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag
|
||||
} = await verifyMfaToken({
|
||||
email,
|
||||
mfaToken
|
||||
});
|
||||
|
||||
// unset temporary (MFA) JWT token and set JWT token
|
||||
SecurityClient.setMfaToken('');
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
encryptionVersion,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
password,
|
||||
salt,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
// TODO: in the future - move this logic elsewhere
|
||||
// because this function is about logging the user in
|
||||
// and not initializing the login details
|
||||
const userOrgs = await getOrganizations();
|
||||
const orgId = userOrgs[0]._id;
|
||||
localStorage.setItem('orgData.id', orgId);
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId
|
||||
});
|
||||
localStorage.setItem('projectData.id', orgUserProjects[0]._id);
|
||||
|
||||
resolve(true);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default attemptLoginMfa;
|
||||
@@ -9,14 +9,14 @@ const BLOCK_SIZE_BYTES = 16; // 128 bit
|
||||
|
||||
interface EncryptProps {
|
||||
text: string;
|
||||
secret: string;
|
||||
secret: string | Buffer;
|
||||
}
|
||||
|
||||
interface DecryptProps {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
secret: string;
|
||||
secret: string | Buffer;
|
||||
}
|
||||
|
||||
interface EncryptOutputProps {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/* eslint-disable new-cap */
|
||||
import crypto from 'crypto';
|
||||
|
||||
import jsrp from 'jsrp';
|
||||
|
||||
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
|
||||
import SRP1 from '@app/pages/api/auth/SRP1';
|
||||
|
||||
import { saveTokenToLocalStorage } from '../saveTokenToLocalStorage';
|
||||
import Aes256Gcm from './aes-256-gcm';
|
||||
import { deriveArgonKey } from './crypto';
|
||||
|
||||
const clientOldPassword = new jsrp.client();
|
||||
const clientNewPassword = new jsrp.client();
|
||||
@@ -63,43 +67,70 @@ const changePassword = async (
|
||||
},
|
||||
async () => {
|
||||
clientNewPassword.createVerifier(async (err, result) => {
|
||||
// The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø)
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password: newPassword,
|
||||
salt: result.salt,
|
||||
mem: 65536,
|
||||
time: 3,
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: localStorage.getItem('PRIVATE_KEY') as string,
|
||||
secret: newPassword
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size),
|
||||
'0'
|
||||
)
|
||||
secret: key
|
||||
});
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString('hex'),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
if (ciphertext) {
|
||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||
localStorage.setItem('iv', iv);
|
||||
localStorage.setItem('tag', tag);
|
||||
try {
|
||||
await changePassword2({
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier
|
||||
});
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await changePassword2({
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
clientProof
|
||||
});
|
||||
if (res && res.status === 400) {
|
||||
setCurrentPasswordError(true);
|
||||
} else if (res && res.status === 200) {
|
||||
setPasswordChanged(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
}
|
||||
} catch (error) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log(error);
|
||||
}
|
||||
saveTokenToLocalStorage({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
});
|
||||
|
||||
setPasswordChanged(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
} catch (error) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import argon2 from 'argon2-browser';
|
||||
|
||||
import aes from './aes-256-gcm';
|
||||
|
||||
const nacl = require('tweetnacl');
|
||||
@@ -9,6 +11,50 @@ type EncryptAsymmetricProps = {
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive a key from password [password] and salt [salt] using Argon2id
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.password - password to derive key from
|
||||
* @param {String} obj.salt - salt to derive key from
|
||||
* @param {Number} obj.mem - used memory, in KiB
|
||||
* @param {Number} obj.time - number of iterations
|
||||
* @param {Number} obj.parallelism - desired parallelism
|
||||
* @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key)
|
||||
* @returns
|
||||
*/
|
||||
const deriveArgonKey = async ({
|
||||
password,
|
||||
salt,
|
||||
mem,
|
||||
time,
|
||||
parallelism,
|
||||
hashLen
|
||||
}: {
|
||||
password: string;
|
||||
salt: string;
|
||||
mem: number;
|
||||
time: number;
|
||||
parallelism: number;
|
||||
hashLen: number;
|
||||
}) => {
|
||||
let derivedKey;
|
||||
try {
|
||||
derivedKey = await argon2.hash({
|
||||
pass: password,
|
||||
salt,
|
||||
type: argon2.ArgonType.Argon2id,
|
||||
mem,
|
||||
time,
|
||||
parallelism,
|
||||
hashLen
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return derivedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||
* [publicKey] likely belongs to the recipient.
|
||||
@@ -138,4 +184,10 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
|
||||
return plaintext;
|
||||
};
|
||||
|
||||
export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };
|
||||
export {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric,
|
||||
deriveArgonKey,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric
|
||||
};
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
interface Props {
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
privateKey: string;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
publicKey?: string;
|
||||
encryptedPrivateKey?: string;
|
||||
iv?: string;
|
||||
tag?: string;
|
||||
privateKey?: string;
|
||||
}
|
||||
|
||||
export const saveTokenToLocalStorage = ({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
@@ -14,11 +20,46 @@ export const saveTokenToLocalStorage = ({
|
||||
privateKey,
|
||||
}: Props) => {
|
||||
try {
|
||||
localStorage.setItem("publicKey", publicKey);
|
||||
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
|
||||
localStorage.setItem("iv", iv);
|
||||
localStorage.setItem("tag", tag);
|
||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||
|
||||
if (protectedKey) {
|
||||
localStorage.removeItem("protectedKey");
|
||||
localStorage.setItem("protectedKey", protectedKey);
|
||||
}
|
||||
|
||||
if (protectedKeyIV) {
|
||||
localStorage.removeItem("protectedKeyIV");
|
||||
localStorage.setItem("protectedKeyIV", protectedKeyIV);
|
||||
}
|
||||
|
||||
if (protectedKeyTag) {
|
||||
localStorage.removeItem("protectedKeyTag");
|
||||
localStorage.setItem("protectedKeyTag", protectedKeyTag);
|
||||
}
|
||||
|
||||
if (publicKey) {
|
||||
localStorage.removeItem("publicKey");
|
||||
localStorage.setItem("publicKey", publicKey);
|
||||
}
|
||||
|
||||
if (encryptedPrivateKey) {
|
||||
localStorage.removeItem("encryptedPrivateKey");
|
||||
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
|
||||
}
|
||||
|
||||
if (iv) {
|
||||
localStorage.removeItem("iv");
|
||||
localStorage.setItem("iv", iv);
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
localStorage.removeItem("tag");
|
||||
localStorage.setItem("tag", tag);
|
||||
}
|
||||
|
||||
if (privateKey) {
|
||||
localStorage.removeItem("PRIVATE_KEY");
|
||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new Error(
|
||||
|
||||
@@ -3,14 +3,19 @@ import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export type CardTitleProps = {
|
||||
children: ReactNode;
|
||||
subTitle?: string;
|
||||
subTitle?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
||||
<div className={twMerge('px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', className)}>
|
||||
<div
|
||||
className={twMerge(
|
||||
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{subTitle && <p className="pt-0.5 text-sm font-normal text-gray-400">{subTitle}</p>}
|
||||
{subTitle && <p className="pt-2 text-sm font-normal text-gray-400">{subTitle}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useToggle } from '@app/hooks';
|
||||
import { Button } from '../Button';
|
||||
import { FormControl } from '../FormControl';
|
||||
import { Input } from '../Input';
|
||||
import { Modal, ModalContent } from '../Modal';
|
||||
import { Modal, ModalClose, ModalContent } from '../Modal';
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -64,9 +64,11 @@ export const DeleteActionModal = ({
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>{' '}
|
||||
</div>
|
||||
}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const inputVariants = cva(
|
||||
'input w-full py-2 text-gray-400 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',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
@@ -84,19 +84,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
|
||||
{leftIcon && <span className="absolute left-0 ml-2">{leftIcon}</span>}
|
||||
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
|
||||
<input
|
||||
{...props}
|
||||
required={isRequired}
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
className={twMerge(
|
||||
leftIcon ? 'pl-9' : 'pl-2.5',
|
||||
rightIcon ? 'pr-9' : 'pr-2.5',
|
||||
leftIcon ? 'pl-10' : 'pl-2.5',
|
||||
rightIcon ? 'pr-10' : 'pr-2.5',
|
||||
inputVariants({ className, isError, size, isRounded, variant })
|
||||
)}
|
||||
/>
|
||||
{rightIcon && <span className="absolute right-0 mr-2">{rightIcon}</span>}
|
||||
{rightIcon && <span className="absolute right-0 mr-3">{rightIcon}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IconButton } from '../IconButton';
|
||||
|
||||
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
||||
title?: ReactNode;
|
||||
subTitle?: string;
|
||||
subTitle?: ReactNode;
|
||||
footerContent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
@@ -5,13 +5,13 @@ import * as Popover from '@radix-ui/react-popover';
|
||||
type Props = {
|
||||
children: any;
|
||||
text: string;
|
||||
onChangeHandler: (value: string, position: number) => void;
|
||||
position: number;
|
||||
onChangeHandler: (value: string, id: string) => void;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type PopoverProps = Props;
|
||||
|
||||
export const PopoverObject = ({children, text, onChangeHandler, position}: Props) => (
|
||||
export const PopoverObject = ({children, text, onChangeHandler, id}: Props) => (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild className='data-[state=open]:outline data-[state=open]:outline-primary data-[state=closed]:hover:outline data-[state=closed]:hover:outline-mineshaft-400'>
|
||||
{children}
|
||||
@@ -26,7 +26,7 @@ export const PopoverObject = ({children, text, onChangeHandler, position}: Props
|
||||
<div className="flex flex-col pt-2 dark">
|
||||
<p className="text-bunker-200 text-[15px] leading-[0px] font-medium mb-5">Comment</p>
|
||||
<textarea
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
onChange={(e) => onChangeHandler(e.target.value, id)}
|
||||
// type={type}
|
||||
value={text}
|
||||
className='z-10 dark:[color-scheme:dark] peer h-[20rem] ph-no-capture bg-bunker-600 border border-mineshaft-500 rounded-md py-2.5 caret-bunker-200 text-sm px-2 w-full outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
|
||||
|
||||
@@ -14,18 +14,28 @@ type Props = {
|
||||
dropdownContainerClassName?: string;
|
||||
isLoading?: boolean;
|
||||
position?: 'item-aligned' | 'popper';
|
||||
isDisabled?: boolean;
|
||||
icon?: IconProp;
|
||||
};
|
||||
|
||||
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
||||
export type SelectProps = Omit<SelectPrimitive.SelectProps, 'disabled'> & Props;
|
||||
|
||||
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
(
|
||||
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
|
||||
{
|
||||
children,
|
||||
placeholder,
|
||||
className,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
dropdownContainerClassName,
|
||||
position,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<SelectPrimitive.Root {...props}>
|
||||
<SelectPrimitive.Root {...props} disabled={isDisabled}>
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
@@ -34,10 +44,10 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
className
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.Value placeholder={placeholder}>
|
||||
<SelectPrimitive.Value placeholder={placeholder}>
|
||||
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
||||
</SelectPrimitive.Value>
|
||||
{!props.disabled && (
|
||||
{!isDisabled && (
|
||||
<SelectPrimitive.Icon className="ml-3">
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||
</SelectPrimitive.Icon>
|
||||
@@ -46,7 +56,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
className={twMerge(
|
||||
'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
|
||||
'relative top-1 z-[100] overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md',
|
||||
dropdownContainerClassName
|
||||
)}
|
||||
position={position}
|
||||
@@ -89,11 +99,12 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
<SelectPrimitive.Item
|
||||
{...props}
|
||||
className={twMerge(
|
||||
`relative flex cursor-pointer
|
||||
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
|
||||
`relative mb-0.5 flex
|
||||
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
||||
outline-none transition-all hover:bg-mineshaft-500`,
|
||||
isSelected && 'bg-primary',
|
||||
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
|
||||
isDisabled &&
|
||||
'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
|
||||
className
|
||||
)}
|
||||
ref={forwardedRef}
|
||||
|
||||
159
frontend/src/components/v2/Tabs/Tabs.tsx
Normal file
159
frontend/src/components/v2/Tabs/Tabs.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useState } from 'react';
|
||||
import { faArrowUpRightFromSquare, faCheck, faClipboard } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
|
||||
|
||||
export type TabsProps = Tabs.TabsProps & {};
|
||||
|
||||
function copyToClipboard(id: string, setState: (value: boolean) => void) {
|
||||
// Get the text field
|
||||
const copyText = document.getElementById(id) as HTMLInputElement;
|
||||
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setState(true);
|
||||
setTimeout(() => setState(false), 2000);
|
||||
// Alert the copied text
|
||||
// alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
|
||||
const CodeItem = ({isCopied, setIsCopied, textExplanation, code, id}: {isCopied: boolean, setIsCopied: (value: boolean) => void, textExplanation: string, code: string, id: string}) => {
|
||||
return <>
|
||||
<p className="mb-2 mt-4 text-bunker-300 text-sm leading-normal">
|
||||
{textExplanation}
|
||||
</p>
|
||||
<div className='font-mono text-sm px-3 py-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
|
||||
<input disabled value={code} id={id} className='w-full bg-transparent text-bunker-200'/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(id, setIsCopied)}
|
||||
className='h-full pl-3.5 pr-2 text-bunker-300 hover:text-primary-200 duration-200'
|
||||
>
|
||||
{isCopied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='pr-0.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faClipboard} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export const TabsObject = () => {
|
||||
const [downloadCodeCopied, setDownloadCodeCopied] = useState(false);
|
||||
const [downloadCode2Copied, setDownloadCode2Copied] = useState(false);
|
||||
const [loginCodeCopied, setLoginCodeCopied] = useState(false);
|
||||
const [initCodeCopied, setInitCodeCopied] = useState(false);
|
||||
const [runCodeCopied, setRunCodeCopied] = useState(false);
|
||||
|
||||
return <Tabs.Root
|
||||
className="flex flex-col w-full cursor-default border border-mineshaft-600 rounded-md"
|
||||
defaultValue="tab1"
|
||||
>
|
||||
<Tabs.List className="shrink-0 flex border-b border-mineshaft-600" aria-label="Manage your account">
|
||||
<Tabs.Trigger
|
||||
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
value="tab1"
|
||||
>
|
||||
MacOS
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
value="tab2"
|
||||
>
|
||||
Windows
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
value="tab3"
|
||||
>
|
||||
Arch Linux
|
||||
</Tabs.Trigger>
|
||||
<a
|
||||
target='_blank'
|
||||
rel="noopener noreferrer"
|
||||
className="bg-bunker-700 hover:text-bunker-100 duration-200 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
href='https://infisical.com/docs/cli/overview'
|
||||
>
|
||||
Other Platforms <FontAwesomeIcon icon={faArrowUpRightFromSquare} className='ml-2'/>
|
||||
</a>
|
||||
</Tabs.List>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
value="tab1"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCode" />
|
||||
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCode" />
|
||||
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCode" />
|
||||
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCode" />
|
||||
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
|
||||
className='text-primary underline underline-offset-2'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href='https://infisical.com/docs/integrations/overview'
|
||||
>
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none"
|
||||
value="tab2"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeW" />
|
||||
<div className='font-mono text-sm px-3 py-2 mt-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
|
||||
<input disabled value="scoop install infisical" id="downloadCodeW2" className='w-full bg-transparent text-bunker-200'/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard("downloadCodeW2", setDownloadCode2Copied)}
|
||||
className='h-full pl-3.5 pr-2 text-bunker-300 hover:text-primary-200 duration-200'
|
||||
>
|
||||
{downloadCode2Copied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='pr-0.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faClipboard} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCodeW" />
|
||||
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCodeW" />
|
||||
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCodeW" />
|
||||
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
|
||||
className='text-primary underline underline-offset-2'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href='https://infisical.com/docs/integrations/overview'
|
||||
>
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
value="tab3"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeL" />
|
||||
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCodeL" />
|
||||
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCodeL" />
|
||||
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCodeL" />
|
||||
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
|
||||
className='text-primary underline underline-offset-2'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href='https://infisical.com/docs/integrations/overview'
|
||||
>
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user