mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
@ -13,7 +13,9 @@ import { apiLimiter } from './helpers/rateLimiter';
|
||||
|
||||
import {
|
||||
workspace as eeWorkspaceRouter,
|
||||
secret as eeSecretRouter
|
||||
secret as eeSecretRouter,
|
||||
secretSnapshot as eeSecretSnapshotRouter,
|
||||
action as eeActionRouter
|
||||
} from './ee/routes/v1';
|
||||
import {
|
||||
signup as v1SignupRouter,
|
||||
@ -70,7 +72,9 @@ if (NODE_ENV === 'production') {
|
||||
|
||||
// (EE) routes
|
||||
app.use('/api/v1/secret', eeSecretRouter);
|
||||
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
|
||||
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||
app.use('/api/v1/action', eeActionRouter);
|
||||
|
||||
// v1 routes
|
||||
app.use('/api/v1/signup', v1SignupRouter);
|
||||
|
@ -123,7 +123,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
secrets = await pull({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
environment
|
||||
environment,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
key = await Key.findOne({
|
||||
@ -188,7 +190,9 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
secrets = await pull({
|
||||
userId: req.serviceToken.user._id.toString(),
|
||||
workspaceId,
|
||||
environment
|
||||
environment,
|
||||
channel: 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
key = {
|
||||
|
@ -2,7 +2,7 @@ import to from "await-to-js";
|
||||
import { Request, Response } from "express";
|
||||
import mongoose, { Types } from "mongoose";
|
||||
import Secret, { ISecret } from "../../models/secret";
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret/types";
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret";
|
||||
const { ValidationError } = mongoose.Error;
|
||||
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||
import { AnyBulkWriteOperation } from 'mongodb';
|
||||
|
@ -11,10 +11,6 @@ import {
|
||||
ServiceToken,
|
||||
ServiceTokenData
|
||||
} from '../../models';
|
||||
import {
|
||||
createWorkspace as create,
|
||||
deleteWorkspace as deleteWork
|
||||
} from '../../helpers/workspace';
|
||||
import {
|
||||
v2PushSecrets as push,
|
||||
pullSecrets as pull,
|
||||
@ -50,7 +46,6 @@ interface V2PushSecret {
|
||||
*/
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
|
||||
try {
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
@ -70,7 +65,9 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
secrets,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
await pushKeys({
|
||||
@ -136,7 +133,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
secrets = await pull({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
environment,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
if (channel !== 'cli') {
|
||||
@ -196,7 +195,7 @@ export const getWorkspaceServiceTokenData = async (
|
||||
) => {
|
||||
let serviceTokenData;
|
||||
try {
|
||||
const { workspaceId } = req.query;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.find({
|
||||
|
31
backend/src/ee/controllers/v1/actionController.ts
Normal file
31
backend/src/ee/controllers/v1/actionController.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Action, SecretVersion } from '../../models';
|
||||
import { ActionNotFoundError } from '../../../utils/errors';
|
||||
|
||||
export const getAction = async (req: Request, res: Response) => {
|
||||
let action;
|
||||
try {
|
||||
const { actionId } = req.params;
|
||||
|
||||
action = await Action
|
||||
.findById(actionId)
|
||||
.populate([
|
||||
'payload.secretVersions.oldSecretVersion',
|
||||
'payload.secretVersions.newSecretVersion'
|
||||
]);
|
||||
|
||||
if (!action) throw ActionNotFoundError({
|
||||
message: 'Failed to find action'
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
throw ActionNotFoundError({
|
||||
message: 'Failed to find action'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
action
|
||||
});
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
import * as stripeController from './stripeController';
|
||||
import * as secretController from './secretController';
|
||||
import * as secretSnapshotController from './secretSnapshotController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
import * as actionController from './actionController';
|
||||
|
||||
export {
|
||||
stripeController,
|
||||
secretController,
|
||||
workspaceController
|
||||
secretSnapshotController,
|
||||
workspaceController,
|
||||
actionController
|
||||
}
|
@ -18,6 +18,7 @@ import { SecretVersion } from '../../models';
|
||||
secretVersions = await SecretVersion.find({
|
||||
secret: secretId
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
|
27
backend/src/ee/controllers/v1/secretSnapshotController.ts
Normal file
27
backend/src/ee/controllers/v1/secretSnapshotController.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretSnapshot } from '../../models';
|
||||
|
||||
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||
let secretSnapshot;
|
||||
try {
|
||||
const { secretSnapshotId } = req.params;
|
||||
|
||||
secretSnapshot = await SecretSnapshot
|
||||
.findById(secretSnapshotId)
|
||||
.populate('secretVersions');
|
||||
|
||||
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get secret snapshot'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secretSnapshot
|
||||
});
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
import { Request, Response } from 'express';
|
||||
import e, { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretSnapshot } from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
Log
|
||||
} from '../../models';
|
||||
|
||||
/**
|
||||
* Return secret snapshots for workspace with id [workspaceId]
|
||||
@ -18,6 +21,7 @@ import { SecretSnapshot } from '../../models';
|
||||
secretSnapshots = await SecretSnapshot.find({
|
||||
workspace: workspaceId
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
@ -32,4 +36,77 @@ import { SecretSnapshot } from '../../models';
|
||||
return res.status(200).send({
|
||||
secretSnapshots
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return count of secret snapshots for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
|
||||
let count;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
count = await SecretSnapshot.countDocuments({
|
||||
workspace: workspaceId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to count number of secret snapshots'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
count
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return (audit) logs for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
||||
let logs
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
const sortBy: string = req.query.sortBy as string;
|
||||
const userId: string = req.query.userId as string;
|
||||
const actionNames: string = req.query.actionNames as string;
|
||||
|
||||
logs = await Log.find({
|
||||
workspace: workspaceId,
|
||||
...( userId ? { user: userId } : {}),
|
||||
...(
|
||||
actionNames
|
||||
? {
|
||||
actionNames: {
|
||||
$in: actionNames.split(',')
|
||||
}
|
||||
} : {}
|
||||
)
|
||||
})
|
||||
.sort({ createdAt: sortBy === 'recent' ? -1 : 1 })
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.populate('actions')
|
||||
.populate('user');
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace logs'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
logs
|
||||
});
|
||||
}
|
112
backend/src/ee/helpers/action.ts
Normal file
112
backend/src/ee/helpers/action.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Secret } from '../../models';
|
||||
import { SecretVersion, Action } from '../models';
|
||||
import { ACTION_UPDATE_SECRETS } from '../../variables';
|
||||
|
||||
/**
|
||||
* Create an (audit) action for secrets including
|
||||
* add, delete, update, and read actions.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {ObjectId[]} obj.secretIds - ids of relevant secrets
|
||||
* @returns {Action} action - new action
|
||||
*/
|
||||
const createActionSecretHelper = async ({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
|
||||
let action;
|
||||
let latestSecretVersions;
|
||||
try {
|
||||
if (name === ACTION_UPDATE_SECRETS) {
|
||||
// case: action is updating secrets
|
||||
// -> add old and new secret versions
|
||||
|
||||
// TODO: make query more efficient
|
||||
latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 },
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
versions: { $push: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
secret: "$_id",
|
||||
versions: { $slice: ["$versions", 2] },
|
||||
},
|
||||
}
|
||||
]))
|
||||
.map((s) => ({
|
||||
oldSecretVersion: s.versions[0]._id,
|
||||
newSecretVersion: s.versions[1]._id
|
||||
}));
|
||||
|
||||
|
||||
} else {
|
||||
// case: action is adding, deleting, or reading secrets
|
||||
// -> add new secret versions
|
||||
latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$secret',
|
||||
version: { $max: '$version' },
|
||||
versionId: { $max: '$_id' } // secret version id
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 }
|
||||
}
|
||||
])
|
||||
.exec())
|
||||
.map((s) => ({
|
||||
newSecretVersion: s.versionId
|
||||
}));
|
||||
}
|
||||
|
||||
action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
}
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create action');
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
export { createActionSecretHelper };
|
41
backend/src/ee/helpers/log.ts
Normal file
41
backend/src/ee/helpers/log.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Log,
|
||||
IAction
|
||||
} from '../models';
|
||||
|
||||
const createLogHelper = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}) => {
|
||||
let log;
|
||||
try {
|
||||
log = await new Log({
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
actionNames: actions.map((a) => a.name),
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create log');
|
||||
}
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
export {
|
||||
createLogHelper
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret
|
||||
Secret,
|
||||
ISecret
|
||||
} from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
@ -9,66 +11,159 @@ import {
|
||||
} from '../models';
|
||||
|
||||
/**
|
||||
* Save a copy of the current state of secrets in workspace with id
|
||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* secretsnapshots collection.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||
*/
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
|
||||
let secretSnapshot;
|
||||
try {
|
||||
const secrets = await Secret.find({
|
||||
const secretIds = (await Secret.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
}, '_id')).map((s) => s._id);
|
||||
|
||||
const latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$secret',
|
||||
version: { $max: '$version' },
|
||||
versionId: { $max: '$_id' } // secret version id
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 }
|
||||
}
|
||||
])
|
||||
.exec())
|
||||
.map((s) => s.versionId);
|
||||
|
||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId
|
||||
}).sort({ version: -1 });
|
||||
|
||||
if (!latestSecretSnapshot) {
|
||||
// case: no snapshots exist for workspace -> create first snapshot
|
||||
await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: 1,
|
||||
secrets
|
||||
}).save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// case: snapshots exist for workspace
|
||||
await new SecretSnapshot({
|
||||
secretSnapshot = await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: latestSecretSnapshot.version + 1,
|
||||
secrets
|
||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||
secretVersions: latestSecretVersions
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to take a secret snapshot');
|
||||
}
|
||||
|
||||
return secretSnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||
* @param {Object} obj
|
||||
* @param {Object[]} obj.secretVersions
|
||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||
*/
|
||||
const addSecretVersionsHelper = async ({
|
||||
secretVersions
|
||||
}: {
|
||||
secretVersions: ISecretVersion[]
|
||||
}) => {
|
||||
let newSecretVersions;
|
||||
try {
|
||||
await SecretVersion.insertMany(secretVersions);
|
||||
newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to add secret versions');
|
||||
}
|
||||
|
||||
return newSecretVersions;
|
||||
}
|
||||
|
||||
const markDeletedSecretVersionsHelper = async ({
|
||||
secretIds
|
||||
}: {
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
try {
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: secretIds }
|
||||
}, {
|
||||
isDeleted: true
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to mark secret versions as deleted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize secret versioning by setting previously unversioned
|
||||
* secrets to version 1 and begin populating secret versions.
|
||||
*/
|
||||
const initSecretVersioningHelper = async () => {
|
||||
try {
|
||||
|
||||
await Secret.updateMany(
|
||||
{ version: { $exists: false } },
|
||||
{ $set: { version: 1 } }
|
||||
);
|
||||
|
||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||
{
|
||||
$lookup: {
|
||||
from: 'secretversions',
|
||||
localField: '_id',
|
||||
foreignField: 'secret',
|
||||
as: 'versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
versions: { $size: 0 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (unversionedSecrets.length > 0) {
|
||||
await addSecretVersionsHelper({
|
||||
secretVersions: unversionedSecrets.map((s, idx) => ({
|
||||
...s,
|
||||
secret: s._id,
|
||||
version: s.version ? s.version : 1,
|
||||
isDeleted: false,
|
||||
workspace: s.workspace,
|
||||
environment: s.environment
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to ensure that secrets are versioned');
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
addSecretVersionsHelper,
|
||||
markDeletedSecretVersionsHelper,
|
||||
initSecretVersioningHelper
|
||||
}
|
7
backend/src/ee/middleware/index.ts
Normal file
7
backend/src/ee/middleware/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import requireLicenseAuth from './requireLicenseAuth';
|
||||
import requireSecretSnapshotAuth from './requireSecretSnapshotAuth';
|
||||
|
||||
export {
|
||||
requireLicenseAuth,
|
||||
requireSecretSnapshotAuth
|
||||
}
|
47
backend/src/ee/middleware/requireSecretSnapshotAuth.ts
Normal file
47
backend/src/ee/middleware/requireSecretSnapshotAuth.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors';
|
||||
import { SecretSnapshot } from '../models';
|
||||
import {
|
||||
validateMembership
|
||||
} from '../../helpers/membership';
|
||||
|
||||
/**
|
||||
* Validate if user on request has proper membership for secret snapshot
|
||||
* @param {Object} obj
|
||||
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
|
||||
*/
|
||||
const requireSecretSnapshotAuth = ({
|
||||
acceptedRoles,
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { secretSnapshotId } = req.params;
|
||||
|
||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
||||
|
||||
if (!secretSnapshot) {
|
||||
return next(SecretSnapshotNotFoundError({
|
||||
message: 'Failed to find secret snapshot'
|
||||
}));
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: secretSnapshot.workspace.toString(),
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
req.secretSnapshot = secretSnapshot as any;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default requireSecretSnapshotAuth;
|
46
backend/src/ee/models/action.ts
Normal file
46
backend/src/ee/models/action.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IAction {
|
||||
name: string;
|
||||
user?: Types.ObjectId,
|
||||
workspace?: Types.ObjectId,
|
||||
payload: {
|
||||
secretVersions?: Types.ObjectId[]
|
||||
}
|
||||
}
|
||||
|
||||
const actionSchema = new Schema<IAction>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace'
|
||||
},
|
||||
payload: {
|
||||
secretVersions: [{
|
||||
oldSecretVersion: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'SecretVersion'
|
||||
},
|
||||
newSecretVersion: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'SecretVersion'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const Action = model<IAction>('Action', actionSchema);
|
||||
|
||||
export default Action;
|
@ -1,9 +1,15 @@
|
||||
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
|
||||
import SecretVersion, { ISecretVersion } from "./secretVersion";
|
||||
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
|
||||
import SecretVersion, { ISecretVersion } from './secretVersion';
|
||||
import Log, { ILog } from './log';
|
||||
import Action, { IAction } from './action';
|
||||
|
||||
export {
|
||||
SecretSnapshot,
|
||||
ISecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
ISecretVersion,
|
||||
Log,
|
||||
ILog,
|
||||
Action,
|
||||
IAction
|
||||
}
|
59
backend/src/ee/models/log.ts
Normal file
59
backend/src/ee/models/log.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
ACTION_DELETE_SECRETS
|
||||
} from '../../variables';
|
||||
|
||||
export interface ILog {
|
||||
_id: Types.ObjectId;
|
||||
user?: Types.ObjectId;
|
||||
workspace?: Types.ObjectId;
|
||||
actionNames: string[];
|
||||
actions: Types.ObjectId[];
|
||||
channel: string;
|
||||
ipAddress?: string;
|
||||
}
|
||||
|
||||
const logSchema = new Schema<ILog>(
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace'
|
||||
},
|
||||
actionNames: {
|
||||
type: [String],
|
||||
enum: [
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
ACTION_DELETE_SECRETS
|
||||
],
|
||||
required: true
|
||||
},
|
||||
actions: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Action',
|
||||
required: true
|
||||
}],
|
||||
channel: {
|
||||
type: String,
|
||||
enum: ['web', 'cli', 'auto'],
|
||||
required: true
|
||||
},
|
||||
ipAddress: {
|
||||
type: String
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const Log = model<ILog>('Log', logSchema);
|
||||
|
||||
export default Log;
|
@ -1,31 +1,9 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../../variables';
|
||||
|
||||
export interface ISecretSnapshot {
|
||||
workspace: Types.ObjectId;
|
||||
version: number;
|
||||
secrets: {
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
environment: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
}[]
|
||||
secretVersions: Types.ObjectId[];
|
||||
}
|
||||
|
||||
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||
@ -39,64 +17,10 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
secrets: [{
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
secretVersions: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'SecretVersion',
|
||||
required: true
|
||||
}]
|
||||
},
|
||||
{
|
||||
|
@ -1,9 +1,30 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../../variables';
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* 1. Modify SecretVersion to also contain XX
|
||||
* - type
|
||||
* - user
|
||||
* - environment
|
||||
* 2. Modify SecretSnapshot to point to arrays of SecretVersion
|
||||
*/
|
||||
|
||||
export interface ISecretVersion {
|
||||
_id?: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId; // new
|
||||
type: string; // new
|
||||
user: Types.ObjectId; // new
|
||||
environment: string; // new
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
@ -27,6 +48,26 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
17
backend/src/ee/routes/v1/action.ts
Normal file
17
backend/src/ee/routes/v1/action.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { param } from 'express-validator';
|
||||
import { actionController } from '../../controllers/v1';
|
||||
|
||||
// TODO: put into action controller
|
||||
router.get(
|
||||
'/:actionId',
|
||||
param('actionId').exists().trim(),
|
||||
validateRequest,
|
||||
actionController.getAction
|
||||
);
|
||||
|
||||
export default router;
|
@ -1,7 +1,11 @@
|
||||
import secret from './secret';
|
||||
import secretSnapshot from './secretSnapshot';
|
||||
import workspace from './workspace';
|
||||
import action from './action';
|
||||
|
||||
export {
|
||||
secret,
|
||||
workspace
|
||||
secretSnapshot,
|
||||
workspace,
|
||||
action
|
||||
}
|
27
backend/src/ee/routes/v1/secretSnapshot.ts
Normal file
27
backend/src/ee/routes/v1/secretSnapshot.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireSecretSnapshotAuth
|
||||
} from '../../middleware';
|
||||
import {
|
||||
requireAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { param } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../../variables';
|
||||
import { secretSnapshotController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/:secretSnapshotId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireSecretSnapshotAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('secretSnapshotId').exists().trim(),
|
||||
validateRequest,
|
||||
secretSnapshotController.getSecretSnapshot
|
||||
);
|
||||
|
||||
export default router;
|
@ -24,4 +24,35 @@ router.get(
|
||||
workspaceController.getWorkspaceSecretSnapshots
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/secret-snapshots/count',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceSecretSnapshotsCount
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/logs',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
query('limit').exists().isInt(),
|
||||
query('sortBy'),
|
||||
query('userId'),
|
||||
query('actionNames'),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceLogs
|
||||
);
|
||||
|
||||
export default router;
|
81
backend/src/ee/services/EELogService.ts
Normal file
81
backend/src/ee/services/EELogService.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Log,
|
||||
Action,
|
||||
IAction
|
||||
} from '../models';
|
||||
import {
|
||||
createLogHelper
|
||||
} from '../helpers/log';
|
||||
import {
|
||||
createActionSecretHelper
|
||||
} from '../helpers/action';
|
||||
import EELicenseService from './EELicenseService';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition log actions
|
||||
*/
|
||||
class EELogService {
|
||||
/**
|
||||
* Create an (audit) log
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId - id of user associated with the log
|
||||
* @param {String} obj.workspaceId - id of workspace associated with the log
|
||||
* @param {Action} obj.actions - actions to include in log
|
||||
* @param {String} obj.channel - channel (web/cli/auto) associated with the log
|
||||
* @param {String} obj.ipAddress - ip address associated with the log
|
||||
* @returns {Log} log - new audit log
|
||||
*/
|
||||
static async createLog({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return null;
|
||||
return await createLogHelper({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an (audit) action for secrets including
|
||||
* add, delete, update, and read actions.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||
* @returns {Action} action - new action
|
||||
*/
|
||||
static async createActionSecret({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return null;
|
||||
return await createActionSecretHelper({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EELogService;
|
@ -1,7 +1,10 @@
|
||||
import { Types } from 'mongoose';
|
||||
import { ISecretVersion } from '../models';
|
||||
import {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
addSecretVersionsHelper,
|
||||
markDeletedSecretVersionsHelper,
|
||||
initSecretVersioningHelper
|
||||
} from '../helpers/secret';
|
||||
import EELicenseService from './EELicenseService';
|
||||
|
||||
@ -11,12 +14,13 @@ import EELicenseService from './EELicenseService';
|
||||
class EESecretService {
|
||||
|
||||
/**
|
||||
* Save a copy of the current state of secrets in workspace with id
|
||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* SecretSnapshot collection.
|
||||
* Requires a valid license key [licenseKey]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
|
||||
*/
|
||||
static async takeSecretSnapshot({
|
||||
workspaceId
|
||||
@ -24,13 +28,14 @@ class EESecretService {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await takeSecretSnapshotHelper({ workspaceId });
|
||||
return await takeSecretSnapshotHelper({ workspaceId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds secret versions [secretVersions] to the SecretVersion collection.
|
||||
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||
* @param {Object} obj
|
||||
* @param {SecretVersion} obj.secretVersions
|
||||
* @param {Object[]} obj.secretVersions
|
||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||
*/
|
||||
static async addSecretVersions({
|
||||
secretVersions
|
||||
@ -38,10 +43,36 @@ class EESecretService {
|
||||
secretVersions: ISecretVersion[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await addSecretVersionsHelper({
|
||||
return await addSecretVersionsHelper({
|
||||
secretVersions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark secret versions associated with secrets with ids [secretIds]
|
||||
* as deleted.
|
||||
* @param {Object} obj
|
||||
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||
*/
|
||||
static async markDeletedSecretVersions({
|
||||
secretIds
|
||||
}: {
|
||||
secretIds: Types.ObjectId[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await markDeletedSecretVersionsHelper({
|
||||
secretIds
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize secret versioning by setting previously unversioned
|
||||
* secrets to version 1 and begin populating secret versions.
|
||||
*/
|
||||
static async initSecretVersioning() {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await initSecretVersioningHelper();
|
||||
}
|
||||
}
|
||||
|
||||
export default EESecretService;
|
@ -1,7 +1,9 @@
|
||||
import EELicenseService from "./EELicenseService";
|
||||
import EESecretService from "./EESecretService";
|
||||
import EELogService from "./EELogService";
|
||||
|
||||
export {
|
||||
EELicenseService,
|
||||
EESecretService
|
||||
EESecretService,
|
||||
EELogService
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||
import {
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@ -19,7 +22,7 @@ interface PushSecret {
|
||||
* @returns
|
||||
*/
|
||||
const eventPushSecrets = ({
|
||||
workspaceId,
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
@ -32,6 +35,26 @@ const eventPushSecrets = ({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return event for pulling secrets
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace to pull secrets from
|
||||
* @returns
|
||||
*/
|
||||
const eventPullSecrets = ({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
return ({
|
||||
name: EVENT_PULL_SECRETS,
|
||||
workspaceId,
|
||||
payload: {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
eventPushSecrets
|
||||
}
|
||||
|
31
backend/src/helpers/database.ts
Normal file
31
backend/src/helpers/database.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { ISecret, Secret } from '../models';
|
||||
import { EESecretService } from '../ee/services';
|
||||
import { getLogger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* Initialize database connection
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.mongoURL - mongo connection string
|
||||
* @returns
|
||||
*/
|
||||
const initDatabaseHelper = async ({
|
||||
mongoURL
|
||||
}: {
|
||||
mongoURL: string;
|
||||
}) => {
|
||||
try {
|
||||
await mongoose.connect(mongoURL);
|
||||
getLogger("database").info("Database connection established");
|
||||
|
||||
await EESecretService.initSecretVersioning();
|
||||
} catch (err) {
|
||||
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
|
||||
}
|
||||
|
||||
return mongoose.connection;
|
||||
}
|
||||
|
||||
export {
|
||||
initDatabaseHelper
|
||||
}
|
@ -1,19 +1,24 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Secret,
|
||||
ISecret,
|
||||
} from '../models';
|
||||
import {
|
||||
EESecretService
|
||||
EESecretService,
|
||||
EELogService
|
||||
} from '../ee/services';
|
||||
import {
|
||||
SecretVersion
|
||||
IAction
|
||||
} from '../ee/models';
|
||||
import {
|
||||
takeSecretSnapshotHelper
|
||||
} from '../ee/helpers/secret';
|
||||
import { decryptSymmetric } from '../utils/crypto';
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_READ_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
interface V1PushSecret {
|
||||
ciphertextKey: string;
|
||||
@ -51,8 +56,6 @@ interface Update {
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
type DecryptSecretType = 'text' | 'object' | 'expanded';
|
||||
|
||||
/**
|
||||
* Push secrets for user with id [userId] to workspace
|
||||
* with id [workspaceId] with environment [environment]. Follow steps:
|
||||
@ -68,7 +71,7 @@ const v1PushSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
secrets,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
@ -78,7 +81,7 @@ const v1PushSecrets = async ({
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
const oldSecrets = await getSecrets({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
@ -101,11 +104,9 @@ const v1PushSecrets = async ({
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: toDelete
|
||||
});
|
||||
}
|
||||
|
||||
@ -188,6 +189,10 @@ const v1PushSecrets = async ({
|
||||
return ({
|
||||
secret: _id,
|
||||
version: version ? version + 1 : 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type: newSecret.type,
|
||||
user: new Types.ObjectId(userId),
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext: newSecret.ciphertextKey,
|
||||
secretKeyIV: newSecret.ivKey,
|
||||
@ -239,6 +244,11 @@ const v1PushSecrets = async ({
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
@ -249,7 +259,11 @@ const v1PushSecrets = async ({
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
@ -284,22 +298,30 @@ const v1PushSecrets = async ({
|
||||
* @param {String} obj.workspaceId - id of workspace to push to
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @param {Object[]} obj.secrets - secrets to push
|
||||
* @param {String} obj.channel - channel (web/cli/auto)
|
||||
* @param {String} obj.ipAddress - ip address of request to push secrets
|
||||
*/
|
||||
const v2PushSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
secrets,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secrets: V2PushSecret[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}): Promise<void> => {
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
const actions: IAction[] = [];
|
||||
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
const oldSecrets = await getSecrets({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
@ -322,12 +344,19 @@ const v1PushSecrets = async ({
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: toDelete
|
||||
});
|
||||
|
||||
const deleteAction = await EELogService.createActionSecret({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds: toDelete
|
||||
});
|
||||
|
||||
deleteAction && actions.push(deleteAction);
|
||||
}
|
||||
|
||||
const toUpdate = oldSecrets
|
||||
@ -348,118 +377,10 @@ const v1PushSecrets = async ({
|
||||
return false;
|
||||
});
|
||||
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
const update: Update = {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
// case: (legacy) secret was not versioned
|
||||
update.version = 1;
|
||||
} else {
|
||||
update['$inc'] = {
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (s.type === SECRET_PERSONAL) {
|
||||
// attach user associated with the personal secret
|
||||
update['user'] = userId;
|
||||
}
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||
},
|
||||
update
|
||||
}
|
||||
};
|
||||
});
|
||||
await Secret.bulkWrite(operations as any);
|
||||
|
||||
// (EE) add secret versions for updated secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: toUpdate.map((s) => {
|
||||
const {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
return ({
|
||||
secret: s._id,
|
||||
version: s.version ? s.version + 1 : 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// handle adding new secrets
|
||||
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map(({
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
}, idx) => {
|
||||
const obj: any = {
|
||||
version: 1,
|
||||
workspace: workspaceId,
|
||||
type: toAdd[idx].type,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
if (toUpdate.length > 0) {
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
@ -467,49 +388,120 @@ const v1PushSecrets = async ({
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash
|
||||
};
|
||||
secretCommentHash,
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
if (toAdd[idx].type === 'personal') {
|
||||
obj['user' as keyof typeof obj] = userId;
|
||||
const update: Update = {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
}
|
||||
|
||||
return obj;
|
||||
})
|
||||
if (!s.version) {
|
||||
// case: (legacy) secret was not versioned
|
||||
update.version = 1;
|
||||
} else {
|
||||
update['$inc'] = {
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (s.type === SECRET_PERSONAL) {
|
||||
// attach user associated with the personal secret
|
||||
update['user'] = userId;
|
||||
}
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||
},
|
||||
update
|
||||
}
|
||||
};
|
||||
});
|
||||
await Secret.bulkWrite(operations as any);
|
||||
|
||||
// (EE) add secret versions for updated secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: toUpdate.map((s) => {
|
||||
return ({
|
||||
...newSecretsObj[`${s.type}-${s.secretKeyHash}`],
|
||||
secret: s._id,
|
||||
version: s.version ? s.version + 1 : 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
user: s.user,
|
||||
environment: s.environment,
|
||||
isDeleted: false
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const updateAction = await EELogService.createActionSecret({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds: toUpdate.map((u) => u._id)
|
||||
});
|
||||
|
||||
updateAction && actions.push(updateAction);
|
||||
}
|
||||
|
||||
// handle adding new secrets
|
||||
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map((s, idx) => ({
|
||||
...s,
|
||||
version: 1,
|
||||
workspace: workspaceId,
|
||||
type: toAdd[idx].type,
|
||||
environment,
|
||||
...( toAdd[idx].type === 'personal' ? { user: userId } : {})
|
||||
}))
|
||||
);
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
secretVersions: newSecrets.map((secretDocument) => {
|
||||
return {
|
||||
...secretDocument.toObject(),
|
||||
secret: secretDocument._id,
|
||||
isDeleted: false
|
||||
}})
|
||||
});
|
||||
|
||||
const addAction = await EELogService.createActionSecret({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds: newSecrets.map((n) => n._id)
|
||||
});
|
||||
addAction && actions.push(addAction);
|
||||
}
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
})
|
||||
|
||||
// (EE) create (audit) log
|
||||
if (actions.length > 0) {
|
||||
await EELogService.createLog({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@ -518,15 +510,14 @@ const v1PushSecrets = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Pull secrets for user with id [userId] for workspace
|
||||
* Get secrets for user with id [userId] for workspace
|
||||
* with id [workspaceId] with environment [environment]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId -id of user to pull secrets for
|
||||
* @param {String} obj.workspaceId - id of workspace to pull from
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
*
|
||||
*/
|
||||
const pullSecrets = async ({
|
||||
const getSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
@ -563,9 +554,64 @@ const pullSecrets = async ({
|
||||
return secrets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pull secrets for user with id [userId] for workspace
|
||||
* with id [workspaceId] with environment [environment]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId -id of user to pull secrets for
|
||||
* @param {String} obj.workspaceId - id of workspace to pull from
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @param {String} obj.channel - channel (web/cli/auto)
|
||||
* @param {String} obj.ipAddress - ip address of request to push secrets
|
||||
*/
|
||||
const pullSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}): Promise<ISecret[]> => {
|
||||
let secrets: any;
|
||||
|
||||
try {
|
||||
secrets = await getSecrets({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
})
|
||||
|
||||
const readAction = await EELogService.createActionSecret({
|
||||
name: ACTION_READ_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds: secrets.map((n: any) => n._id)
|
||||
});
|
||||
|
||||
readAction && await EELogService.createLog({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions: [readAction],
|
||||
channel,
|
||||
ipAddress
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to pull shared and personal secrets');
|
||||
}
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reformat output of pullSecrets() to be compatible with how existing
|
||||
* clients handle secrets
|
||||
* web client handle secrets
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secrets
|
||||
*/
|
||||
|
@ -4,12 +4,12 @@ dotenv.config();
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
|
||||
import { server } from './app';
|
||||
import { initDatabase } from './services/database';
|
||||
import { DatabaseService } from './services';
|
||||
import { setUpHealthEndpoint } from './services/health';
|
||||
import { initSmtp } from './services/smtp';
|
||||
import { setTransporter } from './helpers/nodemailer';
|
||||
|
||||
initDatabase(MONGO_URL);
|
||||
DatabaseService.initDatabase(MONGO_URL);
|
||||
|
||||
setUpHealthEndpoint(server);
|
||||
|
||||
|
@ -11,7 +11,7 @@ export interface IUser {
|
||||
tag?: string;
|
||||
salt?: string;
|
||||
verifier?: string;
|
||||
refreshVersion?: Number;
|
||||
refreshVersion?: number;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
@ -52,7 +52,8 @@ const userSchema = new Schema<IUser>(
|
||||
},
|
||||
refreshVersion: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
select: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { body, query } from 'express-validator';
|
||||
import { userActionController } from '../../controllers/v1';
|
||||
|
||||
// note: [userAction] will be deprecated in /v2 in favor of [action]
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
|
@ -2,7 +2,7 @@ import express, { Request, Response } from 'express';
|
||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types';
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
||||
import { secretController } from '../../controllers/v2';
|
||||
import { fetchAllSecrets } from '../../controllers/v2/secretController';
|
||||
|
||||
|
16
backend/src/services/DatabaseService.ts
Normal file
16
backend/src/services/DatabaseService.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { getLogger } from '../utils/logger';
|
||||
import { initDatabaseHelper } from '../helpers/database';
|
||||
|
||||
/**
|
||||
* Class to handle database actions
|
||||
*/
|
||||
class DatabaseService {
|
||||
static async initDatabase(MONGO_URL: string) {
|
||||
return await initDatabaseHelper({
|
||||
mongoURL: MONGO_URL
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseService;
|
@ -1,10 +0,0 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { getLogger } from '../utils/logger';
|
||||
|
||||
export const initDatabase = (MONGO_URL: string) => {
|
||||
mongoose
|
||||
.connect(MONGO_URL)
|
||||
.then(() => getLogger("database").info("Database connection established"))
|
||||
.catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`));
|
||||
return mongoose.connection;
|
||||
};
|
@ -1,9 +1,11 @@
|
||||
import DatabaseService from './DatabaseService';
|
||||
import postHogClient from './PostHogClient';
|
||||
import BotService from './BotService';
|
||||
import EventService from './EventService';
|
||||
import IntegrationService from './IntegrationService';
|
||||
|
||||
export {
|
||||
DatabaseService,
|
||||
postHogClient,
|
||||
BotService,
|
||||
EventService,
|
||||
|
1
backend/src/types/express/index.d.ts
vendored
1
backend/src/types/express/index.d.ts
vendored
@ -13,6 +13,7 @@ declare global {
|
||||
integrationAuth: any;
|
||||
bot: any;
|
||||
secret: any;
|
||||
secretSnapshot: any;
|
||||
serviceToken: any;
|
||||
accessToken: any;
|
||||
serviceTokenData: any;
|
||||
|
@ -123,6 +123,16 @@ export const SecretNotFoundError = (error?: Partial<RequestErrorContext>) => new
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
//* ----->[SECRET SNAPSHOT ERRORS]<-----
|
||||
export const SecretSnapshotNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? 'secret_snapshot_not_found_error',
|
||||
message: error?.message ?? 'The requested secret snapshot was not found',
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
//* ----->[ACTION ERRORS]<-----
|
||||
export const ActionNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
|
11
backend/src/variables/action.ts
Normal file
11
backend/src/variables/action.ts
Normal file
@ -0,0 +1,11 @@
|
||||
const ACTION_ADD_SECRETS = 'addSecrets';
|
||||
const ACTION_DELETE_SECRETS = 'deleteSecrets';
|
||||
const ACTION_UPDATE_SECRETS = 'updateSecrets';
|
||||
const ACTION_READ_SECRETS = 'readSecrets';
|
||||
|
||||
export {
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS
|
||||
}
|
@ -31,6 +31,12 @@ import {
|
||||
} from './organization';
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
|
||||
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
|
||||
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';
|
||||
|
||||
@ -63,6 +69,10 @@ export {
|
||||
INTEGRATION_GITHUB_API_URL,
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
INTEGRATION_OPTIONS,
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
|
9
docs/getting-started/dashboard/audit-logs.mdx
Normal file
9
docs/getting-started/dashboard/audit-logs.mdx
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Activity Logs"
|
||||
---
|
||||
|
||||
Activity logs record all actions going through Infisical including CRUD operations applied to environment variables. They help answer questions like:
|
||||
|
||||
- Who added or updated environment variables recently?
|
||||
- Did Bob read environment variables last week (if at all)?
|
||||
- What IP address was used for that action?
|
@ -4,11 +4,10 @@ title: "Integrations"
|
||||
|
||||
Integrations allow environment variables to be synced across your entire infrastructure from local development to CI/CD and production.
|
||||
|
||||
We're still early with integrations, but expect more soon.
|
||||
We're still early with integrations, but expect more soon.
|
||||
|
||||
<Card title="View integrations documentation" icon="link" href="/integrations/overview">
|
||||
View all available integrations and their guide
|
||||
<Card title="View integrations" icon="link" href="/integrations/overview">
|
||||
View all available integrations and their guides
|
||||
</Card>
|
||||
|
||||

|
||||
|
||||
|
5
docs/getting-started/dashboard/pit-recovery.mdx
Normal file
5
docs/getting-started/dashboard/pit-recovery.mdx
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Point-in-Time Recovery"
|
||||
---
|
||||
|
||||
Point-in-time (PIT) recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables.
|
5
docs/getting-started/dashboard/versioning.mdx
Normal file
5
docs/getting-started/dashboard/versioning.mdx
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Secret Versioning"
|
||||
---
|
||||
|
||||
Secret versioning allows an individual environment variable to be rolled back without touching other project environment variables.
|
@ -4,14 +4,14 @@ title: "Features"
|
||||
|
||||
This is a non-exhaustive list of features that Infisical offers:
|
||||
|
||||
## Web UI
|
||||
## Platform
|
||||
|
||||
The Web UI is used to manage teams and environment variables.
|
||||
|
||||
- Provision access to organizations and projects.
|
||||
- Add/delete/update, scope, search, sort, hide-unhide environment variables.
|
||||
- Separate environment variables by environment.
|
||||
- Import environment variables via drag-and-drop, export them as a .env file.
|
||||
- Provision members access to organizations and projects.
|
||||
- Manage secrets by adding, deleting, updating them across environments; search, sort, hide/un-hide, export/import them.
|
||||
- Sync secrets to platforms via integrations to platforms like GitHub, Vercel, and Netlify.
|
||||
- Rollback secrets to any point in time.
|
||||
- Rollback each secrets to any version.
|
||||
- Track actions through activity logs.
|
||||
|
||||
## CLI
|
||||
|
||||
|
@ -80,6 +80,9 @@
|
||||
"getting-started/dashboard/organization",
|
||||
"getting-started/dashboard/project",
|
||||
"getting-started/dashboard/integrations",
|
||||
"getting-started/dashboard/pit-recovery",
|
||||
"getting-started/dashboard/versioning",
|
||||
"getting-started/dashboard/audit-logs",
|
||||
"getting-started/dashboard/token"
|
||||
]
|
||||
},
|
||||
|
106
frontend/components/basic/EventFilter.tsx
Normal file
106
frontend/components/basic/EventFilter.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faAngleDown,
|
||||
faEye,
|
||||
faPlus,
|
||||
faShuffle,
|
||||
faTrash,
|
||||
faX
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
|
||||
interface ListBoxProps {
|
||||
selected: string;
|
||||
select: (event: string) => void;
|
||||
}
|
||||
|
||||
const eventOptions = [
|
||||
{
|
||||
name: 'addSecrets',
|
||||
icon: faPlus
|
||||
},
|
||||
{
|
||||
name: 'readSecrets',
|
||||
icon: faEye
|
||||
},
|
||||
{
|
||||
name: 'updateSecrets',
|
||||
icon: faShuffle
|
||||
},
|
||||
{
|
||||
name: 'deleteSecrets',
|
||||
icon: faTrash
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* This is the component that we use for the event picker in the activity logs tab.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.selected - the event that is currently selected
|
||||
* @param {function} obj.select - an action that happens when an item is selected
|
||||
*/
|
||||
export default function EventFilter({
|
||||
selected,
|
||||
select
|
||||
}: ListBoxProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Listbox value={t("activity:event." + selected)} onChange={select}>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
|
||||
{selected != '' ? (
|
||||
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
|
||||
) : (
|
||||
<p className="select-none">Select an event</p>
|
||||
)}
|
||||
{selected != '' ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className="pl-2 w-2 p-2"
|
||||
onClick={() => select('')}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
|
||||
)}
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{eventOptions.map((event, id) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={id}
|
||||
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
|
||||
selected == t("activity:event." + event.name) && 'bg-mineshaft-700'
|
||||
}`}
|
||||
value={event.name}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? 'font-semibold' : 'font-normal'
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
|
||||
{t("activity:event." + event.name)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
})}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
}
|
@ -6,10 +6,12 @@ import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faBookOpen,
|
||||
faFileLines,
|
||||
faGear,
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faTimeline,
|
||||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
@ -119,7 +121,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
}
|
||||
});
|
||||
}
|
||||
router.push("/dashboard/" + newWorkspaceId + "?Development");
|
||||
router.push("/dashboard/" + newWorkspaceId);
|
||||
setIsOpen(false);
|
||||
setNewWorkspaceName("");
|
||||
} else {
|
||||
@ -139,8 +141,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{
|
||||
href:
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any] +
|
||||
"?Development",
|
||||
workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.secrets"),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />,
|
||||
},
|
||||
@ -154,6 +155,11 @@ export default function Layout({ children }: LayoutProps) {
|
||||
title: t("nav:menu.integrations"),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />,
|
||||
},
|
||||
{
|
||||
href: '/activity/' + workspaceMapping[workspaceSelected as any],
|
||||
title: 'Activity Logs',
|
||||
emoji: <FontAwesomeIcon icon={faFileLines} />
|
||||
},
|
||||
{
|
||||
href: "/settings/project/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.project-settings"),
|
||||
@ -192,7 +198,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id + "?Development");
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
||||
} else {
|
||||
setWorkspaceList(
|
||||
userWorkspaces.map((workspace: any) => workspace.name)
|
||||
@ -235,8 +241,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
) {
|
||||
router.push(
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any] +
|
||||
"?Development"
|
||||
workspaceMapping[workspaceSelected as any]
|
||||
);
|
||||
localStorage.setItem(
|
||||
"projectData.id",
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import { Fragment } from "react";
|
||||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import {
|
||||
faAngleDown,
|
||||
faCheck,
|
||||
faPlus,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
faPlus
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
|
||||
interface ListBoxProps {
|
||||
selected: string;
|
||||
@ -34,20 +34,20 @@ export default function ListBox({
|
||||
data,
|
||||
text,
|
||||
buttonAction,
|
||||
isFull,
|
||||
isFull
|
||||
}: ListBoxProps): JSX.Element {
|
||||
return (
|
||||
<Listbox value={selected} onChange={onChange}>
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
className={`text-gray-400 relative ${
|
||||
isFull ? "w-full" : "w-52"
|
||||
isFull ? 'w-full' : 'w-52'
|
||||
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
{text}
|
||||
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
|
||||
{" "}
|
||||
{' '}
|
||||
{selected}
|
||||
</span>
|
||||
</div>
|
||||
@ -70,11 +70,11 @@ export default function ListBox({
|
||||
key={personIdx}
|
||||
className={({ active, selected }) =>
|
||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
|
||||
selected ? "bg-white/10 text-gray-400 font-bold" : ""
|
||||
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
||||
} ${
|
||||
active && !selected
|
||||
? "bg-white/5 text-mineshaft-200 cursor-pointer"
|
||||
: "text-gray-400"
|
||||
? 'bg-white/5 text-mineshaft-200 cursor-pointer'
|
||||
: 'text-gray-400'
|
||||
} `
|
||||
}
|
||||
value={person}
|
||||
@ -83,7 +83,7 @@ export default function ListBox({
|
||||
<>
|
||||
<span
|
||||
className={`block truncate text-primary${
|
||||
selected ? "font-medium" : "font-normal"
|
||||
selected ? 'font-medium' : 'font-normal'
|
||||
}`}
|
||||
>
|
||||
{person}
|
||||
|
@ -115,7 +115,7 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
<FontAwesomeIcon
|
||||
icon={props.icon}
|
||||
className={`flex my-auto font-extrabold ${
|
||||
props.size == "icon-sm" ? "text-sm" : "text-md"
|
||||
props.size == "icon-sm" ? "text-sm" : "text-sm"
|
||||
} ${(props.text || props.textDisabled) && "mr-2"}`}
|
||||
/>
|
||||
)}
|
||||
|
@ -12,6 +12,7 @@ import { envMapping } from "../../../public/data/frequentConstants";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
} from "../../utilities/cryptography/crypto";
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
@ -25,11 +26,15 @@ const expiryMapping = {
|
||||
"12 months": 31104000,
|
||||
};
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const AddServiceTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
serviceTokens,
|
||||
setServiceTokens
|
||||
}) => {
|
||||
const [serviceToken, setServiceToken] = useState("");
|
||||
const [serviceTokenName, setServiceTokenName] = useState("");
|
||||
@ -48,16 +53,14 @@ const AddServiceTokenDialog = ({
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY"),
|
||||
});
|
||||
|
||||
// generate new public/private key pair
|
||||
const pair = nacl.box.keyPair();
|
||||
const publicKey = nacl.util.encodeBase64(pair.publicKey);
|
||||
const privateKey = nacl.util.encodeBase64(pair.secretKey);
|
||||
|
||||
// encrypt workspace key under newly-generated public key
|
||||
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
const {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey,
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
let newServiceToken = await addServiceToken({
|
||||
@ -65,13 +68,15 @@ const AddServiceTokenDialog = ({
|
||||
workspaceId,
|
||||
environment: envMapping[serviceTokenEnv],
|
||||
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce,
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
|
||||
console.log('newServiceToken', newServiceToken);
|
||||
|
||||
const serviceToken = newServiceToken + "," + privateKey;
|
||||
setServiceToken(serviceToken);
|
||||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
||||
setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
@ -161,7 +166,7 @@ const AddServiceTokenDialog = ({
|
||||
"Production",
|
||||
"Testing",
|
||||
]}
|
||||
width="full"
|
||||
isFull={true}
|
||||
text={`${t("common:environment")}: `}
|
||||
/>
|
||||
</div>
|
||||
@ -176,7 +181,7 @@ const AddServiceTokenDialog = ({
|
||||
"6 months",
|
||||
"12 months",
|
||||
]}
|
||||
width="full"
|
||||
isFull={true}
|
||||
text={`${t("common:expired-in")}: `}
|
||||
/>
|
||||
</div>
|
||||
@ -211,7 +216,7 @@ const AddServiceTokenDialog = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-44">
|
||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20">
|
||||
<input
|
||||
type="text"
|
||||
value={serviceToken}
|
||||
@ -236,7 +241,7 @@ const AddServiceTokenDialog = ({
|
||||
)}
|
||||
</button>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
|
||||
{t("common.click-to-copy")}
|
||||
{t("common:click-to-copy")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,36 +1,53 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
|
||||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
||||
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
interface TokenProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
environment: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface ServiceTokensProps {
|
||||
data: TokenProps[];
|
||||
workspaceName: string;
|
||||
setServiceTokens: (value: TokenProps[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too.
|
||||
* This is the component that we utilize for the service token table
|
||||
* #TODO: add the possibility of choosing and doing operations on multiple users.
|
||||
* @param {*} props
|
||||
* @param {object} obj
|
||||
* @param {any[]} obj.data - current state of the service token table
|
||||
* @param {string} obj.workspaceName - name of the current project
|
||||
* @param {function} obj.setServiceTokens - updating the state of the service token table
|
||||
* @returns
|
||||
*/
|
||||
const ServiceTokenTable = ({ data, workspaceName }) => {
|
||||
const router = useRouter();
|
||||
const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTokensProps) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
return (
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div>
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300">
|
||||
<thead className="text-bunker-300 text-sm font-light">
|
||||
<tr>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">Token name</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">Project</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">Environment</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">Valid until</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">TOKEN NAME</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.length > 0 ? (
|
||||
data.map((row, index) => {
|
||||
data?.map((row) => {
|
||||
return (
|
||||
<tr
|
||||
key={guidGenerator()}
|
||||
@ -51,7 +68,14 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
|
||||
<td className="py-2 border-mineshaft-700 border-t">
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||
<Button
|
||||
onButtonPressed={() => {}}
|
||||
onButtonPressed={() => {
|
||||
deleteServiceToken({ serviceTokenId: row._id} );
|
||||
setServiceTokens(data.filter(token => token._id != row._id));
|
||||
createNotification({
|
||||
text: `'${row.name}' token has been revoked.`,
|
||||
type: 'error'
|
||||
});
|
||||
}}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
@ -63,7 +87,7 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="4" className="text-center pt-7 pb-4 text-bunker-400">
|
||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
||||
No service tokens yet
|
||||
</td>
|
||||
</tr>
|
@ -36,7 +36,7 @@ const Notification = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full flex items-center justify-between px-4 py-6 rounded-md border border-bunker-500 pointer-events-auto bg-bunker-500"
|
||||
className="relative w-full flex items-center justify-between px-4 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-bunker-500"
|
||||
role="alert"
|
||||
>
|
||||
{notification.type === 'error' && (
|
||||
@ -56,7 +56,7 @@ const Notification = ({
|
||||
onClick={() => clearNotification(notification.text)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-white w-4 h-3 hover:text-red"
|
||||
className="text-white pl-2 w-4 h-3 hover:text-red"
|
||||
icon={faX}
|
||||
/>
|
||||
</button>
|
||||
|
@ -38,7 +38,7 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
||||
const createNotification = ({
|
||||
text,
|
||||
type = 'success',
|
||||
timeoutMs = 5000
|
||||
timeoutMs = 4000
|
||||
}: Notification) => {
|
||||
const doesNotifExist = notifications.some((notif) => notif.text === text);
|
||||
|
||||
|
@ -53,7 +53,7 @@ const DashboardInputField = ({
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<div
|
||||
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
|
||||
className={`group relative flex flex-col justify-center w-full border ${
|
||||
error ? 'border-red' : 'border-mineshaft-500'
|
||||
} rounded-md`}
|
||||
>
|
||||
@ -85,7 +85,7 @@ const DashboardInputField = ({
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<div
|
||||
className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
|
||||
className={`group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md`}
|
||||
>
|
||||
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
|
||||
<input
|
||||
@ -108,9 +108,9 @@ const DashboardInputField = ({
|
||||
} ${
|
||||
override ? 'text-primary-300' : 'text-gray-400'
|
||||
}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{value.split(REGEX).map((word, id) => {
|
||||
{value?.split(REGEX).map((word, id) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={id}>
|
||||
@ -139,7 +139,7 @@ const DashboardInputField = ({
|
||||
})}
|
||||
</div>
|
||||
{blurred && (
|
||||
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full max-w-2xl rounded-md text-gray-400/50 text-clip">
|
||||
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full rounded-md text-gray-400/50 text-clip">
|
||||
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
{value.split('').map(() => (
|
||||
<FontAwesomeIcon
|
||||
|
@ -15,40 +15,40 @@ interface SecretDataProps {
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
deleteRow: (id: string) => void;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
sidebarSecretId: string;
|
||||
isSnapshot: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @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 {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
deleteRow,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
isBlurred,
|
||||
isDuplicate,
|
||||
toggleSidebar,
|
||||
sidebarSecretId
|
||||
sidebarSecretId,
|
||||
isSnapshot
|
||||
}: KeyPairProps) => {
|
||||
return (
|
||||
<div className={`mx-1 flex flex-col items-center ml-1 ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
|
||||
<div className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && "pointer-events-none"} ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
|
||||
@ -57,7 +57,7 @@ const KeyPair = ({
|
||||
</span>
|
||||
</div>}
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex pr-1 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
@ -67,8 +67,8 @@ const KeyPair = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-5xl">
|
||||
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
||||
<div className="w-full min-w-xl">
|
||||
<div className={`flex min-w-xl items-center ${!isSnapshot && "pr-1.5"} rounded-lg mt-4 md:mt-0 max-h-10`}>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
@ -79,24 +79,15 @@ const KeyPair = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-9 h-9 bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
|
||||
{!isSnapshot && <div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-2"></div>
|
||||
<div className="bg-[#9B3535] hover:bg-red rounded-md duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => deleteRow(keyPair.id)}
|
||||
color="none"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(KeyPair);
|
||||
export default KeyPair;
|
@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@ -40,6 +41,7 @@ interface SideBarProps {
|
||||
savePush: () => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
deleteRow: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,6 +56,7 @@ interface SideBarProps {
|
||||
* @param {function} obj.savePush - save changes andp ush secrets
|
||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @returns the sidebar with 'secret's settings'
|
||||
*/
|
||||
const SideBar = ({
|
||||
@ -67,93 +70,97 @@ const SideBar = ({
|
||||
buttonReady,
|
||||
savePush,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
setSharedToHide,
|
||||
deleteRow
|
||||
}: SideBarProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("dashboard:sidebar.secret")}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("dashboard:sidebar.secret")}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value
|
||||
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data.filter(secret => secret.type == "shared")[0]?.pos}
|
||||
value={data.filter(secret => secret.type == "shared")[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
|
||||
{t("dashboard:sidebar.personal-explanation")}
|
||||
</div>}
|
||||
<div className='mt-4 px-4'>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value &&
|
||||
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={addOverride}
|
||||
keyName={data[0]?.key}
|
||||
value={data[0]?.value}
|
||||
pos={data[0]?.pos}
|
||||
id={data[0]?.id}
|
||||
comment={data[0]?.comment}
|
||||
deleteOverride={deleteOverride}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
<div className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>}
|
||||
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
</div>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value
|
||||
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos}
|
||||
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value}
|
||||
position={data.filter(secret => secret.type == "shared")[0]?.pos}
|
||||
value={data.filter(secret => secret.type == "shared")[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
|
||||
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
|
||||
{t("dashboard:sidebar.personal-explanation")}
|
||||
</div>}
|
||||
<div className='mt-4 px-4'>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value &&
|
||||
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={addOverride}
|
||||
keyName={data[0]?.key}
|
||||
value={data[0]?.value}
|
||||
pos={data[0]?.pos}
|
||||
id={data[0]?.id}
|
||||
comment={data[0]?.comment}
|
||||
deleteOverride={deleteOverride}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
/>
|
||||
</div>}
|
||||
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos}
|
||||
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
|
||||
</div>
|
||||
{/* <div className={`relative mt-4 px-4 opacity-80 duration-200`}>
|
||||
<p className='text-sm text-bunker-200'>Group</p>
|
||||
<ListBox
|
||||
selected={"Database Secrets"}
|
||||
onChange={() => {}}
|
||||
data={["Group1"]}
|
||||
isFull={true}
|
||||
/>
|
||||
</div> */}
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
|
||||
</div>
|
||||
)}
|
||||
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
||||
<Button
|
||||
text={String(t("common:save-changes"))}
|
||||
@ -163,6 +170,14 @@ const SideBar = ({
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
<div className="bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2">
|
||||
<Button
|
||||
text={String(t("Delete"))}
|
||||
onButtonPressed={() => deleteRow({ ids: overrideEnabled ? data.map(secret => secret.id) : [data.filter(secret => secret.type == "shared")[0]?.id], secretName: data[0]?.key })}
|
||||
color="red"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
32
frontend/ee/api/secrets/GetActionData.ts
Normal file
32
frontend/ee/api/secrets/GetActionData.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the data for a certain action performed by a user
|
||||
* @param {object} obj
|
||||
* @param {string} obj.actionId - id of an action for which we are trying to get data
|
||||
* @returns
|
||||
*/
|
||||
const getActionData = async ({ actionId }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/action/' + actionId, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
console.log(188, res)
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).action;
|
||||
} else {
|
||||
console.log('Failed to get the info about an action');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getActionData;
|
72
frontend/ee/api/secrets/GetProjectLogs.ts
Normal file
72
frontend/ee/api/secrets/GetProjectLogs.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
userId: string;
|
||||
actionNames: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the activity logs for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
|
||||
* @param {object} obj.offset - teh starting point of logs that we want to pull
|
||||
* @param {object} obj.limit - how many logs will we output
|
||||
* @param {object} obj.userId - optional userId filter - will only query logs for that user
|
||||
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
|
||||
* @returns
|
||||
*/
|
||||
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => {
|
||||
let payload;
|
||||
if (userId != "" && actionNames != '') {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId),
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else if (userId != "") {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId)
|
||||
}
|
||||
} else if (actionNames != "") {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent'
|
||||
}
|
||||
}
|
||||
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/logs?' +
|
||||
new URLSearchParams(payload),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).logs;
|
||||
} else {
|
||||
console.log('Failed to get project logs');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectLogs;
|
39
frontend/ee/api/secrets/GetProjectSercetShanpshots.ts
Normal file
39
frontend/ee/api/secrets/GetProjectSercetShanpshots.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the secret snapshots for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||
* @param {object} obj.offset - teh starting point of snapshots that we want to pull
|
||||
* @param {object} obj.limit - how many snapshots will we output
|
||||
* @returns
|
||||
*/
|
||||
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots?' +
|
||||
new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
}), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).secretSnapshots;
|
||||
} else {
|
||||
console.log('Failed to get project secret snapshots');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectSecretShanpshots;
|
31
frontend/ee/api/secrets/GetProjectSercetSnapshotsCount.ts
Normal file
31
frontend/ee/api/secrets/GetProjectSercetSnapshotsCount.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the count of secret snapshots for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||
* @returns
|
||||
*/
|
||||
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).count;
|
||||
} else {
|
||||
console.log('Failed to get the count of project secret snapshots');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectSercetSnapshotsCount;
|
31
frontend/ee/api/secrets/GetSecretSnapshotData.ts
Normal file
31
frontend/ee/api/secrets/GetSecretSnapshotData.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface SnapshotProps {
|
||||
secretSnapshotId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the secrets for a certain secret snapshot
|
||||
* @param {object} obj
|
||||
* @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets
|
||||
* @returns
|
||||
*/
|
||||
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret-snapshot/' + secretSnapshotId, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).secretSnapshot;
|
||||
} else {
|
||||
console.log('Failed to get the secrets of a certain snapshot');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getSecretSnapshotData;
|
@ -17,7 +17,7 @@ interface secretVersionProps {
|
||||
*/
|
||||
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret/' + secretId + '/secret-versions?'+
|
||||
'/api/v1/secret/' + secretId + '/secret-versions?' +
|
||||
new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
@ -32,7 +32,7 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to get project secrets');
|
||||
console.log('Failed to get secret version history');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
185
frontend/ee/components/ActivitySideBar.tsx
Normal file
185
frontend/ee/components/ActivitySideBar.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import getActionData from "ee/api/secrets/GetActionData";
|
||||
import patienceDiff from 'ee/utilities/findTextDifferences';
|
||||
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
||||
|
||||
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} = require('../../components/utilities/cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
currentAction: string;
|
||||
}
|
||||
|
||||
interface SecretProps {
|
||||
secret: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyHash: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueHash: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}
|
||||
|
||||
interface DecryptedSecretProps {
|
||||
newSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
oldSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
|
||||
* @returns the sidebar with the payload of user activity logs
|
||||
*/
|
||||
const ActivitySideBar = ({
|
||||
toggleSidebar,
|
||||
currentAction
|
||||
}: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
||||
const [actionMetaData, setActionMetaData] = useState<ActionProps>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempActionData = await getActionData({ actionId: currentAction });
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
// #TODO: make this a separate function and reuse across the app
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
|
||||
newSecretVersion?: SecretProps;
|
||||
oldSecretVersion?: SecretProps;
|
||||
}) => {
|
||||
return {
|
||||
newSecretVersion: {
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.newSecretVersion!.secretKeyIV,
|
||||
tag: encryptedSecretVersion.newSecretVersion!.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}),
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.newSecretVersion!.secretValueIV,
|
||||
tag: encryptedSecretVersion.newSecretVersion!.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
},
|
||||
oldSecretVersion: {
|
||||
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}): undefined,
|
||||
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
}): undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setActionData(decryptedSecretVersions);
|
||||
setActionMetaData({name: tempActionData.name});
|
||||
setIsLoading(false);
|
||||
}
|
||||
getLogData();
|
||||
}, [currentAction]);
|
||||
|
||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("activity:event." + actionMetaData?.name)}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col px-4'>
|
||||
{(actionMetaData?.name == 'readSecrets'
|
||||
|| actionMetaData?.name == 'addSecrets'
|
||||
|| actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) =>
|
||||
<div key={id}>
|
||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
||||
<DashboardInputField
|
||||
key={id}
|
||||
onChangeHandler={() => {}}
|
||||
type="value"
|
||||
position={1}
|
||||
value={item.newSecretVersion.value}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) =>
|
||||
<>
|
||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
||||
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
|
||||
<div className='bg-red/30 px-2'>- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
|
||||
<div className='bg-green-500/30 px-2'>+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
};
|
||||
|
||||
export default ActivitySideBar;
|
129
frontend/ee/components/ActivityTable.tsx
Normal file
129
frontend/ee/components/ActivityTable.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faAngleDown,
|
||||
faAngleRight,
|
||||
faUpRightFromSquare
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import timeSince from 'ee/utilities/timeSince';
|
||||
|
||||
import guidGenerator from '../../components/utilities/randomId';
|
||||
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: string;
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a single row of the activity table
|
||||
* @param obj
|
||||
* @param {logData} obj.row - data for a certain event
|
||||
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
|
||||
* @returns
|
||||
*/
|
||||
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => {
|
||||
const [payloadOpened, setPayloadOpened] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
|
||||
<td
|
||||
onClick={() => setPayloadOpened(!payloadOpened)}
|
||||
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={payloadOpened ? faAngleDown : faAngleRight}
|
||||
className={`mt-2.5 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${
|
||||
payloadOpened && 'bg-mineshaft-500'
|
||||
} p-1 duration-100 h-4 w-4 rounded-md`}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.user}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.channel}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{timeSince(new Date(row.createdAt))}
|
||||
</td>
|
||||
</tr>
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>Timestamp</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>}
|
||||
{payloadOpened &&
|
||||
row.payload?.map((action, index) =>
|
||||
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td></td>
|
||||
<td className="">{t("activity:event." + action.name)}</td>
|
||||
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
|
||||
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
|
||||
</td>
|
||||
</tr>)}
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>IP Address</td>
|
||||
<td>{row.ipAddress}</td>
|
||||
</tr>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the table for activity logs (one of the tabs)
|
||||
* @param {object} obj
|
||||
* @param {logData} obj.data - data for user activity logs
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @returns
|
||||
*/
|
||||
const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar: (value: string) => void; }) => {
|
||||
return (
|
||||
<div className="w-full px-6 mt-8">
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
|
||||
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5"></div>
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300">
|
||||
<tr className='text-sm'>
|
||||
<th className="text-left pl-6 pt-2.5 pb-3"></th>
|
||||
<th className="text-left font-semibold pt-2.5 pb-3">EVENT</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">USER</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">SOURCE</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">TIME</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((row, index) => {
|
||||
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTable;
|
158
frontend/ee/components/PITRecoverySidebar.tsx
Normal file
158
frontend/ee/components/PITRecoverySidebar.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import getProjectSecretShanpshots from "ee/api/secrets/GetProjectSercetShanpshots";
|
||||
import getSecretSnapshotData from "ee/api/secrets/GetSecretSnapshotData";
|
||||
import timeSince from "ee/utilities/timeSince";
|
||||
|
||||
import Button from "~/components/basic/buttons/Button";
|
||||
import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cryptography/crypto";
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: boolean) => void;
|
||||
setSnapshotData: (value: any) => void;
|
||||
chosenSnapshot: string;
|
||||
}
|
||||
|
||||
interface SnaphotProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface EncrypetedSecretVersionListProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
environment: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {function} obj.setSnapshotData - state manager for snapshot data
|
||||
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
|
||||
*
|
||||
*
|
||||
* @returns the sidebar with the options for point-in-time recovery (commits)
|
||||
*/
|
||||
const PITRecoverySidebar = ({
|
||||
toggleSidebar,
|
||||
setSnapshotData,
|
||||
chosenSnapshot
|
||||
}: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState<SnaphotProps[]>([]);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 15;
|
||||
|
||||
const loadMoreSnapshots = () => {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const results = await getProjectSecretShanpshots({ workspaceId: String(router.query.id), limit: currentLimit, offset: currentOffset })
|
||||
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
|
||||
setIsLoading(false);
|
||||
}
|
||||
getLogData();
|
||||
}, [currentOffset]);
|
||||
|
||||
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string; }) => {
|
||||
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
|
||||
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => {
|
||||
return {
|
||||
id: encryptedSecretVersion._id,
|
||||
pos: pos,
|
||||
type: encryptedSecretVersion.type,
|
||||
environment: encryptedSecretVersion.environment,
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.secretKeyIV,
|
||||
tag: encryptedSecretVersion.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}),
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.secretValueIV,
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setSnapshotData({ id: secretSnapshotData._id, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
|
||||
}
|
||||
|
||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar(false)}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col px-2 py-2'>
|
||||
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => <div key={snapshot._id} className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black" : "bg-mineshaft-700"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}>
|
||||
<div className="flex flex-row items-start">
|
||||
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
|
||||
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
|
||||
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
|
||||
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
|
||||
</div>
|
||||
</div>)}
|
||||
<div className='flex justify-center w-full mb-14'>
|
||||
<div className='items-center w-40'>
|
||||
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
};
|
||||
|
||||
export default PITRecoverySidebar;
|
@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
@ -22,15 +23,18 @@ interface EncrypetedSecretVersionListProps {
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} secretId - the id of a secret for which are querying version history
|
||||
* @returns a list of versions for a specific secret
|
||||
*/
|
||||
const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([{createdAt: "123", value: "124"}]);
|
||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSecretVersionHistory = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10});
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
@ -61,43 +65,54 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
})
|
||||
|
||||
setSecretVersions(decryptedSecretVersions);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
};
|
||||
getSecretVersionHistory();
|
||||
}, []);
|
||||
}, [secretId]);
|
||||
|
||||
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
|
||||
<p className=''>{t("dashboard:sidebar.version-history")}</p>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none'>
|
||||
<div className='h-48 overflow-y-auto overflow-x-none'>
|
||||
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<div className='w-0 h-full border-l mt-1'></div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
||||
<div className='pr-2 pt-1'>
|
||||
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}
|
||||
<p className=''>{t("dashboard:sidebar.version-history")}</p>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full'>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-48 overflow-y-auto overflow-x-none'>
|
||||
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<div className='w-0 h-full border-l mt-1'></div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
||||
<div className='pr-2 pt-1'>
|
||||
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
||||
{/* <div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Updated by:</span>{version.user}</p></div> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
||||
|
346
frontend/ee/utilities/findTextDifferences.ts
Normal file
346
frontend/ee/utilities/findTextDifferences.ts
Normal file
@ -0,0 +1,346 @@
|
||||
/**
|
||||
*
|
||||
* @param textOld - old secret
|
||||
* @param textNew - new (updated) secret
|
||||
* @param diffPlusFlag - a flag for whether we want to detect moving segments
|
||||
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
|
||||
* @returns
|
||||
*/
|
||||
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
|
||||
|
||||
/**
|
||||
* findUnique finds all unique values in arr[lo..hi], inclusive. This
|
||||
* function is used in preparation for determining the longest common
|
||||
* subsequence. Specifically, it first reduces the array range in question
|
||||
* to unique values.
|
||||
* @param chars - an array of characters
|
||||
* @param lo
|
||||
* @param hi
|
||||
* @returns - an ordered Map, with the arr[i] value as the Map key and the
|
||||
* array index i as the Map value.
|
||||
*/
|
||||
function findUnique(chars: string[], lo: number, hi: number) {
|
||||
const characterMap = new Map();
|
||||
|
||||
for (let i=lo; i<=hi; i++) {
|
||||
const character = chars[i];
|
||||
|
||||
if (characterMap.has(character)) {
|
||||
characterMap.get(character).count++;
|
||||
characterMap.get(character).index = i;
|
||||
} else {
|
||||
characterMap.set(character, { count: 1, index: i });
|
||||
}
|
||||
}
|
||||
|
||||
characterMap.forEach((val, key, map) => {
|
||||
if (val.count !== 1) {
|
||||
map.delete(key);
|
||||
} else {
|
||||
map.set(key, val.index);
|
||||
}
|
||||
});
|
||||
|
||||
return characterMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param aArray
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bArray
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @returns an ordered Map, with the Map key as the common line between aArray
|
||||
* and bArray, with the Map value as an object containing the array indexes of
|
||||
* the matching unique lines.
|
||||
*
|
||||
*/
|
||||
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) {
|
||||
const ma = findUnique(aArray, aLo, aHi);
|
||||
const mb = findUnique(bArray, bLo, bHi);
|
||||
|
||||
ma.forEach((val, key, map) => {
|
||||
if (mb.has(key)) {
|
||||
map.set(key, {
|
||||
indexA: val,
|
||||
indexB: mb.get(key)
|
||||
});
|
||||
} else {
|
||||
map.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
return ma;
|
||||
}
|
||||
|
||||
/**
|
||||
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
|
||||
* and determines the Longest Common Subsequence (LCS).
|
||||
* @param abMap
|
||||
* @returns an ordered array of objects containing the array indexes of the
|
||||
* matching lines for a LCS.
|
||||
*/
|
||||
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) {
|
||||
const ja: any = [];
|
||||
|
||||
// First, walk the list creating the jagged array.
|
||||
abMap.forEach((val, key, map) => {
|
||||
let i = 0;
|
||||
|
||||
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!ja[i]) {
|
||||
ja[i] = [];
|
||||
}
|
||||
|
||||
if (0 < i) {
|
||||
val.prev = ja[i-1][ja[i - 1].length - 1];
|
||||
}
|
||||
ja[i].push(val);
|
||||
});
|
||||
|
||||
// Now, pull out the longest common subsequence.
|
||||
let lcs: any[] = [];
|
||||
|
||||
if (0 < ja.length) {
|
||||
const n = ja.length - 1;
|
||||
lcs = [ja[n][ja[n].length - 1]];
|
||||
|
||||
while (lcs[lcs.length - 1].prev) {
|
||||
lcs.push(lcs[lcs.length - 1].prev);
|
||||
}
|
||||
}
|
||||
|
||||
return lcs.reverse();
|
||||
}
|
||||
|
||||
// "result" is the array used to accumulate the textOld that are deleted, the
|
||||
// lines that are shared between textOld and textNew, and the textNew that were
|
||||
// inserted.
|
||||
|
||||
const result: any[] = [];
|
||||
let deleted = 0;
|
||||
let inserted = 0;
|
||||
|
||||
// aMove and bMove will contain the lines that don't match, and will be returned
|
||||
// for possible searching of lines that moved.
|
||||
|
||||
const aMove: any[] = [];
|
||||
const aMoveIndex: any[] = [];
|
||||
const bMove: any[] = [];
|
||||
const bMoveIndex: any[] = [];
|
||||
|
||||
/**
|
||||
* addToResult simply pushes the latest value onto the "result" array. This
|
||||
* array captures the diff of the line, aIndex, and bIndex from the textOld
|
||||
* and textNew array.
|
||||
* @param aIndex
|
||||
* @param bIndex
|
||||
*/
|
||||
function addToResult(aIndex: number, bIndex: number) {
|
||||
if (bIndex < 0) {
|
||||
aMove.push(textOld[aIndex]);
|
||||
aMoveIndex.push(result.length);
|
||||
deleted++;
|
||||
} else if (aIndex < 0) {
|
||||
bMove.push(textNew[bIndex]);
|
||||
bMoveIndex.push(result.length);
|
||||
inserted++;
|
||||
}
|
||||
|
||||
result.push({
|
||||
line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex],
|
||||
aIndex: aIndex,
|
||||
bIndex: bIndex,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* addSubMatch handles the lines between a pair of entries in the LCS. Thus,
|
||||
* this function might recursively call recurseLCS to further match the lines
|
||||
* between textOld and textNew.
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
*/
|
||||
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
|
||||
// Match any lines at the beginning of textOld and textNew.
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
|
||||
addToResult(aLo++, bLo++);
|
||||
}
|
||||
|
||||
// Match any lines at the end of textOld and textNew, but don't place them
|
||||
// in the "result" array just yet, as the lines between these matches at
|
||||
// the beginning and the end need to be analyzed first.
|
||||
|
||||
const aHiTemp = aHi;
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
|
||||
aHi--;
|
||||
bHi--;
|
||||
}
|
||||
|
||||
// Now, check to determine with the remaining lines in the subsequence
|
||||
// whether there are any unique common lines between textOld and textNew.
|
||||
//
|
||||
// If not, add the subsequence to the result (all textOld having been
|
||||
// deleted, and all textNew having been inserted).
|
||||
//
|
||||
// If there are unique common lines between textOld and textNew, then let's
|
||||
// recursively perform the patience diff on the subsequence.
|
||||
|
||||
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
|
||||
|
||||
if (uniqueCommonMap.size === 0) {
|
||||
while (aLo <= aHi) {
|
||||
addToResult(aLo++, -1);
|
||||
}
|
||||
|
||||
while (bLo <= bHi) {
|
||||
addToResult(-1, bLo++);
|
||||
}
|
||||
} else {
|
||||
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
|
||||
}
|
||||
|
||||
// Finally, let's add the matches at the end to the result.
|
||||
while (aHi < aHiTemp) {
|
||||
addToResult(++aHi, ++bHi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* recurseLCS finds the longest common subsequence (LCS) between the arrays
|
||||
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
|
||||
* recursively performs another LCS search (via addSubMatch), until there are
|
||||
* none found, at which point the subsequence is dumped to the result.
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @param uniqueCommonMap
|
||||
*/
|
||||
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
|
||||
const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi));
|
||||
|
||||
if (x.length === 0) {
|
||||
addSubMatch(aLo, aHi, bLo, bHi);
|
||||
} else {
|
||||
if (aLo < x[0].indexA || bLo < x[0].indexB) {
|
||||
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
|
||||
}
|
||||
|
||||
let i;
|
||||
for (i = 0; i < x.length - 1; i++) {
|
||||
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1);
|
||||
}
|
||||
|
||||
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
|
||||
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
|
||||
|
||||
if (diffPlusFlag) {
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
aMove: aMove,
|
||||
aMoveIndex: aMoveIndex,
|
||||
bMove: bMove,
|
||||
bMoveIndex: bMoveIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* use: patienceDiffPlus( textOld[], textNew[] )
|
||||
*
|
||||
* where:
|
||||
* textOld[] contains the original text lines.
|
||||
* textNew[] contains the new text lines.
|
||||
*
|
||||
* returns an object with the following properties:
|
||||
* lines[] with properties of:
|
||||
* line containing the line of text from textOld or textNew.
|
||||
* aIndex referencing the index in aLine[].
|
||||
* bIndex referencing the index in textNew[].
|
||||
* (Note: The line is text from either textOld or textNew, with aIndex and bIndex
|
||||
* referencing the original index. If aIndex === -1 then the line is new from textNew,
|
||||
* and if bIndex === -1 then the line is old from textOld.)
|
||||
* moved is true if the line was moved from elsewhere in textOld[] or textNew[].
|
||||
* lineCountDeleted is the number of lines from textOld[] not appearing in textNew[].
|
||||
* lineCountInserted is the number of lines from textNew[] not appearing in textOld[].
|
||||
* lineCountMoved is the number of lines that moved.
|
||||
*/
|
||||
|
||||
function patienceDiffPlus(textOld: string[], textNew: string[]) {
|
||||
|
||||
const difference = patienceDiff(textOld, textNew, true);
|
||||
|
||||
let aMoveNext = difference.aMove;
|
||||
let aMoveIndexNext = difference.aMoveIndex;
|
||||
let bMoveNext = difference.bMove;
|
||||
let bMoveIndexNext = difference.bMoveIndex;
|
||||
|
||||
delete difference.aMove;
|
||||
delete difference.aMoveIndex;
|
||||
delete difference.bMove;
|
||||
delete difference.bMoveIndex;
|
||||
|
||||
let lastLineCountMoved;
|
||||
|
||||
do {
|
||||
const aMove = aMoveNext;
|
||||
const aMoveIndex = aMoveIndexNext;
|
||||
const bMove = bMoveNext;
|
||||
const bMoveIndex = bMoveIndexNext;
|
||||
|
||||
aMoveNext = [];
|
||||
aMoveIndexNext = [];
|
||||
bMoveNext = [];
|
||||
bMoveIndexNext = [];
|
||||
|
||||
const subDiff = patienceDiff(aMove!, bMove!);
|
||||
|
||||
lastLineCountMoved = difference.lineCountMoved;
|
||||
|
||||
subDiff.lines.forEach((v, i) => {
|
||||
|
||||
if (0 <= v.aIndex && 0 <= v.bIndex) {
|
||||
|
||||
difference.lines[aMoveIndex![v.aIndex]].moved = true;
|
||||
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
|
||||
difference.lines[bMoveIndex![v.bIndex]].moved = true;
|
||||
difference.lineCountInserted--;
|
||||
difference.lineCountDeleted--;
|
||||
difference.lineCountMoved++;
|
||||
} else if (v.bIndex < 0) {
|
||||
aMoveNext!.push(aMove![v.aIndex]);
|
||||
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
|
||||
} else {
|
||||
bMoveNext!.push(bMove![v.bIndex]);
|
||||
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
|
||||
}
|
||||
});
|
||||
} while (0 < difference.lineCountMoved - lastLineCountMoved);
|
||||
|
||||
return difference;
|
||||
|
||||
}
|
||||
|
||||
export default patienceDiff;
|
35
frontend/ee/utilities/timeSince.ts
Normal file
35
frontend/ee/utilities/timeSince.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Time since a certain date
|
||||
* @param {Date} date - the timestamp got which we want to understand how long ago it happened
|
||||
* @returns {String} text - how much time has passed since a certain timestamp
|
||||
*/
|
||||
function timeSince(date: Date) {
|
||||
const seconds = Math.floor(
|
||||
((new Date() as any) - (date as any)) / 1000
|
||||
) as number;
|
||||
|
||||
let interval = seconds / 31536000;
|
||||
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' years ago';
|
||||
}
|
||||
interval = seconds / 2592000;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' months ago';
|
||||
}
|
||||
interval = seconds / 86400;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' days ago';
|
||||
}
|
||||
interval = seconds / 3600;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' hours ago';
|
||||
}
|
||||
interval = seconds / 60;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' minutes ago';
|
||||
}
|
||||
return Math.floor(seconds) + ' seconds ago';
|
||||
}
|
||||
|
||||
export default timeSince;
|
4093
frontend/package-lock.json
generated
4093
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
145
frontend/pages/activity/[id].tsx
Normal file
145
frontend/pages/activity/[id].tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import ActivitySideBar from 'ee/components/ActivitySideBar';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import EventFilter from '~/components/basic/EventFilter';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
||||
|
||||
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
|
||||
import ActivityTable from '../../ee/components/ActivityTable';
|
||||
|
||||
|
||||
interface logData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: {
|
||||
email: string;
|
||||
};
|
||||
actions: {
|
||||
_id: string;
|
||||
name: string;
|
||||
payload: {
|
||||
secretVersions: string[];
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logDataPoint {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: string;
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the tab that includes all of the user activity logs
|
||||
*/
|
||||
export default function Activity() {
|
||||
const router = useRouter();
|
||||
const [eventChosen, setEventChosen] = useState('');
|
||||
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 10;
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>()
|
||||
const { t } = useTranslation();
|
||||
|
||||
// this use effect updates the data in case of a new filter being added
|
||||
useEffect(() => {
|
||||
setCurrentOffset(0);
|
||||
const getLogData = async () => {
|
||||
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen })
|
||||
setLogsData(tempLogsData.map((log: logData) => {
|
||||
return {
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log.user.email,
|
||||
payload: log.actions.map(action => {
|
||||
return {
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
getLogData();
|
||||
}, [eventChosen]);
|
||||
|
||||
// this use effect adds more data in case 'View More' button is clicked
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen })
|
||||
setLogsData(logsData.concat(tempLogsData.map((log: logData) => {
|
||||
return {
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log.user.email,
|
||||
payload: log.actions.map(action => {
|
||||
return {
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}
|
||||
})
|
||||
}
|
||||
})))
|
||||
}
|
||||
getLogData();
|
||||
}, [currentLimit, currentOffset]);
|
||||
|
||||
const loadMoreLogs = () => {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
|
||||
<NavHeader pageName="Project Activity" isProjectRelated={true} />
|
||||
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4 text-bunker-100">Activity Logs</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
Event history for this Infisical project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-6 h-8 mt-2">
|
||||
<EventFilter
|
||||
selected={eventChosen}
|
||||
select={setEventChosen}
|
||||
/>
|
||||
</div>
|
||||
<ActivityTable
|
||||
data={logsData}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<div className='flex justify-center w-full mb-6'>
|
||||
<div className='items-center w-60'>
|
||||
<Button text="View More" textDisabled="End of History" active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Activity.requireAuth = true;
|
||||
|
||||
export const getServerSideProps = getTranslatedServerSideProps(["activity"]);
|
@ -5,14 +5,21 @@ interface Props {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
expiresIn: number;
|
||||
publicKey: string;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route gets service tokens for a specific user in a project
|
||||
* @param {*} param0
|
||||
* @param {object} obj
|
||||
* @param {string} obj.name - name of the service token
|
||||
* @param {string} obj.workspaceId - workspace for which we are issuing the token
|
||||
* @param {string} obj.environment - environment for which we are issuing the token
|
||||
* @param {string} obj.expiresIn - how soon the service token expires in ms
|
||||
* @param {string} obj.encryptedKey - encrypted project key through random symmetric encryption
|
||||
* @param {string} obj.iv - obtained through symmetric encryption
|
||||
* @param {string} obj.tag - obtained through symmetric encryption
|
||||
* @returns
|
||||
*/
|
||||
const addServiceToken = ({
|
||||
@ -20,11 +27,11 @@ const addServiceToken = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
expiresIn,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce
|
||||
iv,
|
||||
tag
|
||||
}: Props) => {
|
||||
return SecurityClient.fetchCall('/api/v1/service-token/', {
|
||||
return SecurityClient.fetchCall('/api/v2/service-token/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@ -34,13 +41,13 @@ const addServiceToken = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
expiresIn,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce
|
||||
iv,
|
||||
tag
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).token;
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to add service tokens');
|
||||
}
|
||||
|
30
frontend/pages/api/serviceToken/deleteServiceToken.ts
Normal file
30
frontend/pages/api/serviceToken/deleteServiceToken.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
serviceTokenId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route revokes a specific service token
|
||||
* @param {object} obj
|
||||
* @param {string} obj.serviceTokenId - id of a cervice token that we want to delete
|
||||
* @returns
|
||||
*/
|
||||
const deleteServiceToken = ({
|
||||
serviceTokenId,
|
||||
}: Props) => {
|
||||
return SecurityClient.fetchCall('/api/v2/service-token/' + serviceTokenId, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to delete a service token');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default deleteServiceToken;
|
@ -7,7 +7,7 @@ import SecurityClient from '~/utilities/SecurityClient';
|
||||
*/
|
||||
const getServiceTokens = ({ workspaceId }: { workspaceId: string }) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/service-tokens',
|
||||
'/api/v2/workspace/' + workspaceId + '/service-token-data',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -16,7 +16,7 @@ const getServiceTokens = ({ workspaceId }: { workspaceId: string }) => {
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).serviceTokens;
|
||||
return (await res.json()).serviceTokenData;
|
||||
} else {
|
||||
console.log('Failed to get service tokens');
|
||||
}
|
||||
|
@ -6,8 +6,9 @@ import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faArrowDownAZ,
|
||||
faArrowDownZA,
|
||||
faArrowLeft,
|
||||
faCheck,
|
||||
faCopy,
|
||||
faClockRotateLeft,
|
||||
faDownload,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
@ -16,6 +17,8 @@ import {
|
||||
faPlus,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount';
|
||||
import PITRecoverySidebar from 'ee/components/PITRecoverySidebar';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import ListBox from '~/components/basic/Listbox';
|
||||
@ -30,12 +33,13 @@ import pushKeys from '~/components/utilities/secrets/pushKeys';
|
||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
||||
import guidGenerator from '~/utilities/randomId';
|
||||
|
||||
import { envMapping } from '../../public/data/frequentConstants';
|
||||
import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants';
|
||||
import getUser from '../api/user/getUser';
|
||||
import checkUserAction from '../api/userActions/checkUserAction';
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||
|
||||
const queryString = require("query-string");
|
||||
|
||||
interface SecretDataProps {
|
||||
type: 'personal' | 'shared';
|
||||
@ -46,6 +50,19 @@ interface SecretDataProps {
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface SnapshotProps {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
secretVersions: {
|
||||
id: string;
|
||||
pos: number;
|
||||
type: "personal" | "shared";
|
||||
environment: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* this function finds the teh duplicates in an array
|
||||
* @param arr - array of anything (e.g., with secret keys and types (personal/shared))
|
||||
@ -76,22 +93,20 @@ export default function Dashboard() {
|
||||
const [workspaceId, setWorkspaceId] = useState('');
|
||||
const [blurred, setBlurred] = useState(true);
|
||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||
const [env, setEnv] = useState(
|
||||
router.asPath.split('?').length == 1
|
||||
? 'Development'
|
||||
: Object.keys(envMapping).includes(router.asPath.split('?')[1])
|
||||
? router.asPath.split('?')[1]
|
||||
: 'Development'
|
||||
);
|
||||
const [env, setEnv] = useState('Development');
|
||||
const [snapshotEnv, setSnapshotEnv] = useState('Development');
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchKeys, setSearchKeys] = useState('');
|
||||
const [errorDragAndDrop, setErrorDragAndDrop] = useState(false);
|
||||
const [projectIdCopied, setProjectIdCopied] = useState(false);
|
||||
const [sortMethod, setSortMethod] = useState('alphabetical');
|
||||
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
||||
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
||||
const [sidebarSecretId, toggleSidebar] = useState("None");
|
||||
const [PITSidebarOpen, togglePITSidebar] = useState(false);
|
||||
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
||||
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
|
||||
const [numSnapshots, setNumSnapshots] = useState<number>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
@ -142,17 +157,39 @@ export default function Dashboard() {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
console.log(1, 'reloaded')
|
||||
const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) })
|
||||
setNumSnapshots(tempNumSnapshots);
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
|
||||
if (
|
||||
!listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0])
|
||||
!listWorkspaces.includes(router.asPath.split('/')[2])
|
||||
) {
|
||||
router.push('/dashboard/' + listWorkspaces[0]);
|
||||
}
|
||||
|
||||
if (env != router.asPath.split('?')[1]) {
|
||||
router.push(router.asPath.split('?')[0] + '?' + env);
|
||||
}
|
||||
const user = await getUser();
|
||||
setIsNew(
|
||||
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
|
||||
? true
|
||||
: false
|
||||
);
|
||||
|
||||
const userAction = await checkUserAction({
|
||||
action: 'first_time_secrets_pushed'
|
||||
});
|
||||
setHasUserEverPushed(userAction ? true : false);
|
||||
} catch (error) {
|
||||
console.log('Error', error);
|
||||
setData(undefined);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setBlurred(true);
|
||||
setWorkspaceId(String(router.query.id));
|
||||
|
||||
@ -174,18 +211,7 @@ export default function Dashboard() {
|
||||
dataToSort?.map((item) => item.key).indexOf(item)
|
||||
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
|
||||
)
|
||||
|
||||
const user = await getUser();
|
||||
setIsNew(
|
||||
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
|
||||
? true
|
||||
: false
|
||||
);
|
||||
|
||||
const userAction = await checkUserAction({
|
||||
action: 'first_time_secrets_pushed'
|
||||
});
|
||||
setHasUserEverPushed(userAction ? true : false);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log('Error', error);
|
||||
setData(undefined);
|
||||
@ -241,9 +267,14 @@ export default function Dashboard() {
|
||||
sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
|
||||
};
|
||||
|
||||
const deleteRow = (id: string) => {
|
||||
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
||||
setButtonReady(true);
|
||||
setData(data!.filter((row: SecretDataProps) => row.id !== id));
|
||||
toggleSidebar("None");
|
||||
createNotification({
|
||||
text: `${secretName} has been deleted. Remember to save changes.`,
|
||||
type: 'error'
|
||||
});
|
||||
setData(data!.filter((row: SecretDataProps) => !ids.includes(row.id)));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -317,12 +348,21 @@ export default function Dashboard() {
|
||||
/**
|
||||
* Save the changes of environment variables and push them to the database
|
||||
*/
|
||||
const savePush = async () => {
|
||||
// Format the new object with environment variables
|
||||
const obj = Object.assign(
|
||||
{},
|
||||
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment] }))
|
||||
);
|
||||
const savePush = async (dataToPush?: any[], envToPush?: string) => {
|
||||
let obj;
|
||||
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
|
||||
if ((dataToPush ?? [])?.length > 0) {
|
||||
obj = Object.assign(
|
||||
{},
|
||||
...dataToPush!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
|
||||
);
|
||||
} else {
|
||||
// Format the new object with environment variables
|
||||
obj = Object.assign(
|
||||
{},
|
||||
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
|
||||
);
|
||||
}
|
||||
|
||||
// Checking if any of the secret keys start with a number - if so, don't do anything
|
||||
const nameErrors = !Object.keys(obj)
|
||||
@ -346,13 +386,17 @@ export default function Dashboard() {
|
||||
|
||||
// Once "Save changed is clicked", disable that button
|
||||
setButtonReady(false);
|
||||
pushKeys({ obj, workspaceId: String(router.query.id), env });
|
||||
console.log(envToPush ? envToPush : env, env, envToPush)
|
||||
pushKeys({ obj, workspaceId: String(router.query.id), env: envToPush ? envToPush : env });
|
||||
|
||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||
if (!hasUserEverPushed) {
|
||||
setCheckDocsPopUpVisible(true);
|
||||
await registerUserAction({ action: 'first_time_secrets_pushed' });
|
||||
}
|
||||
|
||||
// increasing the number of project commits
|
||||
setNumSnapshots(numSnapshots ?? 0 + 1);
|
||||
};
|
||||
|
||||
const addData = (newData: SecretDataProps[]) => {
|
||||
@ -395,27 +439,10 @@ export default function Dashboard() {
|
||||
alink.click();
|
||||
};
|
||||
|
||||
const deleteCertainRow = (id: string) => {
|
||||
deleteRow(id);
|
||||
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
||||
deleteRow({ids, secretName});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function copies the project id to the clipboard
|
||||
*/
|
||||
function copyToClipboard() {
|
||||
const copyText = document.getElementById('myInput') as HTMLInputElement;
|
||||
|
||||
if (copyText) {
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setProjectIdCopied(true);
|
||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return data ? (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
@ -438,6 +465,12 @@ export default function Dashboard() {
|
||||
savePush={savePush}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
deleteRow={deleteCertainRow}
|
||||
/>}
|
||||
{PITSidebarOpen && <PITRecoverySidebar
|
||||
toggleSidebar={togglePITSidebar}
|
||||
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : "")}
|
||||
setSnapshotData={setSnapshotData}
|
||||
/>}
|
||||
<div className="w-full max-h-96 pb-2">
|
||||
<NavHeader pageName={t("dashboard:title")} isProjectRelated={true} />
|
||||
@ -453,9 +486,22 @@ export default function Dashboard() {
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl">
|
||||
{snapshotData &&
|
||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||
<Button
|
||||
text={String(t("Go back to current"))}
|
||||
onButtonPressed={() => setSnapshotData(undefined)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faArrowLeft}
|
||||
/>
|
||||
</div>}
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4 mt-1">{t("dashboard:title")}</p>
|
||||
{data?.length == 0 && (
|
||||
<div className="font-semibold mr-4 mt-1 flex flex-row items-center">
|
||||
<p>{snapshotData ? "Secret Snapshot" : t("dashboard:title")}</p>
|
||||
{snapshotData && <span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>{new Date(snapshotData.createdAt).toLocaleString()}</span>}
|
||||
</div>
|
||||
{!snapshotData && data?.length == 0 && (
|
||||
<ListBox
|
||||
selected={env}
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
@ -464,35 +510,17 @@ export default function Dashboard() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400">
|
||||
<p className="mr-2 font-bold pl-4">{`${t(
|
||||
"common:project-id"
|
||||
)}:`}</p>
|
||||
<input
|
||||
type="text"
|
||||
value={workspaceId}
|
||||
id="myInput"
|
||||
className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none"
|
||||
disabled
|
||||
></input>
|
||||
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
|
||||
>
|
||||
{projectIdCopied ? (
|
||||
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
)}
|
||||
</button>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-white/10 rounded-md text-center text-gray-400 text-sm">
|
||||
{t("common:click-to-copy")}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||
<Button
|
||||
text={String(numSnapshots + " " + t("Commits"))}
|
||||
onButtonPressed={() => togglePITSidebar(true)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faClockRotateLeft}
|
||||
/>
|
||||
</div>
|
||||
{(data?.length !== 0 || buttonReady) && (
|
||||
<div className={`flex justify-start max-w-sm mt-2`}>
|
||||
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
||||
<div className={`flex justify-start max-w-sm mt-1`}>
|
||||
<Button
|
||||
text={String(t("common:save-changes"))}
|
||||
onButtonPressed={savePush}
|
||||
@ -504,18 +532,64 @@ export default function Dashboard() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{snapshotData && <div className={`flex justify-start max-w-sm mt-1`}>
|
||||
<Button
|
||||
text={String(t("Rollback to this snapshot"))}
|
||||
onButtonPressed={async () => {
|
||||
const envsToRollback = snapshotData.secretVersions.map(sv => sv.environment).filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
// Update secrets in the state only for the current environment
|
||||
setData(
|
||||
snapshotData.secretVersions
|
||||
.filter(row => reverseEnvMapping[row.environment] == env)
|
||||
.map((sv, position) => {
|
||||
return {
|
||||
id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Rollback each of the environments in the snapshot
|
||||
// #TODO: clean up other environments
|
||||
envsToRollback.map(async (envToRollback) => {
|
||||
await savePush(
|
||||
snapshotData.secretVersions
|
||||
.filter(row => row.environment == envToRollback)
|
||||
.map((sv, position) => {
|
||||
return {id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''}
|
||||
}),
|
||||
reverseEnvMapping[envToRollback]
|
||||
);
|
||||
});
|
||||
setSnapshotData(undefined);
|
||||
createNotification({
|
||||
text: `Rollback has been performed successfully.`,
|
||||
type: 'success'
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-6 w-full pr-12">
|
||||
<div className="flex flex-col max-w-5xl pb-1">
|
||||
<div className="w-full flex flex-row items-start">
|
||||
{data?.length !== 0 && (
|
||||
{(!snapshotData || data?.length !== 0) && (
|
||||
<>
|
||||
<ListBox
|
||||
{!snapshotData
|
||||
? <ListBox
|
||||
selected={env}
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
onChange={setEnv}
|
||||
/>
|
||||
: <ListBox
|
||||
selected={snapshotEnv}
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
onChange={setSnapshotEnv}
|
||||
/>}
|
||||
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
|
||||
<FontAwesomeIcon
|
||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
||||
@ -528,7 +602,7 @@ export default function Dashboard() {
|
||||
placeholder={String(t("dashboard:search-keys"))}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
onButtonPressed={() => reorderRows(1)}
|
||||
color="mineshaft"
|
||||
@ -539,15 +613,15 @@ export default function Dashboard() {
|
||||
: faArrowDownZA
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
</div>}
|
||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
onButtonPressed={download}
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
icon={faDownload}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
onButtonPressed={changeBlurred}
|
||||
@ -556,7 +630,7 @@ export default function Dashboard() {
|
||||
icon={blurred ? faEye : faEyeSlash}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative ml-2 min-w-max flex flex-row items-start justify-end">
|
||||
{!snapshotData && <div className="relative ml-2 min-w-max flex flex-row items-start justify-end">
|
||||
<Button
|
||||
text={String(t("dashboard:add-key"))}
|
||||
onButtonPressed={addRow}
|
||||
@ -570,32 +644,65 @@ export default function Dashboard() {
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-primary"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{data?.length !== 0 ? (
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full my-48">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
data?.length !== 0 ? (
|
||||
<div className="flex flex-col w-full mt-1 mb-2">
|
||||
<div
|
||||
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
|
||||
{data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
||||
{!snapshotData && data?.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||
.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
||||
<KeyPair
|
||||
key={keyPair.id}
|
||||
keyPair={keyPair}
|
||||
deleteRow={deleteCertainRow}
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
isBlurred={blurred}
|
||||
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
|
||||
toggleSidebar={toggleSidebar}
|
||||
sidebarSecretId={sidebarSecretId}
|
||||
isSnapshot={false}
|
||||
/>
|
||||
))}
|
||||
{snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key))
|
||||
.filter(row => reverseEnvMapping[row.environment] == snapshotEnv)
|
||||
.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||
.filter(row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
snapshotData.secretVersions?.map((item) => item.key).indexOf(item)
|
||||
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id).includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
||||
<KeyPair
|
||||
key={keyPair.id}
|
||||
keyPair={keyPair}
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
isBlurred={blurred}
|
||||
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
|
||||
toggleSidebar={toggleSidebar}
|
||||
sidebarSecretId={sidebarSecretId}
|
||||
isSnapshot={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full max-w-5xl px-2 pt-3">
|
||||
{!snapshotData && <div className="w-full max-w-5xl px-2 pt-3">
|
||||
<DropZone
|
||||
setData={addData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
@ -605,12 +712,12 @@ export default function Dashboard() {
|
||||
keysExist={true}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
|
||||
{isKeyAvailable && (
|
||||
{isKeyAvailable && !snapshotData && (
|
||||
<DropZone
|
||||
setData={setData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
@ -639,7 +746,7 @@ export default function Dashboard() {
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,12 +2,13 @@ import { useEffect, useRef, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faCopy, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from "~/components/basic/buttons/Button";
|
||||
import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog";
|
||||
import InputField from "~/components/basic/InputField";
|
||||
import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable";
|
||||
import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable.tsx";
|
||||
import NavHeader from "~/components/navigation/NavHeader";
|
||||
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
|
||||
|
||||
@ -16,6 +17,7 @@ import deleteWorkspace from "../../api/workspace/deleteWorkspace";
|
||||
import getWorkspaces from "../../api/workspace/getWorkspaces";
|
||||
import renameWorkspace from "../../api/workspace/renameWorkspace";
|
||||
|
||||
|
||||
export default function SettingsBasic() {
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
@ -26,9 +28,28 @@ export default function SettingsBasic() {
|
||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||
let [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] =
|
||||
useState(false);
|
||||
const [projectIdCopied, setProjectIdCopied] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* This function copies the project id to the clipboard
|
||||
*/
|
||||
function copyToClipboard() {
|
||||
// const copyText = document.getElementById('myInput') as HTMLInputElement;
|
||||
const copyText = document.getElementById('myInput')
|
||||
|
||||
if (copyText) {
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setProjectIdCopied(true);
|
||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(async () => {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
userWorkspaces.map((userWorkspace) => {
|
||||
@ -103,6 +124,8 @@ export default function SettingsBasic() {
|
||||
workspaceId={router.query.id}
|
||||
closeModal={closeAddServiceTokenModal}
|
||||
workspaceName={workspaceName}
|
||||
serviceTokens={serviceTokens}
|
||||
setServiceTokens={setServiceTokens}
|
||||
/>
|
||||
<div className="flex flex-row mr-6 max-w-5xl">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
@ -124,7 +147,7 @@ export default function SettingsBasic() {
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-md mt-2 flex flex-col items-start">
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2">
|
||||
<p className="text-xl font-semibold mb-4">
|
||||
<p className="text-xl font-semibold mb-4 mt-2">
|
||||
{t("common:display-name")}
|
||||
</p>
|
||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
||||
@ -150,7 +173,7 @@ export default function SettingsBasic() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
|
||||
<div className="bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
|
||||
<p className="text-xl font-semibold self-start">
|
||||
{t("common:project-id")}
|
||||
</p>
|
||||
@ -169,18 +192,36 @@ export default function SettingsBasic() {
|
||||
{t("settings-project:docs")}
|
||||
</a>
|
||||
</p>
|
||||
<div className="max-h-28 w-ful">
|
||||
<InputField
|
||||
type="varName"
|
||||
value={router.query.id}
|
||||
placeholder=""
|
||||
isRequired
|
||||
static
|
||||
text={t("settings-project:auto-generated")}
|
||||
/>
|
||||
<p className="mt-4 text-xs text-bunker-300">{t("settings-project:auto-generated")}</p>
|
||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400">
|
||||
<p className="mr-2 font-bold pl-4">{`${t(
|
||||
"common:project-id"
|
||||
)}:`}</p>
|
||||
<input
|
||||
type="text"
|
||||
value={workspaceId}
|
||||
id="myInput"
|
||||
className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none"
|
||||
disabled
|
||||
></input>
|
||||
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
|
||||
>
|
||||
{projectIdCopied ? (
|
||||
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
)}
|
||||
</button>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm">
|
||||
{t("common:click-to-copy")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2">
|
||||
<div className="bg-white/5 rounded-md px-6 pt-4 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<p className="text-xl font-semibold mb-3">
|
||||
@ -205,47 +246,12 @@ export default function SettingsBasic() {
|
||||
<ServiceTokenTable
|
||||
data={serviceTokens}
|
||||
workspaceName={workspaceName}
|
||||
setServiceTokens={setServiceTokens}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <div className="bg-white/5 rounded-md px-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-6 pt-6">
|
||||
<p className="text-xl font-semibold self-start">
|
||||
Project Environments
|
||||
</p>
|
||||
<p className="text-md mr-1 text-gray-400 mt-2 self-start">
|
||||
Choose which environments will show up
|
||||
in your Dashboard. Some common ones
|
||||
include Development, Staging, and
|
||||
Production. Often, teams choose to add
|
||||
Testing.
|
||||
</p>
|
||||
<p className="text-sm mr-1 text-gray-500 self-start">
|
||||
Note: the text in brackets shows how
|
||||
these environmant should be accessed in
|
||||
CLI.
|
||||
</p>
|
||||
<div className="rounded-md h-10 w-full mr-auto mt-4 flex flex-row">
|
||||
{envOptions.map((env) => (
|
||||
<div className="bg-white/5 hover:bg-white/10 duration-200 h-full w-max px-3 flex flex-row items-center justify-between rounded-md mr-1 text-sm">
|
||||
{env}
|
||||
<XIcon
|
||||
className="h-5 w-5 ml-2 mt-0.5 text-white cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="group bg-white/5 hover:bg-primary hover:text-black duration-200 h-full w-max py-1 px-3 flex flex-row items-center justify-between rounded-md mr-1 cursor-pointer text-sm font-semibold">
|
||||
<PlusIcon
|
||||
className="h-5 w-5 text-white mr-2 group-hover:text-black"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Add
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
|
||||
<div className="bg-white/5 rounded-md px-6 pt-4 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
|
||||
<p className="text-xl font-bold text-red">
|
||||
{t("settings-project:danger-zone")}
|
||||
</p>
|
||||
|
@ -141,16 +141,16 @@ export default function SignupInvite() {
|
||||
// Step 4 of the sign up process (download the emergency kit pdf)
|
||||
const stepConfirmEmail = (
|
||||
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl text-center font-semibold mb-8 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 mb-6 flex justify-center text-primary-100">
|
||||
Confirm your email
|
||||
</p>
|
||||
<Image
|
||||
src="/images/envelope.svg"
|
||||
src="/images/dragon-signupinvite.svg"
|
||||
height={262}
|
||||
width={410}
|
||||
alt="verify email"
|
||||
></Image>
|
||||
<div className="flex max-w-max flex-col items-center justify-center md:p-2 max-h-24 max-w-md mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<div className="flex max-w-max flex-col items-center justify-center md:p-2 max-h-24 max-w-md mx-auto text-lg px-4 mt-10 mb-2">
|
||||
<Button
|
||||
text="Confirm Email"
|
||||
onButtonPressed={async () => {
|
||||
|
129
frontend/public/images/dragon-signupinvite.svg
Normal file
129
frontend/public/images/dragon-signupinvite.svg
Normal file
@ -0,0 +1,129 @@
|
||||
<svg width="378" height="600" viewBox="0 0 378 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_118_2691)">
|
||||
<path d="M52.51 256.81C52.51 256.81 45.39 236.73 45.92 222.75C46.45 208.77 47.9 204.03 47.9 204.03L41.62 196.12C41.62 196.12 43.11 195.5 44.7 196.12C46.29 196.74 47.26 197.14 47.26 197.14C47.26 197.14 48.2 188.87 50.89 181.48C53.58 174.09 56.94 166.32 56.94 166.32C56.94 166.32 31.36 134.25 29.44 127.61C27.52 120.97 32.24 107.9 32.44 88.67C32.64 69.44 24.52 46.3 19.6 36.25C14.68 26.2 8.47 10.37 8.47 10.37C8.47 10.37 31.4 26.51 49.98 53.59C68.56 80.67 76.51 104.73 78.01 112.64C79.51 120.55 80.36 129.13 80.36 129.13C80.36 129.13 86.49 122.44 89.65 119.25C92.81 116.06 95.75 112.27 95.75 112.27C95.75 112.27 93.02 119.98 91.98 125.61C90.94 131.24 90.47 138.92 90.47 138.92C90.47 138.92 93.96 135.92 98.65 134.78C103.34 133.64 107.2 133.01 107.2 133.01C107.2 133.01 118.91 115.02 128.26 103C137.61 90.98 149.09 78.96 149.09 78.96C149.09 78.96 145.35 97.39 142.55 107.54C139.75 117.69 138.01 132.13 138.14 131.32C138.27 130.51 144.61 131.91 146.56 131.98C148.51 132.05 150.83 133.13 150.83 133.13C150.83 133.13 156.58 125.51 166.72 122.13C176.86 118.75 183.15 119.04 183.15 119.04C183.15 119.04 178.13 123.87 175.61 128.51C173.09 133.15 169.23 137.98 169.23 137.98C169.23 137.98 175.99 139.72 181.12 142.23C186.25 144.74 192.68 149.28 192.68 149.28C192.68 149.28 195.75 128.67 194.94 117.87C194.13 107.07 196.2 105.02 204.7 99.8C213.2 94.58 241.29 69.28 252.49 57.33C263.69 45.38 275.77 27.16 279.69 20.35C279.69 20.35 282.11 42.2 269.42 78.26C256.73 114.32 246.34 131.77 233.08 148.45C219.82 165.13 215.3 167.94 215.3 167.94C215.3 167.94 232.52 189.29 241.54 205.71C250.56 222.13 277.83 281.26 287.88 312.34C297.93 343.42 325.15 440.06 327.84 463.85C330.53 487.64 341.96 544.7 344.13 558.2C344.13 558.2 326.64 478.31 311.18 439.78C295.72 401.25 274.22 330.09 249.81 282.67C225.4 235.25 215.01 221.3 215.01 221.3C215.01 221.3 213.39 229.6 210.77 236.81C208.15 244.02 205.45 248.19 205.45 248.19C205.45 248.19 212.37 246.63 214.01 245.65C215.65 244.67 213.57 251.78 210.77 255.93C207.97 260.08 202.92 265.49 202.92 265.49C202.92 265.49 202.92 283.01 187.01 293.81C171.1 304.61 164.96 305.77 162.12 309.72C159.28 313.67 157.58 317.93 159.05 336.27C160.52 354.61 167.2 382.9 167.2 382.9L156.41 383.08C156.41 383.08 154.86 390.33 148.66 396.81C142.46 403.29 137.62 406.57 137.62 406.57C137.62 406.57 139.99 413.05 143.18 417.88C146.37 422.71 146.83 423.9 146.83 423.9C146.83 423.9 138.28 422.08 132.52 417.15C126.76 412.22 125.58 410.58 125.58 410.58C125.58 410.58 115 412.77 106.88 408.94C98.76 405.11 89.55 396.81 89.55 396.81C89.55 396.81 89.09 403.83 85.9 406.57C82.71 409.31 77.23 411.95 77.23 411.95C77.23 411.95 77.23 417.88 79.33 422.17C81.43 426.46 84.17 429.1 84.17 429.1C84.17 429.1 75.23 425.27 71.49 420.71C67.75 416.15 65.56 412.77 65.56 412.77C65.56 412.77 49.69 410.31 44.58 398.81C39.47 387.31 39.84 378.28 39.84 378.28C39.84 378.28 30.72 366.15 35.28 356.57C39.84 346.99 49.79 345.71 53.62 346.99C57.45 348.27 63.84 351.28 63.84 351.28C63.84 351.28 69.59 344.62 75.61 331.12C81.63 317.62 84.7 305.49 83.44 302.25C82.18 299.01 75.96 289.63 71.08 287.37C66.2 285.11 58.52 281.89 51.34 271.18C44.16 260.47 43.04 251.11 43.04 251.11C43.04 251.11 46.42 255.46 48.7 256.13C50.98 256.8 52.54 256.77 52.54 256.77L52.51 256.81Z" fill="#ADD63D"/>
|
||||
<path d="M167.2 382.94C167.2 382.94 160.59 349.3 159.05 336.31C157.51 323.32 157.81 309.31 165.49 305.87C173.17 302.43 192.85 293.32 197.51 285.21C202.17 277.1 202.91 265.52 202.91 265.52C202.91 265.52 208.03 261.43 210.76 255.96C213.49 250.49 214 245.68 214 245.68L205.44 248.22C205.44 248.22 210.36 238.57 212.34 231.98C214.32 225.39 214.99 221.32 214.99 221.32C214.99 221.32 238.33 257.8 250.6 284.29C262.87 310.78 286.79 367.25 301.95 411.75C317.11 456.25 332.34 507.48 338.22 532.85C344.1 558.22 346.81 568.69 348.34 579.62C349.87 590.55 350.82 596.9 350.82 596.9H146.4C146.4 596.9 148.45 585.23 149.26 577.29C150.07 569.35 150.86 558.22 150.86 558.22C150.86 558.22 242.6 557.01 248.61 557.01C254.62 557.01 260.4 556.22 260.66 544.93C260.92 533.64 260.6 396.04 260.6 392.23C260.6 388.42 258.38 382.99 249.48 382.95C240.58 382.91 167.19 382.95 167.19 382.95L167.2 382.94Z" fill="#E5F234"/>
|
||||
<path d="M139.9 266.29C139.9 266.29 138.7 255.81 138.7 252.56V245.57C138.7 245.57 152.81 240.57 161.66 231.8C170.51 223.03 178.7 209.54 178.7 209.54C178.7 209.54 183.64 215.68 183.4 224.96C183.16 234.24 181.35 248.69 170.03 254.48C158.71 260.27 150.27 260.07 146.9 261.97C143.53 263.87 141.84 266.32 141.48 267.21C141.12 268.1 139.91 266.29 139.91 266.29H139.9Z" fill="white"/>
|
||||
<path d="M55.84 221.33C55.84 221.33 54.71 235.46 59.38 247.24C64.05 259.02 69.99 261.56 76.21 263.4C82.43 265.24 86.53 265.95 87.95 268.07C89.37 270.19 90.5 273.44 90.5 273.44C90.5 273.44 91.63 262.41 91.35 251.38C91.07 240.35 90.5 239.22 90.5 239.22C90.5 239.22 81.31 247.2 70.98 240.17C60.65 233.14 60.23 227.34 59.38 225.93C58.53 224.52 55.84 221.34 55.84 221.34V221.33Z" fill="white"/>
|
||||
<path d="M239.77 202.63C239.77 202.63 252.91 204.91 259.85 208.5C266.79 212.09 274.59 218.86 278.26 226.09C281.93 233.32 282.43 236.9 282.43 236.9C282.43 236.9 276.92 227.04 268.65 225.5C260.38 223.96 256.91 223.83 255.31 224.5C253.71 225.17 252.51 226.09 252.51 226.09L239.77 202.64V202.63Z" fill="#30993A"/>
|
||||
<path d="M303.74 356.09C303.74 356.09 313.8 365.8 324.37 376.87C334.94 387.94 339.07 401.17 340.23 409.12C341.39 417.07 341.08 419.63 341.08 419.63C341.08 419.63 338.82 405.78 329.49 396.74C320.16 387.7 310.97 385.45 310.97 385.45L303.74 356.09V356.09Z" fill="#30993A"/>
|
||||
<path d="M339.29 519.97C339.29 519.97 352.44 527.4 361.59 544.85C370.74 562.3 370.17 575.88 370.17 575.88C370.17 575.88 367.02 561.87 359.02 554.58C351.02 547.29 342.72 545.29 342.72 545.29L339.29 519.98V519.97Z" fill="#30993A"/>
|
||||
<path d="M38.44 383.12H10.8C6.94001 383.12 3.82001 386.25 3.82001 390.1V548.14C3.82001 552.7 7.52001 556.4 12.08 556.4H250.98C255.57 556.4 259.31 552.7 259.35 548.1L260.63 390.73C260.66 386.54 257.27 383.12 253.08 383.12H156.42C156.42 383.12 155.19 391.65 146.41 399.13L137.63 406.61C137.63 406.61 140.2 415.72 143.52 419.83C146.84 423.94 148.03 426.34 148.03 426.34C148.03 426.34 137.22 421.39 132.54 417.19C127.86 412.99 125.6 410.62 125.6 410.62C125.6 410.62 109.78 414.06 100.47 406.14C91.16 398.22 89.57 396.85 89.57 396.85C89.57 396.85 87.97 408.97 82.61 410.48L77.25 411.99C77.25 411.99 77.09 417.58 79.35 422.21C81.61 426.84 84.19 429.14 84.19 429.14C84.19 429.14 77.44 428.68 71.51 420.75L65.58 412.81C65.58 412.81 50.74 411.15 45.9 401.8C41.06 392.45 38.48 383.12 38.48 383.12H38.44Z" fill="#E6FFFF"/>
|
||||
<path d="M133.29 514.89C154.509 514.89 171.71 497.689 171.71 476.47C171.71 455.251 154.509 438.05 133.29 438.05C112.071 438.05 94.87 455.251 94.87 476.47C94.87 497.689 112.071 514.89 133.29 514.89Z" fill="#63ED4A"/>
|
||||
<path d="M209.1 173.6C209.1 173.6 234.52 202.63 257.89 254.45C277.4 297.7 296.34 361.95 305.17 398.01C314 434.07 315.41 443.46 315.41 443.46L336.65 522.52C336.65 522.52 317.94 400.97 304.96 361.03C291.98 321.09 275.55 269.28 260.87 241.86C246.19 214.44 228.76 184.08 222.6 175.98C216.44 167.88 215.74 167.3 215.74 167.3L209.09 173.58L209.1 173.6Z" fill="#30993A"/>
|
||||
<path d="M191.01 157.59C191.01 157.59 187.12 153.53 179.33 149.44C172.09 145.63 166.19 143.41 166.19 143.41L170.37 138.29C170.37 138.29 182.39 142.28 187.53 144.93C192.67 147.58 193.3 148.46 193.3 148.46L191.01 157.59V157.59Z" fill="#30993A"/>
|
||||
<path d="M91.35 145.17C95.13 143.08 99.36 141.26 104.15 139.96C105.28 137.55 106.63 135 107.83 132.81C106.01 133.12 104.37 133.46 102.99 133.81C98.77 134.88 94.99 136.16 91.14 137.91L91.35 145.17V145.17Z" fill="#30993A"/>
|
||||
<path d="M149.82 133.39C149.93 133.27 150.07 133.16 150.22 133.04C146.52 132.2 142.45 131.66 138.27 131.36C138.06 133.31 137.83 135.53 137.62 137.53C140.48 137.7 143.29 137.97 146.04 138.32C147.04 136.79 148.19 135.14 149.81 133.39H149.82Z" fill="#30993A"/>
|
||||
<path d="M47.9 198.94C48.54 199.3 49.3 199.81 50.12 200.42C50.92 196.53 51.92 192.74 53.14 189.3C55.42 182.9 57.94 177.69 61.06 173.01C60.72 172.08 60.42 171.48 60.01 170.88L57.46 167.33C54.76 172.02 52.9 176.24 51.92 178.74C50.28 182.92 48.65 190.29 47.52 198.59L47.91 198.94H47.9Z" fill="#30993A"/>
|
||||
<path d="M123.93 231.98C123.93 231.98 128.46 238.41 138.69 235.48C148.92 232.55 155.47 224.74 164.65 213.8C173.83 202.86 180.66 196.81 185.64 193.78C190.62 190.75 197.09 188.75 197.09 188.75C197.09 188.75 186.32 197.52 178.51 208.49C170.7 219.46 161.21 233.36 153.75 239.01C146.29 244.66 141.89 245.82 136.52 244.65C131.15 243.48 127.25 241.33 125.69 238.5C124.13 235.67 123.93 231.97 123.93 231.97V231.98Z" fill="#30993A"/>
|
||||
<path d="M93.48 234.12C93.48 234.12 87.99 237.47 81.25 236.5C74.51 235.53 70.71 229.96 67.63 224.67C63.67 217.87 57.37 205.97 50.11 200.42C45.74 197.08 41.61 196.12 41.61 196.12C41.61 196.12 56.62 213.06 61.05 226.07C65.48 239.08 70.42 244.22 77.2 243.96C83.98 243.7 87.28 242.2 89.18 240.15C91.08 238.1 93.48 234.12 93.48 234.12V234.12Z" fill="#30993A"/>
|
||||
<path d="M138.75 276.15C138.75 276.15 134.81 270.58 131.81 262.76C128.81 254.94 127.81 244.85 128.17 242.03C128.53 239.21 137.26 245.22 137.63 245.07C138 244.92 138.16 255.04 139.34 261.05C140.52 267.06 138.75 276.15 138.75 276.15V276.15Z" fill="#30993A"/>
|
||||
<path d="M183.7 239.82C187.57 231.26 189.28 224.74 190.46 216.85C191.64 208.96 192.75 192.59 192.75 192.59C192.75 192.59 184.83 199.42 184.38 205.26C183.93 211.1 185.64 218.4 185.64 223.65C185.64 228.9 183.7 239.82 183.7 239.82V239.82Z" fill="#30993A"/>
|
||||
<path d="M195.35 253.07C195.35 253.07 201.93 244.14 207.07 231.22C212.21 218.3 213.83 208.67 215.01 201.46C215.01 201.46 216.19 212.57 214.04 223.79C211.89 235.01 205.78 249.96 205.78 249.96L195.35 253.06V253.07Z" fill="#30993A"/>
|
||||
<path d="M140.77 294.62C140.77 294.62 144.8 292.03 155.8 292.1C166.8 292.17 176.34 292.87 184.33 288.98C192.32 285.09 196.62 280.43 200.4 272.28C204.18 264.13 190.34 280.74 181.48 283.05C172.62 285.36 162.45 285.57 157.06 286.42C151.67 287.27 147.78 287.94 145.21 289.72C142.64 291.5 140.76 294.63 140.76 294.63L140.77 294.62Z" fill="#30993A"/>
|
||||
<path d="M81.25 287.84C85.27 289.15 87.63 292.02 87.63 292.02L84.44 302.59C84.44 302.59 80.47 293.77 75.32 290.7C70.17 287.63 68.38 286.29 68.38 286.29C68.38 286.29 72.28 286.73 75.01 286.99C77.74 287.25 81.24 287.85 81.24 287.85L81.25 287.84Z" fill="#30993A"/>
|
||||
<path d="M60.62 255.5C55.52 248.94 53.05 242.72 50.95 232.58C48.85 222.44 48.48 206.68 48.48 206.68L56.6 218.56C56.6 218.56 53.78 227.38 55.72 236.15C57.66 244.92 60.62 255.51 60.62 255.51V255.5Z" fill="#30993A"/>
|
||||
<path d="M147.91 180.56C147.91 180.56 156.73 167.42 164.38 159.05C172.03 150.68 176.25 147.71 179.33 149.44C182.41 151.17 182.02 145.09 176.3 143.41C170.58 141.73 167.52 141.79 167.52 141.79C167.52 141.79 161.64 150.37 158.47 157.59C155.3 164.81 147.92 180.55 147.92 180.55L147.91 180.56Z" fill="#30993A"/>
|
||||
<path d="M120.72 214.72C120.72 214.72 127.62 209.07 132.93 196.12C137.77 184.32 139.04 175.66 140.78 163.37C142.52 151.08 142.27 143.24 146.04 138.32C149.81 133.4 138.69 132.81 138.69 132.81C138.69 132.81 136.23 152.3 133.77 167.83C131.31 183.36 130.07 194.06 127.59 200.14C125.11 206.22 120.72 214.72 120.72 214.72V214.72Z" fill="#30993A"/>
|
||||
<path d="M91.42 147.31C92.23 151.71 94.89 157.79 94.89 157.79L103.05 138.32L90.46 141.18L91.42 147.31V147.31Z" fill="#30993A"/>
|
||||
<path d="M94.44 211.75C94.44 211.75 89.48 202.56 88.29 190.76C87.1 178.96 88.64 169.71 89.65 164.02C90.66 158.33 93.49 150.86 93.49 150.86L97.45 152.47C97.45 152.47 91.59 170.63 91.59 181.75C91.59 192.87 91.46 199.91 92.03 202.72C92.6 205.53 94.43 211.74 94.43 211.74L94.44 211.75Z" fill="#30993A"/>
|
||||
<path d="M209.1 173.6C206.93 175.84 204.99 177.24 204.99 177.24C204.99 177.24 223.54 154.02 232.28 142.04C241.65 129.21 253.55 107.54 258.71 94.01C263.87 80.48 278.32 28.42 278.32 28.42C278.32 28.42 277.89 48.41 270.49 75.13C263.09 101.85 250.74 122.23 243.22 134.54C235.7 146.85 226.36 156.15 221.99 161.56C217.62 166.97 213.41 170.12 212.15 171.35C210.89 172.58 209.11 173.59 209.11 173.59L209.1 173.6Z" fill="#30993A"/>
|
||||
<path d="M221.09 137.53C221.09 137.53 229.91 122.95 239.19 108.44C248.47 93.93 259.17 78.28 259.17 78.28C259.17 78.28 251.55 100.57 244.63 112.18C237.71 123.79 229.6 129.78 227.56 132.23C225.52 134.68 221.09 137.53 221.09 137.53V137.53Z" fill="#30993A"/>
|
||||
<path d="M12.63 15.82C12.63 15.82 35.12 38.31 49.73 64.03C64.34 89.75 68.68 107.83 70.94 120.17C73.2 132.51 73.2 146.07 73.2 146.07L81.02 129.13C81.02 129.13 76.35 94.98 60.87 71.1C45.39 47.22 31.18 30.09 25.1 24.49C19.02 18.89 12.63 15.82 12.63 15.82Z" fill="#30993A"/>
|
||||
<path d="M123.93 284.38C123.93 284.38 123.94 302.84 125.56 316.38C127.18 329.92 131.07 339.68 133.92 344.73C133.92 344.73 123.82 345.04 122.47 334.79C121.12 324.54 122.63 309.74 122.47 301.25C122.31 292.76 123.93 284.38 123.93 284.38Z" fill="#30993A"/>
|
||||
<path d="M151.64 301.25C151.64 301.25 150.56 306.94 150.72 317.29C150.72 317.29 151.67 310.74 155.9 307.19C160.13 303.64 165.62 301.88 169.57 300.32C173.52 298.76 165.85 306.48 162.7 310.7C159.55 314.92 158.01 317.2 158.42 328.92C158.83 340.64 161.77 358.55 163.3 368.55C164.83 378.55 167.2 382.93 167.2 382.93L154.62 383.11C154.62 383.11 159.59 373.2 156.4 362.66C153.21 352.12 149.72 345.16 146.39 343.13C143.06 341.1 144.48 334.91 145.35 325.82C146.22 316.73 146.84 310.36 148.1 307.56C149.36 304.76 151.63 301.24 151.63 301.24L151.64 301.25Z" fill="#30993A"/>
|
||||
<path d="M151.64 301.25C151.64 301.25 145.65 305.2 142.16 314.55C138.67 323.9 136.28 338.05 136.28 338.05C136.28 338.05 138.29 338.55 140.76 340.05C143.23 341.55 144.07 341.85 144.07 341.85C144.07 341.85 144.29 319.6 146.2 313.58C148.11 307.56 149.83 303.89 149.83 303.89L151.64 301.24V301.25Z" fill="#30993A"/>
|
||||
<path d="M136.29 338.05C136.29 338.05 146.8 343.39 150.34 358.27C153.66 372.2 150.78 385.28 141.09 394.37C132.34 402.58 118.75 405.25 109.59 403.11C100.43 400.97 92.16 396.99 85.44 386.71C85.44 386.71 90.43 399.86 102.37 406.5C114.31 413.14 127.38 408.13 127.38 408.13C127.38 408.13 132.66 407.66 134.88 406.19C137.1 404.72 150.67 398.12 153.31 390.61C155.95 383.1 159.22 374.68 156.79 364.09C154.36 353.5 150.97 349.72 146.69 344.73C142.41 339.74 140.16 338.97 138.81 338.49C137.46 338.01 136.29 338.06 136.29 338.06V338.05Z" fill="#30993A"/>
|
||||
<path d="M128.15 410.8C128.15 410.8 132.71 410.8 136.61 408.29C140.51 405.78 137.23 403.62 134.56 403.93C131.89 404.24 124.72 406.75 124.72 406.75L128.15 410.8Z" fill="#30993A"/>
|
||||
<path d="M46.08 383.12C46.08 383.12 45.4 386.83 47.29 393.57C49.18 400.31 55.54 405.38 62.82 407.13C70.1 408.88 77.36 407.58 81.3 402.96C85.24 398.34 86.1 393.12 85.44 386.72C85.44 386.72 89.91 392.72 87.36 400.92C84.81 409.12 77.31 413.81 75.03 413.99C72.75 414.17 66.57 414.3 66.35 413.99C66.13 413.68 48.66 408.26 45.23 400.55C41.8 392.84 38.85 384.78 39.06 381.68C39.27 378.58 40.56 378.66 40.56 378.66L46.77 382.75" fill="#30993A"/>
|
||||
<path d="M60.91 386.77C60.91 386.77 50.31 388.69 44.99 387.23C39.67 385.77 39.12 380.98 39.12 380.98L42.11 379.68C42.11 379.68 50.31 384.35 53.31 385.41C56.31 386.47 60.9 386.77 60.9 386.77H60.91Z" fill="#30993A"/>
|
||||
<path d="M122.59 383.12C122.59 383.12 130.98 382.75 138.75 377.45C145.36 372.94 147.39 366.74 147.39 361.28C147.39 354.19 143.91 348.55 137.95 344.73C131.99 340.91 131.23 345.14 131.23 345.14C131.23 345.14 141.67 347.09 142.77 354.52C143.87 361.95 143.09 365.91 139.21 370.41C135.33 374.91 131.78 378.16 129.56 379.42C127.34 380.68 122.57 383.12 122.57 383.12H122.59Z" fill="#30993A"/>
|
||||
<path d="M132.11 362.67C134.1 369.3 126.98 371.3 120.85 372.66C114.72 374.02 110.28 376.08 110.28 376.08C110.28 376.08 115.03 371.23 121.28 367.35C127.53 363.47 132.11 362.67 132.11 362.67Z" fill="#30993A"/>
|
||||
<path d="M61.25 376.88C61.25 376.88 57.8 375.75 53.01 375.13C48.22 374.51 45.06 374.21 43.36 372.06C41.85 370.16 42.02 367.42 43.36 365.81C44.7 364.2 47.82 366.54 53.12 370.92C58.42 375.3 61.25 376.88 61.25 376.88Z" fill="#30993A"/>
|
||||
<path d="M190.68 159.35C190.68 159.35 197.27 149.45 200.57 135.96C203.87 122.47 203.73 114.64 214.99 104.35C226.25 94.06 236 86.09 250.28 69.89C264.56 53.69 272.89 39 278.33 28.42C283.77 17.84 266.7 43.24 252.98 56.83C239.26 70.42 209.45 95.59 203.15 100.79C196.85 105.99 195.39 110.24 195.06 120.42C194.73 130.6 193.98 136.96 193.47 143.42C192.96 149.88 190.69 159.36 190.69 159.36L190.68 159.35Z" fill="#EBFFAB"/>
|
||||
<path d="M94.44 211.75C94.44 211.75 94.38 180.96 104.45 155.22C114.52 129.48 146.83 83.26 146.83 83.26C146.83 83.26 128.29 97.73 115.02 121.34C101.75 144.95 92.5 163.02 92.04 175.67C91.58 188.32 92.78 198.15 92.65 201.47C92.52 204.79 94.43 211.75 94.43 211.75H94.44Z" fill="#EBFFAB"/>
|
||||
<path d="M147.91 180.56C147.91 180.56 150.99 157.98 158.23 144.58C165.47 131.18 174.6 123.46 179.04 120.79C183.48 118.12 174.07 128.83 168.3 138.61C162.53 148.39 158.52 155.28 155.98 161.18C153.44 167.08 147.91 180.56 147.91 180.56Z" fill="#EBFFAB"/>
|
||||
<path d="M91.94 118.93C91.94 118.93 85.2 128.78 79.57 146.98C73.94 165.18 75.08 178.72 75.56 183.87C76.04 189.02 77.98 198.6 77.98 198.6C77.98 198.6 69.99 181.45 70.59 166.01C71.19 150.57 73.71 141.41 79.32 132.8C84.93 124.19 91.94 116.36 91.94 116.36V118.92V118.93Z" fill="#EBFFAB"/>
|
||||
<path d="M92.6 238.71C92.6 238.71 97.22 252.29 94.47 280.54C91.72 308.79 81.58 337.66 77.8 347.39C74.92 354.79 72.59 359.67 72.59 359.67C72.59 359.67 72.28 359.2 69.44 357.2C66.6 355.2 62.74 351.67 62.74 351.67C62.74 351.67 69.32 347.67 73.55 339.07C77.78 330.47 85.32 305.49 87.63 294.95C89.94 284.41 92.93 264.11 92.65 255.49C92.37 246.87 92.6 238.7 92.6 238.7V238.71Z" fill="#EBFFAB"/>
|
||||
<path d="M103.71 359.12C99.5 362.43 95.34 366.85 95.34 366.85C95.34 366.85 109.25 356.96 119.87 352.43C129.08 348.5 136.82 347.78 139.94 352.26C142.73 356.26 143.1 351 139.94 348.51C136.78 346.02 130.02 341.66 121.63 345.56C113.24 349.46 108.05 354.08 106.42 356.1C104.79 358.12 103.71 359.13 103.71 359.13V359.12Z" fill="#EBFFAB"/>
|
||||
<path d="M46.52 380.38C41.94 376.81 38.33 372.56 37.92 365.8C37.51 359.04 40.8 354.83 45.66 352.33C50.52 349.83 58.18 349.83 64.23 353.61C68.87 356.51 58.18 346.1 51.02 346.63C43.86 347.16 38.88 350.19 36.04 355.15C33.2 360.11 32.46 363.89 34.28 369.08C36.1 374.27 37.94 376.47 40.89 378.64C43.84 380.81 46.52 380.37 46.52 380.37V380.38Z" fill="#EBFFAB"/>
|
||||
<path d="M45.15 255.5C45.15 255.5 48.12 263.94 57.64 271.67C67.16 279.4 75.62 280.92 81.26 281.61C81.26 281.61 68.65 284.92 60.63 279.34C52.61 273.76 48.39 267.23 46.61 262.71C44.83 258.19 45.16 255.5 45.16 255.5H45.15Z" fill="#EBFFAB"/>
|
||||
<path d="M161.04 283.66C172.02 281.37 181.06 276.6 191.01 269.27C201.55 261.5 211.74 248.51 211.74 248.51C211.74 248.51 208.44 259.19 199.4 268.61C190.36 278.03 180.47 283.16 172.66 284.37C164.85 285.58 161.04 283.65 161.04 283.65V283.66Z" fill="#EBFFAB"/>
|
||||
<path d="M132.71 221.33C135.69 214.65 141.01 206.72 148.75 200.75C156.49 194.78 163.2 192.17 163.2 192.17C163.2 192.17 152.17 201.37 146.82 206.75C141.47 212.13 132.7 221.33 132.7 221.33H132.71Z" fill="#EBFFAB"/>
|
||||
<path d="M52.76 157.49C46.61 146.55 38.16 132.68 37.97 126.06C37.78 119.44 40.58 107.91 39.67 92.96C37.78 61.75 23.88 32.63 12.62 15.82C12.62 15.82 28.63 53.87 30.84 71.99C33.05 90.11 31.22 110.21 29.16 120.23C27.1 130.25 29.31 129.31 35.81 138.32C42.31 147.33 52.75 157.49 52.75 157.49H52.76Z" fill="#EBFFAB"/>
|
||||
<path d="M115.47 396.75C112.39 396.75 104.91 396.73 99.08 392.58C92.55 387.92 89.89 382.72 88.14 378.19C88.14 378.19 94.01 385.08 101.51 390.17C107.69 394.36 115.47 396.75 115.47 396.75Z" fill="#EBFFAB"/>
|
||||
<path d="M213.09 235.8C213.09 235.8 234.88 269.47 252.19 309.4C269.5 349.33 289.02 397.96 301.26 436.08C313.5 474.2 334.73 544.97 340.53 596.9H350.85C350.85 596.9 329.25 489.6 312.87 444.82C296.49 400.04 272.59 334.57 257.9 302.3C243.21 270.03 216.46 222.33 216.46 222.33C216.46 222.33 213.04 229.81 212.36 232C211.68 234.19 213.09 235.82 213.09 235.82V235.8Z" fill="#B1B511"/>
|
||||
<path d="M159.81 320.37C159.81 320.37 169.7 321.92 193.18 321.3C216.66 320.68 243.87 314.47 248.01 313.13C252.15 311.79 252.16 309.25 250.41 305.36C248.66 301.47 253.19 304.77 255.67 310.71C258.15 316.65 257.96 319.73 237.88 323.26C217.8 326.79 188.91 327.27 177.64 327.82C166.37 328.37 159.05 326.27 159.05 326.27L159.81 320.37V320.37Z" fill="#B1B511"/>
|
||||
<path d="M261.44 405.91C265.67 405.76 276.13 405.33 281.51 404.52C286.89 403.71 290.12 403.73 288.79 400.14C287.46 396.55 290.68 396.87 292.07 401.06C293.46 405.25 296.81 407.43 289.88 408.67C282.95 409.91 260.65 411.73 260.65 411.73L261.43 405.9L261.44 405.91Z" fill="#B1B511"/>
|
||||
<path d="M260.78 508.93C260.78 508.93 272.96 508.5 291.41 506.82C309.86 505.14 316.92 503.72 318.52 503.4C320.12 503.08 320.82 501.9 319.93 498.67C319.04 495.44 323.37 499.92 323.89 503.8C324.41 507.68 323.94 509.27 316.4 510.36C308.86 511.45 260.11 515.24 260.11 515.24L260.78 509.57V508.93Z" fill="#B1B511"/>
|
||||
<path d="M177.64 382.94C177.44 377.3 175.61 356.17 176.29 340.06C176.97 323.95 177.44 316.6 190.08 305.79C202.72 294.98 207.18 286.55 209.09 278.35C211 270.15 210.77 266.65 210.77 266.65C210.77 266.65 217.42 260.1 222.14 250.7C226.86 241.3 216.64 227.28 216.64 227.28L215.32 224.83L205.33 249.23L215.32 245.87C215.32 245.87 207.99 259.32 206.55 261.43C205.11 263.54 203.9 265.29 203.52 269.11C203.14 272.93 198.97 286.78 185.68 295.49C172.39 304.2 165.38 307.52 161.84 311.88C158.3 316.24 157.11 323.98 159.13 337.12C161.15 350.26 167.18 382.97 167.18 382.97H177.62L177.64 382.94Z" fill="#B1B511"/>
|
||||
<path d="M150.28 565.92H257.19C263.95 565.92 269.42 560.44 269.42 553.69V401.39C269.42 396.59 265.53 392.7 260.73 392.7H260.6V549.45C260.61 553.28 257.5 556.37 253.67 556.35L151.3 555.73L150.26 565.92H150.28Z" fill="#B1B511"/>
|
||||
<path d="M140.35 247.23C140.35 247.23 153.1 247.48 162.17 242.28C170.41 237.56 173.39 232.31 174.83 230.31C177 227.27 182.38 233.27 177.65 242.98C172.92 252.69 185.03 241.37 183.33 226.81C181.63 212.25 179.21 208.49 179.21 208.49C179.21 208.49 170.05 226.57 159.47 234.59C148.89 242.61 138.71 244.26 138.71 244.26L140.37 247.24L140.35 247.23Z" fill="#DBDBDB"/>
|
||||
<path d="M63.05 253C61.76 250.78 60.35 246.57 60.62 243.22C60.89 239.87 62.62 238.15 63.82 237.84C65.02 237.53 55.94 224.81 55.94 224.81C55.94 224.81 53.7 231.54 56.95 241.4C60.2 251.26 63.32 256.16 63.18 254.58L63.05 253V253Z" fill="#DBDBDB"/>
|
||||
<path d="M68.39 244.4C71.68 246.87 78.84 248.86 84.17 248.86C89.5 248.86 92.04 247.39 92.04 247.39L91.38 239.86C91.38 239.86 84.49 245.6 78.01 245.36C71.53 245.12 68.39 244.41 68.39 244.41V244.4Z" fill="#DBDBDB"/>
|
||||
<path d="M171.36 471.24L254.54 394.37L169.56 463.75L171.36 471.24Z" fill="#B6D8D6"/>
|
||||
<path d="M11.55 395.26L95.03 473.03L97.24 465.84L11.55 395.26Z" fill="#B6D8D6"/>
|
||||
<path d="M5.95 550.56H251.05C252.98 550.56 254.54 549 254.54 547.07V384.54C254.54 384.54 260.49 383.66 260.61 392.71C260.73 401.76 260.62 549.46 260.62 549.46C260.62 549.46 260.49 556.48 246.84 557.43C233.19 558.38 9.93 556.4 9.93 556.4C9.93 556.4 5.95 556.76 5.95 553.66V550.56V550.56Z" fill="#B6D8D6"/>
|
||||
<path d="M86.87 431.77C86.87 431.77 85.48 422.85 85.44 420.3L85.39 417.74C85.39 417.74 88.17 417.18 92.51 414.63C96.85 412.08 100.07 407.36 100.07 407.36L90.51 397.69C90.51 397.69 91.18 405.94 83.73 409.12C76.28 412.3 76.03 412.29 76.17 412.96C76.31 413.63 76.75 418.57 79.83 423.18C82.91 427.79 86.88 431.77 86.88 431.77H86.87Z" fill="#B6D8D6"/>
|
||||
<path d="M96.67 408.63C96.67 408.63 100.73 413.29 113.11 415.68C124.14 417.81 131.5 415.97 131.5 415.97L126.27 411.33C126.27 411.33 112.77 412.87 107.17 409.95C101.57 407.03 96.67 405.1 96.67 405.1V408.63V408.63Z" fill="#B6D8D6"/>
|
||||
<path d="M152.24 428.63C152.24 428.63 149.92 420.26 148.77 415.31L147.62 410.35C147.62 410.35 156.21 405.95 161.9 398.81C167.59 391.67 169.58 384.51 169.58 384.51H156.43C156.43 384.51 152.39 395.01 147.41 399.87C142.43 404.73 137.64 406.61 137.64 406.61C137.64 406.61 141.01 415.42 144.32 419.64C147.63 423.86 152.25 428.63 152.25 428.63H152.24Z" fill="#B6D8D6"/>
|
||||
<path d="M11.55 539.42V416.1C11.55 415.02 12.4 414.13 13.48 414.08C14.63 414.03 15.59 414.95 15.59 416.1V539.42C15.59 540.54 14.69 541.44 13.57 541.44C12.45 541.44 11.55 540.54 11.55 539.42Z" fill="white"/>
|
||||
<path d="M123.93 231.98C127.22 240.34 134.75 242.94 142.9 240.24C154.4 236.91 161.76 225.18 169.49 216.12C177.79 206.1 185.95 195.02 197.52 188.51C188.13 197.83 182.14 209.25 174.85 220.09C169.83 227.53 165.18 235.33 157.73 240.99C152.88 244.67 146.85 246.9 140.78 247.21C131.84 247.78 123.19 241.46 123.94 231.99L123.93 231.98Z" fill="#0E1016"/>
|
||||
<path d="M140.5 244.41C139.88 252.6 140.98 261.04 142.12 269.17L138.16 268.34C139.94 265.98 141.69 263.78 143.95 261.83C144.78 261.12 145.89 260.42 146.99 260.2C150.78 259.37 154.13 259.16 157.75 258.19C171.83 255.13 181.28 242.36 181.39 228.24C181.34 221.76 180.13 214.85 176.47 209.44C175.14 207.09 178.29 207.99 179.97 206.88C181.75 205.71 182.15 202.65 183.72 205.08C186.96 212.44 187.48 220.7 186.71 228.6C185.67 238.21 180.89 247.25 173.71 253.65C169.38 257.38 163.98 259.79 158.53 261.19C154.98 262.23 150.86 262.57 147.48 263.64C143.52 266.37 141.19 272.12 138.77 276.15C137.28 265.51 135.64 254.71 136.6 243.95C136.96 241.33 140.78 241.8 140.52 244.41H140.5Z" fill="#0E1016"/>
|
||||
<path d="M153.41 239.26C153.41 239.26 154.37 243.22 158.19 246.71C162.01 250.2 164.6 251.1 164.6 251.1C164.6 251.1 165.84 245.7 165.39 239.85C164.94 234 163.21 229.92 163.21 229.92C163.21 229.92 160.44 233.46 157.91 235.64C155.38 237.82 153.4 239.27 153.4 239.27L153.41 239.26Z" fill="#0E1016"/>
|
||||
<path d="M140.77 294.62C144.36 285.43 159.13 284.22 167.6 282.43C190.27 278.44 204.78 263.89 214.21 243.56C214.21 243.51 216.98 245.8 216.99 245.79C210.33 249.47 203.07 252.88 195.35 253.08C198.89 251.63 202.19 250 205.46 248.24C210.09 245.72 214.92 242.65 219.38 239.84C213.39 261.52 197.4 281.72 174.32 285.77C172.35 286.15 170.15 286.43 168.15 286.58C159.95 287.43 145.65 287.08 140.77 294.63V294.62Z" fill="#0E1016"/>
|
||||
<path d="M201.59 250.35C205.75 243.1 209.71 235.32 211.86 227.21C212.93 223.12 213.61 218.84 214.13 214.54C214.61 210.23 214.94 205.87 215.01 201.47C215.97 205.78 216.45 210.2 216.59 214.64C216.96 223.62 215.55 232.82 211.84 241.03C210.66 243.69 209.37 246.27 207.86 248.75C207.03 250.13 200.99 251.3 201.59 250.35V250.35Z" fill="#0E1016"/>
|
||||
<path d="M206.56 264.2C206.28 273.72 202.08 282.8 196.03 290.03C189.86 297.04 180.9 301.27 172.41 304.17C168.79 305.62 164.34 306.26 162.12 309.76C160.08 313.3 160.21 317.65 160.09 321.81C160.09 324.74 160.08 327.72 160.23 330.58C161.43 348.08 164.88 365.64 169.69 382.48C170.08 383.78 169.03 383.58 167.72 383.96C166.38 384.36 165.28 385.12 164.96 383.76C163.55 377.94 162.29 372.1 161.21 366.24C158.57 351.58 156.11 336.72 156.44 321.76C156.66 308.69 157.93 304.5 170.77 299.88C181.65 295.59 191.46 290.51 197.03 279.88C199.71 275.05 201.7 269.63 202.04 264.12C202.21 261.24 206.48 261.27 206.56 264.2V264.2Z" fill="#0E1016"/>
|
||||
<path d="M93.48 234.12C93.73 240.88 87.12 247.03 80.29 247.16C69.72 247.96 62.98 241.65 59.08 232.57C53.73 220 49.86 206.15 39.65 196.52L37.97 194.82C39.9 194.78 42.07 194.65 43.97 194.93C45.31 195.12 46.55 195.35 47.79 196.08L45.87 196.91C47.36 185.87 50.93 175.28 55.66 165.22C56.66 163.22 58.31 167.64 58.09 168.11C57.18 170.08 56.32 172.11 55.47 174.13C52.3 181.59 50.09 189.43 48.33 197.33L47.94 199.09L46.42 198.16C44.68 197.08 42.32 196.9 40.29 196.75L40.92 194.98C46.76 199.2 51.29 205.08 54.89 211.23C58.48 217.39 61.42 223.92 64.44 229.98C67.3 235.49 70.71 240.54 77.29 240.96C78.23 241.08 79.27 241.13 80.25 241.18C85.58 241.41 90.11 238.18 93.47 234.11L93.48 234.12Z" fill="#0E1016"/>
|
||||
<path d="M94.44 211.75C86.88 193.62 89.17 173.07 95.11 154.82C103.77 127.42 128.74 96.38 148.78 75.82L153.34 71.11C149.83 84.55 146.56 98.2 143.76 111.79C138.7 134.88 137.12 158.17 133.92 181.61C133.04 187.5 131.61 193.46 129.46 199.07C127.38 204.7 124.78 210.21 120.73 214.73C122.24 208.89 124.08 203.42 125.67 197.85C130.18 180.84 130.83 163.34 133.31 145.87C136.22 122.37 141.72 99.26 148.36 76.55L151.28 78.21C142.86 87.6 134.89 97.44 127.32 107.52C118.9 118.9 110.94 130.65 104.76 143.31C100.1 153.14 96.96 163.73 94.79 174.35C92.28 186.77 93.25 199.16 94.44 211.76V211.75Z" fill="#0E1016"/>
|
||||
<path d="M169.57 136.05C175.94 138.15 182.34 140.66 188.17 144.02C189.66 144.88 191.13 145.81 192.52 146.84C193.01 147.21 192.22 151.49 191.46 150.79C191.16 150.49 190.71 150.13 190.28 149.79C187.3 147.51 184.02 145.67 180.65 144.01C176.88 142.13 172.56 140.42 168.58 139C166.65 138.32 167.62 135.42 169.58 136.04L169.57 136.05Z" fill="#0E1016"/>
|
||||
<path d="M138.05 129.14C142.39 129.59 146.69 130.3 150.93 131.26C151.74 131.46 151.14 131.96 151.02 132.58C150.9 133.2 150.4 133.8 150.4 133.8C146.23 133 142.02 132.44 137.8 132.15C135.79 131.97 136.04 128.97 138.06 129.14H138.05Z" fill="#0E1016"/>
|
||||
<path d="M90.91 137.05C96.16 134.38 101.68 132.69 107.38 131.36C109.08 131 109.68 133.52 107.99 133.96C105.12 134.66 102.28 135.49 99.51 136.5C96.75 137.51 93.97 138.65 91.5 140.06C89.8 140.99 90.63 137.2 90.92 137.06L90.91 137.05Z" fill="#0E1016"/>
|
||||
<path d="M146.15 138.19C151.47 128.09 161.6 120.6 172.75 118.15C177.9 116.88 183.57 116.64 188.88 116.36C183.17 121.2 178.46 127.27 174.25 133.42C167.5 143.28 161.53 153.73 155.97 164.34C153.18 169.69 150.47 175.08 147.92 180.55C149.5 174.69 151.52 168.96 153.82 163.34C158.44 152.07 163.98 141.17 170.87 131.08C174.44 126 178.23 121 183.07 117.02L184.26 120.11C167.82 121.79 157.81 126.3 146.16 138.19H146.15Z" fill="#0E1016"/>
|
||||
<path d="M91.42 147.31C89.53 138.42 89.26 128.97 91.62 120.13C92.39 117.16 93.35 114.29 94.6 111.43L97.2 113.33C83.34 126.58 73.01 144.8 71.93 164.2C71.01 176.11 74.51 187.14 77.99 198.6C73.14 192.07 70.27 184.26 68.95 176.25C64.5 149.31 79.41 122.62 100.24 106.34C97.16 113.67 94.33 121.71 92.99 129.54C92.04 135.38 91.71 141.34 91.43 147.3L91.42 147.31Z" fill="#0E1016"/>
|
||||
<path d="M190.68 159.35C191.11 151.76 191.51 144.32 191.76 136.95C192.09 129.32 190.85 121.99 191.95 114.14C192.49 110.07 194.27 105.68 197.32 102.47C199.9 99.78 203.28 97.12 205.94 94.7L222.94 79.99C239.56 65.22 256.8 50.77 269.53 32.39C273.68 26.38 277.65 20 280.85 13.48L286.62 0.899994C284.13 25.59 280.06 50.47 273.55 74.4C272.18 79.34 270.32 85.48 268.73 90.35C262.96 108.04 254.15 124.8 242.93 139.53C238.49 145.47 233.7 151.14 228.61 156.51C223.51 161.85 218.31 167.2 211.86 171.02C216.5 165.26 221.55 160.06 226.32 154.46C241.47 136.43 254.42 116.16 262.13 93.89C263.82 89.16 265.77 83.12 267.11 78.31C273.07 57.27 277.39 35.66 280.69 14.07L284.91 15.21C283.42 18.86 281.73 22.22 279.91 25.6C274.43 35.68 267.81 45.13 260.06 53.61C244.48 70.3 227.35 84.84 210.18 99.63C207.61 101.94 203.26 105.13 201.18 107.64C199.4 109.76 198.39 112.44 197.87 115.16C196.62 122.2 197.51 129.99 196.58 137.31C195.58 144.88 193.91 152.39 190.71 159.35H190.68Z" fill="#0E1016"/>
|
||||
<path d="M221.09 137.53C229.36 128.97 236.98 120.09 243.03 109.92C249.06 99.77 253.99 88.98 259.16 78.28C254.61 101.76 242.21 125.15 221.09 137.53V137.53Z" fill="#0E1016"/>
|
||||
<path d="M60.12 171.02C48.94 159.62 39.65 146.97 29.61 134.77C27.41 132.29 26.31 128.5 26.28 125.38C26.2 117.69 28.85 110.66 29.17 103.06C30.06 84.92 27.59 66.47 21.66 49.31C15.83 32.44 8.18 15.87 0 0C37.22 30.27 68.76 66.02 80.62 113.63C81.83 118.44 82.77 123.33 83.24 128.36C83.37 129.78 78.14 134.2 78.2 132.66C78.27 120.6 75 108.38 71.04 96.95C58.85 61.64 33.36 32.66 4.87 9.23L7.86 6.75C18.45 26.52 28.51 47.19 32.63 69.45C34.63 80.66 35.61 92.02 34.98 103.42C34.82 107.17 34.11 111.13 33.38 114.8C32.53 120.39 30.41 126.77 33.95 131.36C39.04 138 43.93 144.83 48.81 151.63C53.07 157.78 57.2 164.07 60.11 171.02H60.12Z" fill="#0E1016"/>
|
||||
<path d="M49.3 204.17C45.85 221.94 47.62 240.92 54.15 257.81L55.12 260.29L52.63 259.57C48 258.16 44.14 255.02 41.63 250.95L43.89 250.14C46.65 265.15 55.4 277.72 68.88 285.01C74.25 288.2 80.17 291.47 83.47 297.23C84.49 298.94 85.23 300.79 85.76 302.67C86.13 303.99 82.19 308.59 82.01 307.33C81.6 301.55 79.07 296.58 74.39 293.07C69.69 289.4 64.08 286.54 59.43 282.6C49.65 274.58 43.65 262.82 41.46 250.49L40.6 245.07C40.6 245.07 43.73 249.68 43.72 249.68C45.37 252.31 47.72 254.49 50.48 255.91C51.37 256.38 52.4 256.81 53.26 257.04L51.74 258.8C49.96 254.42 48.6 249.95 47.48 245.4C45.2 236.31 44.23 226.86 44.55 217.5C44.72 212.79 45.08 208.17 46.01 203.41C46.19 202.49 47.08 201.89 48 202.07C48.96 202.26 49.56 203.23 49.32 204.17H49.3Z" fill="#0E1016"/>
|
||||
<path d="M59.37 221.16C56.34 230.59 57.63 245.33 63.41 253.53C66.75 258.09 71.98 260.61 77.47 261.82C80.41 262.53 83.63 262.96 86.41 264.39C88.54 265.46 90.56 267.05 91.96 269.05C92.6 269.96 88.87 277.55 89.11 275.91C89.31 274.59 89.27 273.66 89.1 272.94C88.45 269.98 85.71 267.78 82.92 266.67C77.98 264.98 72.66 264.42 67.97 261.72C63.75 259.51 60.29 255.81 58.09 251.58C53.52 241.65 51.9 230.29 54.11 219.53C54.74 217.29 56.2 218.25 57.6 219.19C58.52 219.81 59.85 219.71 59.36 221.16H59.37Z" fill="#0E1016"/>
|
||||
<path d="M92.51 238.14C95.67 258.03 92.93 278.23 88.23 297.64C85.85 307.31 83.05 316.88 79.62 326.26C76.97 333.28 74.25 340.58 69.74 346.72C68.15 348.86 66.56 350.76 64.53 352.59C63.75 353.28 63.44 352.45 62.75 351.67C62.03 350.87 61.26 350.39 62.09 349.71C63.74 348.22 65.36 346.32 66.7 344.48C70.98 338.59 73.53 331.77 76.16 324.97C82.82 306.54 88.03 287.37 90.18 267.85C91.1 258.18 91.29 248.2 89.95 238.61C89.63 236.96 92.16 236.36 92.52 238.13L92.51 238.14Z" fill="#0E1016"/>
|
||||
<path d="M69.41 241.41C69.41 241.41 69.04 245.47 70.17 250C71.39 254.86 75.08 257.82 75.08 257.82C75.08 257.82 78.48 255.21 79.18 251.19C79.94 246.82 79.41 244.18 79.41 244.18C79.41 244.18 74.6 243.8 73.37 243.35C72.14 242.9 69.42 241.41 69.42 241.41H69.41Z" fill="#0E1016"/>
|
||||
<path d="M69.44 357.21C63.58 353.07 58.04 348.96 50.8 348.76C42.32 348.18 35.19 355.85 35.56 364.14C36.73 376.89 49.58 383.25 60.91 386.77C45.81 388.16 27.03 375.05 32.5 358.18C38.6 339.99 61.79 340.81 69.44 357.2V357.21Z" fill="#0E1016"/>
|
||||
<path d="M103.71 359.12C109.74 348.5 125.26 337.31 137.58 344.5C142.98 347.63 145.85 354.16 145.29 360.18C144.26 371.87 133.37 380.55 122.59 383.13C131.05 377.83 140.28 370.34 141.5 359.89C142.24 353.34 137.81 347.13 131.2 346.41C127.95 345.91 124.74 346.95 121.7 348.08C115.05 350.55 109.44 354.83 103.71 359.13V359.12Z" fill="#0E1016"/>
|
||||
<path d="M110.28 376.08C110.28 376.08 117.23 367.29 122.45 363.71C127.67 360.13 130.84 360.74 131.86 362.18C132.88 363.62 132.19 366.86 127.95 367.97C124.04 368.99 120.79 370.16 116.75 372.34C113.05 374.34 110.28 376.09 110.28 376.09V376.08Z" fill="#0E1016"/>
|
||||
<path d="M61.25 376.88C59.3 373.19 53.13 367.52 48.37 365.59C43.61 363.66 42.31 366.66 42.67 368.17C43.16 370.22 45.72 370.99 50.75 372.5C56.67 374.28 61.24 376.88 61.24 376.88H61.25Z" fill="#0E1016"/>
|
||||
<path d="M123.93 284.38C123.56 293.38 122.92 306.01 122.98 314.85C123.11 324.86 124.33 334.97 125.54 344.9C125.7 345.99 124.93 347.01 123.84 347.15C122.79 347.29 121.83 346.57 121.64 345.54C120.71 340.44 120.08 335.36 119.73 330.22C118.49 314.86 120.85 299.39 123.93 284.37V284.38Z" fill="#0E1016"/>
|
||||
<path d="M143.62 341.89C143.21 335.24 143.71 328.69 144.43 322.08C145.36 314.87 146.28 306.7 151.64 301.24C148.35 307.79 147.79 315.12 147.17 322.34C146.69 329.5 146.57 336.89 147.48 343.96C147.54 344.42 147.46 344.81 147.29 345.12C147.29 345.12 143.69 342.73 143.64 341.88L143.62 341.89Z" fill="#0E1016"/>
|
||||
<path d="M157.18 387.4C161.37 376.69 160.41 364.27 155.43 354.04C152.76 348.62 148.5 343.82 143.07 341.11C142.3 340.74 141.53 340.39 140.77 340.05C141.72 340.9 142.68 341.67 143.63 342.49C149.72 348.33 153.5 356.35 154.61 364.69C155.54 370.84 155.26 377.45 153.47 383.38C150.17 392.77 143.3 400.95 134.89 406.19C136.57 410.07 138.32 413.86 140.55 417.45C141.49 418.94 142.45 420.38 143.42 421.8C138.38 419.3 133.81 415.8 130.06 411.62C129.03 410.49 128.17 409.32 127.39 408.13C116.93 410.53 105 408.18 96.8 401.07C92.3 397.48 88.58 391.49 85.45 386.71L86.68 394.39C86.86 396.15 86.78 398.14 86.5 399.93C85.78 405.13 81.68 409.66 76.49 410.38L74.43 410.67C74.78 412.79 75.17 415.01 75.78 417.04C76.59 419.95 77.93 422.68 79.44 425.31C79.25 425.19 79.05 425.08 78.86 424.97C73.93 422.03 70.84 416.98 68.43 411.94L67.97 410.93L66.76 410.84C49.57 409.02 40.2 394.65 41.74 378.51L40.33 376.55C40.47 374.54 37.54 374.28 37.32 376.29C36.59 387.35 40.11 398.93 48.33 406.58C52.5 410.36 57.65 413.08 63.18 414.18C63.97 414.33 64.71 414.45 65.52 414.53C70.66 424.27 76.07 428.49 86.87 431.76L84.7 428.14C81.93 423.6 79.39 418.87 78.4 413.66C82.36 413.01 85.7 410.61 87.95 407.76C89.51 405.79 90.43 403.89 90.66 401.42C94.32 406.01 98.47 408.14 103.99 410.42C110.82 413.19 118.53 414.3 125.77 412.58C128.46 415.87 131.65 418.54 135.08 421.02C140.16 424.71 146.27 426.81 152.25 428.63C149.36 424.46 146.16 419.94 143.52 415.64C142.02 413.11 140.67 410.4 139.49 407.68C147.49 403.24 153.63 395.79 157.19 387.41L157.18 387.4Z" fill="#0E1016"/>
|
||||
<path d="M216.62 166.67C229.07 181.17 238.86 197.71 248.05 214.38C306.9 323.51 329.29 447.36 349.07 568.49C350.61 577.83 352.12 587.18 353.87 596.42C354.14 597.76 353.26 599.08 351.91 599.33C350.57 599.58 349.27 598.7 349.02 597.36C347.28 587.98 345.79 578.62 344.29 569.27C325.02 448.49 303.04 325.05 245.19 215.94C238.46 203.5 231.14 191.37 222.92 179.88C220.18 176.1 217.29 172.27 214.21 168.83C213.62 168.17 214.62 168.55 215.29 167.96C215.95 167.37 216.02 166.01 216.62 166.68V166.67Z" fill="#0E1016"/>
|
||||
<path d="M144.83 596.56C146.93 583.43 148.14 570.11 148.6 556.81C148.71 555.07 150.03 555.65 151.32 555.73C152.61 555.81 153.84 555.39 153.73 557.14C152.58 570.19 150.87 584.27 148.94 597.23C148.47 599.98 144.41 599.34 144.84 596.56H144.83Z" fill="#0E1016"/>
|
||||
<path d="M216.55 216.68C251.43 271.93 276.5 332.66 298.69 393.91C320.2 455.25 339.67 517.79 350.82 581.93C351 583 350.27 584.02 349.19 584.19C348.12 584.36 347.11 583.64 346.93 582.58C344.21 566.63 340.67 550.81 336.82 535.06C329.14 503.78 319.67 472.3 309.67 441.66C286.67 372.56 259.96 304.27 223.75 240.94C220.71 235.71 217.59 230.47 214.35 225.4C213.26 223.69 215.27 215.52 215.27 215.52C215.73 215.76 216.17 216.14 216.56 216.68H216.55Z" fill="#0E1016"/>
|
||||
<path d="M251.13 223.4C257.61 221.28 264.73 221.6 271.03 224.16C277.13 226.99 281.46 232.01 284.84 237.64L281.91 238.71C281.36 236.46 280.5 234.1 279.55 231.89C274.67 220.15 265.75 212.05 253.63 208.35C250.07 207.17 246.41 206.87 242.68 206.06C241.08 205.65 236.7 204.18 238.47 201.74C239.08 201.05 240.15 201 240.82 201.63C242.11 202.84 244.57 203.12 247.05 203.54C249.55 203.93 252.13 204.47 254.61 205.25C270.54 210.14 280.82 222.1 285 238.02L287.88 249.76C285.01 244.81 281.1 235.95 276.8 232.12C270.93 225.89 261.02 224.38 253.54 228.36C251.78 229.51 252.59 228.06 251.87 226.81C251.07 225.43 248.94 224.14 251.16 223.38L251.13 223.4Z" fill="#0E1016"/>
|
||||
<path d="M302.76 349.1C303.87 351.94 305.94 354.64 307.77 357.14C312.85 363.95 318.98 369.75 325.21 375.52C331.43 381.73 336.37 389.19 339.63 397.37C342.27 403.96 343.29 411 343.27 418.13C343.27 418.13 342.85 432.46 342.85 432.47L339.88 418.43C338.68 412.88 336.36 407.24 333.16 402.47C328.3 395.35 321.05 389.65 312.77 387.14C311.86 386.88 310.87 386.64 310.08 386.54C309.02 386.41 308.27 385.44 308.4 384.37C308.54 383.23 309.66 382.46 310.77 382.73C312.99 383.28 314.82 384.02 316.82 384.93C330.23 391.05 339.59 403.58 343.23 417.7L339.84 417.99C339.91 414.77 339.65 411.36 339.12 408.1C337.51 398.28 332.59 389.17 326.11 381.67C323.89 379.1 321.55 376.92 318.96 374.65C313.77 370.01 309 364.96 304.67 359.5C302.76 357.04 300.86 354.53 299.43 351.71C299.21 351.3 299.01 350.76 298.82 350.23C298.44 349.17 299 348 300.06 347.62C301.21 347.2 302.45 347.9 302.74 349.08L302.76 349.1Z" fill="#0E1016"/>
|
||||
<path d="M339.73 517.08C354.7 529.44 366.38 546.33 369.99 565.6C371.31 570.8 371.51 580.67 371.89 586.1L368.2 574.43C366.1 568.03 363.03 561.56 358.72 556.35C354.45 551.43 348.53 547.29 342.15 545.77C341.02 545.6 340.25 544.53 340.44 543.41C340.63 542.19 341.91 541.41 343.09 541.8C345.04 542.44 346.57 543.21 348.31 544.14C360.2 550.53 366.55 561.19 371.12 573.49L368.13 574.03C367.53 564.87 364.8 555.58 360.54 547.4C356.14 539.19 350.13 531.87 343.25 525.6C341.27 523.81 339.17 522.05 337.09 520.55C334.78 518.79 337.37 515.32 339.73 517.09V517.08Z" fill="#0E1016"/>
|
||||
<path d="M180.24 328.93C162.69 328.93 157.99 326.88 157.74 326.76C157.11 326.47 156.84 325.73 157.13 325.1C157.42 324.48 158.15 324.2 158.78 324.49C158.94 324.56 167.84 328.14 205.35 325.39C229.46 323.62 258.8 318.91 262.16 311.45C262.44 310.82 263.18 310.54 263.81 310.82C264.44 311.1 264.72 311.84 264.44 312.47C261.16 319.76 239.69 325.38 205.54 327.88C195.05 328.65 186.76 328.93 180.25 328.93H180.24Z" fill="#0E1016"/>
|
||||
<path d="M260.59 412.59C259.93 412.59 259.38 412.08 259.34 411.42C259.3 410.73 259.82 410.14 260.51 410.09C266.14 409.74 271.67 409.35 276.5 408.95C294.77 407.44 298.49 404.54 298.52 404.51C299.03 404.05 299.82 404.09 300.29 404.61C300.75 405.12 300.71 405.92 300.19 406.38C299.79 406.73 295.77 409.88 276.71 411.45C271.86 411.85 266.32 412.24 260.67 412.59C260.64 412.59 260.62 412.59 260.59 412.59V412.59Z" fill="#0E1016"/>
|
||||
<path d="M260.52 516.12C259.86 516.12 259.31 515.61 259.27 514.94C259.23 514.25 259.76 513.66 260.45 513.62L262.95 513.47C306.52 510.87 326.49 508.83 329.61 503.61C329.96 503.02 330.73 502.82 331.32 503.18C331.91 503.53 332.11 504.3 331.75 504.89C328.05 511.08 311.36 513.08 263.09 515.96L260.58 516.11C260.58 516.11 260.53 516.11 260.51 516.11L260.52 516.12Z" fill="#0E1016"/>
|
||||
<path d="M156.09 380.98L247.28 381.31H250.13C255.91 380.75 261.5 384.32 262.26 390.39C262.52 404.5 262.22 427.75 262.29 441.91C262.23 476.43 262.56 512.85 263.01 547.35C263.44 554.44 258.01 559.91 250.89 559.47C250.12 559.49 247.45 559.45 246.62 559.47C177.04 559.32 88.35 559.12 18.65 558.88L12.95 558.86C12.52 558.86 11.9 558.84 11.33 558.79C6.11 558.3 1.84 553.45 2.03 548.21C2.29 510.67 2.34 434.23 2.39 397.18C2.42 393.18 1.81 388.6 4.72 385.34C6.73 382.79 10.29 381.47 13.42 381.7H16.27L39.07 381.68C41 381.71 40.94 384.57 39.07 384.57L16.27 384.55H13.42C12.4 384.55 11.69 384.55 10.89 384.72C7.76 385.37 5.28 388.31 5.24 391.53C4.95 429.96 5.59 506.38 5.57 545.36V548.21C5.46 552.18 8.98 555.51 12.93 555.21C84.84 554.94 174.77 554.79 246.6 554.61H249.45H250.87H251.58H251.93L252.19 554.59C255.4 554.51 258.17 551.63 258.15 548.42C258.18 548.1 258.15 544.99 258.17 544.51C258.81 503.42 258.94 460.25 258.84 419.13L258.81 396.33V393.48V392.06C258.81 391.8 258.8 391.63 258.79 391.44C258.73 389.74 257.95 388.1 256.75 386.9C254.89 385.11 252.7 384.8 250.11 384.94H247.26L156.07 385.27C153.25 385.23 153.22 381.05 156.07 380.99L156.09 380.98Z" fill="#0E1016"/>
|
||||
<path d="M95.56 466.74C68.17 444 38.65 418.54 11.55 395.26C31.76 411.09 56.6 430.52 76.55 446.44C83.73 452.18 90.93 457.89 98.05 463.71C99.03 464.53 97.86 465.08 97.24 465.84C96.62 466.6 96.58 467.55 95.56 466.74V466.74Z" fill="#0E1016"/>
|
||||
<path d="M254.54 394.37C234.23 412.87 213.13 430.45 192.02 448C184.98 453.84 177.9 459.64 170.77 465.38C170.02 465.99 169.46 465.54 168.86 464.79C168.26 464.05 167.83 463.29 168.55 462.68C175.56 456.8 182.63 450.99 189.74 445.23C211.08 427.95 232.44 410.68 254.54 394.38V394.37Z" fill="#0E1016"/>
|
||||
<path d="M173.2 476.48C173.46 504.82 144.19 524.28 118.15 513.22C103.63 507.24 93.55 492.19 93.57 476.48C93.34 448.05 122.41 428.72 148.55 439.84C163.03 445.87 173.24 460.72 173.21 476.48H173.2ZM170.01 476.48C170.11 443.49 130.79 427.67 107.54 450.65C84.52 473.95 100.44 513.03 133.37 513.1C153.11 513.28 170.16 496.22 170.02 476.47L170.01 476.48Z" fill="#0E1016"/>
|
||||
<path d="M128.71 493.09L107.69 473.05L112.52 467.98L128.44 483.16L171.93 436.98L177.03 441.78L128.71 493.09Z" fill="#0E1016"/>
|
||||
<path d="M374.54 599.9H123.19C121.53 599.9 120.19 598.56 120.19 596.9C120.19 595.24 121.53 593.9 123.19 593.9H374.54C376.2 593.9 377.54 595.24 377.54 596.9C377.54 598.56 376.2 599.9 374.54 599.9V599.9Z" fill="#ADD63D"/>
|
||||
<path d="M256.25 316.08C257.98 315.21 258.92 314.05 259.1 312.39C259.28 310.73 258.63 309.3 258.08 308.1C257.53 306.9 259.88 308.05 261.1 310.52C262.32 312.99 262.79 313.82 261.32 314.64C259.85 315.46 256.26 317.07 256.26 317.07V316.07L256.25 316.08Z" fill="#0E1016"/>
|
||||
<path d="M257.89 317.99C259.39 317.19 260.74 317.3 261.85 318.04C262.77 318.66 263.07 319.21 263.67 320.59C264.27 321.97 263.21 315.68 262.59 314.9C261.97 314.12 261.86 314.24 260.34 314.9C258.82 315.56 257.88 317.98 257.88 317.98L257.89 317.99Z" fill="#0E1016"/>
|
||||
<path d="M293.86 406.31C294.6 406.1 295.48 405.59 295.86 404.44C296.24 403.29 295.97 401.99 295.71 401.27C295.45 400.55 297.48 401.79 297.97 403.63C298.46 405.47 299.63 406.21 297.84 406.62C296.05 407.03 293.86 406.31 293.86 406.31Z" fill="#0E1016"/>
|
||||
<path d="M294.19 408.8C295.48 408.44 296.75 408.5 297.5 409.07C298.25 409.64 298.91 410.26 299.34 411.48C299.77 412.7 299.09 409.43 298.72 408.04C298.35 406.65 297.83 406.63 297.83 406.63L293.38 407.91L294.18 408.81L294.19 408.8Z" fill="#0E1016"/>
|
||||
<path d="M325.22 506.58C326.09 506.23 327.08 505.43 327.63 504.06C328.04 503.01 328.01 501.67 327.63 500.31C327.25 498.95 329.31 502.16 329.63 503.62C329.95 505.08 330.17 504.81 328.8 505.94C327.43 507.07 325.22 506.79 325.22 506.79V506.58V506.58Z" fill="#0E1016"/>
|
||||
<path d="M325.22 509.22C326.34 508.81 327.83 508.64 328.87 509.22C329.91 509.8 330.44 510.44 330.87 512.04C331.3 513.64 331.5 511.84 330.87 509.24C330.24 506.64 329.86 505.93 329.86 505.93L325.5 507.47L325.21 509.22H325.22Z" fill="#0E1016"/>
|
||||
<path d="M160.14 319.82C160.36 315.77 164.42 312.72 170.07 309.16C176.37 305.2 187.01 300.42 195.02 291.13C201.23 283.92 181.86 296.89 175.75 299.76C169.64 302.63 164.89 303.62 161.84 307.12C158.79 310.62 159.06 317.26 159.06 317.26L160.14 319.82Z" fill="#0E1016"/>
|
||||
<path d="M187.37 200.83C185.8 203.13 186.02 210.41 186.48 214.85C186.94 219.29 181.41 215.65 181.41 210.48C181.41 205.31 186.16 200.33 186.28 200.15C186.4 199.97 187.38 200.82 187.38 200.82L187.37 200.83Z" fill="#0E1016"/>
|
||||
<path d="M116.76 345.98C118.33 345.1 120.96 342.41 120.49 338.1C120.02 333.79 123.03 338.28 123.13 341.07C123.23 343.86 125.83 343.67 122.15 345.33C118.47 346.99 116.77 345.98 116.77 345.98H116.76Z" fill="#0E1016"/>
|
||||
<path d="M124.29 334.25C124.47 335.91 124.89 338.7 125.97 340.06C127.05 341.42 128.18 342.04 130.04 342.24C131.9 342.44 124.66 344.93 123.95 343.42C123.24 341.91 123.03 335.78 123.03 335.78L124.29 334.26V334.25Z" fill="#0E1016"/>
|
||||
<path d="M59.38 278.35C62.08 280.6 66.34 283.52 72.05 284.99C77.76 286.46 81.26 287.84 81.26 287.84C81.26 287.84 77.43 287.27 73.6 287.02C73.03 286.98 72.6 287.24 73.41 287.75C75.25 288.89 70.34 287.51 66.18 285.15C61.29 282.38 59.38 278.35 59.38 278.35V278.35Z" fill="#0E1016"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_118_2691">
|
||||
<rect width="377.54" height="599.9" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 46 KiB |
8
frontend/public/locales/en/activity.json
Normal file
8
frontend/public/locales/en/activity.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"event": {
|
||||
"readSecrets": "Secrets Viewed",
|
||||
"updateSecrets": "Secrets Updated",
|
||||
"addSecrets": "Secrets Added",
|
||||
"deleteSecrets": "Secrets Deleted"
|
||||
}
|
||||
}
|
@ -1238,6 +1238,7 @@ module.exports = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
"./ee/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
@ -1382,12 +1383,12 @@ module.exports = {
|
||||
"0%": {
|
||||
transform: "scale(0.2)",
|
||||
opacity: 0,
|
||||
transform: "translateY(120%)",
|
||||
// transform: "translateY(120%)",
|
||||
},
|
||||
"100%": {
|
||||
transform: "scale(1)",
|
||||
opacity: 1,
|
||||
transform: "translateY(100%)",
|
||||
// transform: "translateY(100%)",
|
||||
},
|
||||
},
|
||||
popright: {
|
||||
@ -1410,12 +1411,12 @@ module.exports = {
|
||||
"0%": {
|
||||
transform: "scale(0.2)",
|
||||
opacity: 0,
|
||||
transform: "translateY(80%)",
|
||||
// transform: "translateY(80%)",
|
||||
},
|
||||
"100%": {
|
||||
transform: "scale(1)",
|
||||
opacity: 1,
|
||||
transform: "translateY(100%)",
|
||||
// transform: "translateY(100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Reference in New Issue
Block a user