mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
20 Commits
infisical/
...
daniel/rec
Author | SHA1 | Date | |
---|---|---|---|
|
1c90df9dd4 | ||
|
702cd0d403 | ||
|
75267987fc | ||
|
d734a3f6f4 | ||
|
cbb749e34a | ||
|
9f23106c6c | ||
|
1e7744b498 | ||
|
44c736facd | ||
|
51928ddb47 | ||
|
c7cded4af6 | ||
|
8b56e20b42 | ||
|
39c2c37cc0 | ||
|
3131ae7dae | ||
|
5315a67d74 | ||
|
79de7f9f5b | ||
|
71ffed026d | ||
|
ee98b15e2b | ||
|
945d81ad4b | ||
|
d175256bb4 | ||
|
ee0c79d018 |
@@ -141,6 +141,12 @@ export const PROJECTS = {
|
|||||||
},
|
},
|
||||||
ROLLBACK_TO_SNAPSHOT: {
|
ROLLBACK_TO_SNAPSHOT: {
|
||||||
secretSnapshotId: "The ID of the snapshot to rollback to."
|
secretSnapshotId: "The ID of the snapshot to rollback to."
|
||||||
|
},
|
||||||
|
LIST_INTEGRATION: {
|
||||||
|
workspaceId: "The ID of the project to list integrations for."
|
||||||
|
},
|
||||||
|
LIST_INTEGRATION_AUTHORIZATION: {
|
||||||
|
workspaceId: "The ID of the project to list integration auths for."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -215,7 +221,8 @@ export const SECRETS = {
|
|||||||
|
|
||||||
export const RAW_SECRETS = {
|
export const RAW_SECRETS = {
|
||||||
LIST: {
|
LIST: {
|
||||||
recursive: "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories.",
|
recursive:
|
||||||
|
"Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.",
|
||||||
workspaceId: "The ID of the project to list secrets from.",
|
workspaceId: "The ID of the project to list secrets from.",
|
||||||
workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
|
workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
|
||||||
environment: "The slug of the environment to list secrets from.",
|
environment: "The slug of the environment to list secrets from.",
|
||||||
@@ -502,11 +509,8 @@ export const INTEGRATION_AUTH = {
|
|||||||
url: "",
|
url: "",
|
||||||
namespace: "",
|
namespace: "",
|
||||||
refreshToken: "The refresh token for integration authorization."
|
refreshToken: "The refresh token for integration authorization."
|
||||||
},
|
|
||||||
LIST_AUTHORIZATION: {
|
|
||||||
workspaceId: "The ID of the project to list integration auths for."
|
|
||||||
}
|
}
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const INTEGRATION = {
|
export const INTEGRATION = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
|
@@ -7,7 +7,7 @@ import {
|
|||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { INTEGRATION_AUTH, PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -326,8 +326,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "List integrations for a project.",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION.workspaceId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -370,7 +376,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim().describe(INTEGRATION_AUTH.LIST_AUTHORIZATION.workspaceId)
|
workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION_AUTHORIZATION.workspaceId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -146,7 +146,27 @@ export const integrationServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
const deletedIntegration = await integrationDAL.deleteById(id);
|
const deletedIntegration = await integrationDAL.transaction(async (tx) => {
|
||||||
|
// delete integration
|
||||||
|
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
|
||||||
|
|
||||||
|
// check if there are other integrations that share the same integration auth
|
||||||
|
const integrations = await integrationDAL.find(
|
||||||
|
{
|
||||||
|
integrationAuthId: integration.integrationAuthId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (integrations.length === 0) {
|
||||||
|
// no other integration shares the same integration auth
|
||||||
|
// -> delete the integration auth
|
||||||
|
await integrationAuthDAL.deleteById(integration.integrationAuthId, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedIntegrationResult;
|
||||||
|
});
|
||||||
|
|
||||||
return { ...integration, ...deletedIntegration };
|
return { ...integration, ...deletedIntegration };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -126,13 +126,11 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findProjectById = async (id: string) => {
|
const findProjectById = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const workspaces = await db(TableName.ProjectMembership)
|
const workspaces = await db(TableName.Project)
|
||||||
.where(`${TableName.Project}.id`, id)
|
.where(`${TableName.Project}.id`, id)
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.Project),
|
selectAllTableCols(TableName.Project),
|
||||||
db.ref("id").withSchema(TableName.Project).as("_id"),
|
|
||||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||||
@@ -141,10 +139,11 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
{ column: `${TableName.Project}.name`, order: "asc" },
|
{ column: `${TableName.Project}.name`, order: "asc" },
|
||||||
{ column: `${TableName.Environment}.position`, order: "asc" }
|
{ column: `${TableName.Environment}.position`, order: "asc" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const project = sqlNestRelationships({
|
const project = sqlNestRelationships({
|
||||||
data: workspaces,
|
data: workspaces,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
|
parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "envId",
|
key: "envId",
|
||||||
@@ -174,14 +173,12 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
|
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const projects = await db(TableName.ProjectMembership)
|
const projects = await db(TableName.Project)
|
||||||
.where(`${TableName.Project}.slug`, slug)
|
.where(`${TableName.Project}.slug`, slug)
|
||||||
.where(`${TableName.Project}.orgId`, orgId)
|
.where(`${TableName.Project}.orgId`, orgId)
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.Project),
|
selectAllTableCols(TableName.Project),
|
||||||
db.ref("id").withSchema(TableName.Project).as("_id"),
|
|
||||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||||
@@ -194,7 +191,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
const project = sqlNestRelationships({
|
const project = sqlNestRelationships({
|
||||||
data: projects,
|
data: projects,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
|
parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "envId",
|
key: "envId",
|
||||||
|
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "@app/lib/crypto";
|
} from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy, unique } from "@app/lib/fn";
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
||||||
@@ -92,7 +93,8 @@ const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
|
|||||||
const generatePaths = (
|
const generatePaths = (
|
||||||
map: FolderMap,
|
map: FolderMap,
|
||||||
parentId: string = "null",
|
parentId: string = "null",
|
||||||
basePath: string = ""
|
basePath: string = "",
|
||||||
|
currentDepth: number = 0
|
||||||
): { path: string; folderId: string }[] => {
|
): { path: string; folderId: string }[] => {
|
||||||
const children = map[parentId || "null"] || [];
|
const children = map[parentId || "null"] || [];
|
||||||
let paths: { path: string; folderId: string }[] = [];
|
let paths: { path: string; folderId: string }[] = [];
|
||||||
@@ -105,13 +107,20 @@ const generatePaths = (
|
|||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`;
|
const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`;
|
||||||
|
|
||||||
|
// Add the current path
|
||||||
paths.push({
|
paths.push({
|
||||||
path: currPath,
|
path: currPath,
|
||||||
folderId: child.id
|
folderId: child.id
|
||||||
}); // Add the current path
|
});
|
||||||
|
|
||||||
// Recursively generate paths for children, passing down the formatted pathh
|
// We make sure that the recursion depth doesn't exceed 20.
|
||||||
const childPaths = generatePaths(map, child.id, currPath);
|
// We do this to create "circuit break", basically to ensure that we can't encounter any potential memory leaks.
|
||||||
|
if (currentDepth >= 20) {
|
||||||
|
logger.info(`generatePaths: Recursion depth exceeded 20, breaking out of recursion [map=${JSON.stringify(map)}]`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Recursively generate paths for children, passing down the formatted path
|
||||||
|
const childPaths = generatePaths(map, child.id, currPath, currentDepth + 1);
|
||||||
paths = paths.concat(
|
paths = paths.concat(
|
||||||
childPaths.map((p) => ({
|
childPaths.map((p) => ({
|
||||||
path: p.path,
|
path: p.path,
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List Project Integrations"
|
||||||
|
openapi: "GET /api/v1/workspace/{workspaceId}/integrations"
|
||||||
|
---
|
Binary file not shown.
After Width: | Height: | Size: 300 KiB |
@@ -519,7 +519,8 @@
|
|||||||
"api-reference/endpoints/integrations/delete-auth-by-id",
|
"api-reference/endpoints/integrations/delete-auth-by-id",
|
||||||
"api-reference/endpoints/integrations/create",
|
"api-reference/endpoints/integrations/create",
|
||||||
"api-reference/endpoints/integrations/update",
|
"api-reference/endpoints/integrations/update",
|
||||||
"api-reference/endpoints/integrations/delete"
|
"api-reference/endpoints/integrations/delete",
|
||||||
|
"api-reference/endpoints/integrations/list-project-integrations"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -121,24 +121,35 @@ Without email configuration, Infisical's core functions like sign-up/login and s
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="AWS SES">
|
<Accordion title="AWS SES">
|
||||||
1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console.
|
<Steps>
|
||||||
2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials
|
<Step title="Create a verifed identity">
|
||||||
|
This will be used to verify the email you are sending from.
|
||||||
|

|
||||||
|
<Info>
|
||||||
|
If you AWS SES is under sandbox mode, you will only be able to send emails to verified identies.
|
||||||
|
</Info>
|
||||||
|
</Step>
|
||||||
|
<Step title="Create an account and configure AWS SES">
|
||||||
|
Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
</Step>
|
||||||
3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables:
|
<Step title="Set up your SMTP environment variables">
|
||||||
|
With your AWS SES SMTP credentials, you can now set up your SMTP environment variables for your Infisical instance.
|
||||||
|
|
||||||
```
|
```
|
||||||
SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
|
SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
|
||||||
SMTP_USERNAME=xxx # your SMTP username
|
SMTP_USERNAME=xxx # your SMTP username
|
||||||
SMTP_PASSWORD=xxx # your SMTP password
|
SMTP_PASSWORD=xxx # your SMTP password
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
SMTP_SECURE=false
|
||||||
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
|
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
Remember that you will need to restart Infisical for this to work properly.
|
Remember that you will need to restart Infisical for this to work properly.
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { Button } from "../v2";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoEnvironmentsBanner = ({ projectId }: IProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
|
||||||
|
<div className="flex w-full flex-col text-sm">
|
||||||
|
<span className="mb-2 text-lg font-semibold">
|
||||||
|
No environments in your project was found
|
||||||
|
</span>
|
||||||
|
<p className="prose">
|
||||||
|
In order to use integrations, you need to create at least one environment in your project.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="my-2">
|
||||||
|
<Button onClick={() => router.push(`/project/${projectId}/settings#environments`)}>
|
||||||
|
Add environments
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,10 +1,17 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { NoEnvironmentsBanner } from "@app/components/integrations/NoEnvironmentsBanner";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { DeleteActionModal, Skeleton, Tooltip } from "@app/components/v2";
|
import { DeleteActionModal, Skeleton, Tooltip } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
|
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
|
||||||
|
|
||||||
@@ -31,18 +38,32 @@ export const CloudIntegrationSection = ({
|
|||||||
"deleteConfirmation"
|
"deleteConfirmation"
|
||||||
] as const);
|
] as const);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const isEmpty = !isLoading && !cloudIntegrations?.length;
|
const isEmpty = !isLoading && !cloudIntegrations?.length;
|
||||||
|
|
||||||
const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
|
const sortedCloudIntegrations = useMemo(() => {
|
||||||
|
const sortedIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
if (currentWorkspace?.environments.length === 0) {
|
||||||
|
return sortedIntegrations.map((integration) => ({ ...integration, isAvailable: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedIntegrations;
|
||||||
|
}, [cloudIntegrations, currentWorkspace?.environments]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<div className="px-5">
|
||||||
|
{currentWorkspace?.environments.length === 0 && (
|
||||||
|
<NoEnvironmentsBanner projectId={currentWorkspace.id} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||||
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
|
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
|
||||||
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
|
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
|
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
|
||||||
{isLoading &&
|
{isLoading &&
|
||||||
Array.from({ length: 12 }).map((_, index) => (
|
Array.from({ length: 12 }).map((_, index) => (
|
||||||
|
@@ -251,7 +251,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
Don't have an acount yet? {t("login.create-account")}
|
Don't have an account yet? {t("login.create-account")}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user