mirror of
https://github.com/Infisical/infisical.git
synced 2025-06-29 04:31:59 +00:00
Compare commits
32 Commits
misc/add-p
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
194fbb79f2 | |||
faaba8deb7 | |||
ae21b157a9 | |||
6167c70a74 | |||
0ecf75cbdb | |||
3f8aa0fa4b | |||
6487c83bda | |||
c08fbbdab2 | |||
a4aa65bb81 | |||
258d19cbe4 | |||
de91356127 | |||
ccb07942de | |||
3d278b0925 | |||
956fb2efb4 | |||
13894261ce | |||
d7ffa70906 | |||
b8fa7c5bb6 | |||
2baacfcd8f | |||
31c11f7d2a | |||
c5f06dece4 | |||
662e79ac98 | |||
17249d603b | |||
9bdff9c504 | |||
4552ce6ca4 | |||
ba4b8801eb | |||
36a5f728a1 | |||
502429d914 | |||
27abfa4fff | |||
4d43accc8a | |||
3c89a69410 | |||
e741b63e63 | |||
60749cfc43 |
@ -100,10 +100,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
|
||||
// Filter by date range
|
||||
if (startDate) {
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
||||
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
|
||||
}
|
||||
if (endDate) {
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
|
||||
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]);
|
||||
}
|
||||
|
||||
// we timeout long running queries to prevent DB resource issues (2 minutes)
|
||||
|
@ -31,7 +31,7 @@ export type TListProjectAuditLogDTO = {
|
||||
|
||||
export type TCreateAuditLogDTO = {
|
||||
event: Event;
|
||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
|
||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor;
|
||||
orgId?: string;
|
||||
projectId?: string;
|
||||
} & BaseAuthData;
|
||||
@ -229,7 +229,10 @@ export enum EventType {
|
||||
GET_APP_CONNECTION = "get-app-connection",
|
||||
CREATE_APP_CONNECTION = "create-app-connection",
|
||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||
DELETE_APP_CONNECTION = "delete-app-connection"
|
||||
DELETE_APP_CONNECTION = "delete-app-connection",
|
||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||
READ_SHARED_SECRET = "read-shared-secret"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -252,6 +255,8 @@ interface ScimClientActorMetadata {}
|
||||
|
||||
interface PlatformActorMetadata {}
|
||||
|
||||
interface UnknownUserActorMetadata {}
|
||||
|
||||
export interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
@ -267,6 +272,11 @@ export interface PlatformActor {
|
||||
metadata: PlatformActorMetadata;
|
||||
}
|
||||
|
||||
export interface UnknownUserActor {
|
||||
type: ActorType.UNKNOWN_USER;
|
||||
metadata: UnknownUserActorMetadata;
|
||||
}
|
||||
|
||||
export interface IdentityActor {
|
||||
type: ActorType.IDENTITY;
|
||||
metadata: IdentityActorMetadata;
|
||||
@ -1907,6 +1917,35 @@ interface DeleteAppConnectionEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSharedSecretEvent {
|
||||
type: EventType.CREATE_SHARED_SECRET;
|
||||
metadata: {
|
||||
id: string;
|
||||
accessType: string;
|
||||
name?: string;
|
||||
expiresAfterViews?: number;
|
||||
usingPassword: boolean;
|
||||
expiresAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSharedSecretEvent {
|
||||
type: EventType.DELETE_SHARED_SECRET;
|
||||
metadata: {
|
||||
id: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ReadSharedSecretEvent {
|
||||
type: EventType.READ_SHARED_SECRET;
|
||||
metadata: {
|
||||
id: string;
|
||||
name?: string;
|
||||
accessType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -2083,4 +2122,7 @@ export type Event =
|
||||
| GetAppConnectionEvent
|
||||
| CreateAppConnectionEvent
|
||||
| UpdateAppConnectionEvent
|
||||
| DeleteAppConnectionEvent;
|
||||
| DeleteAppConnectionEvent
|
||||
| CreateSharedSecretEvent
|
||||
| DeleteSharedSecretEvent
|
||||
| ReadSharedSecretEvent;
|
||||
|
@ -34,6 +34,8 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL;
|
||||
|
||||
const db = knex({
|
||||
client: providerInputs.client,
|
||||
connection: {
|
||||
@ -43,7 +45,16 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
ssl,
|
||||
pool: { min: 0, max: 1 }
|
||||
pool: { min: 0, max: 1 },
|
||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
||||
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
||||
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||
options: isMsSQLClient
|
||||
? {
|
||||
trustServerCertificate: !providerInputs.ca,
|
||||
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { KMSServiceException } from "@aws-sdk/client-kms";
|
||||
import { STSServiceException } from "@aws-sdk/client-sts";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -71,7 +73,16 @@ export const externalKmsServiceFactory = ({
|
||||
switch (provider.type) {
|
||||
case KmsProviders.Aws:
|
||||
{
|
||||
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
|
||||
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs }).catch((error) => {
|
||||
if (error instanceof STSServiceException || error instanceof KMSServiceException) {
|
||||
throw new InternalServerError({
|
||||
message: error.message ? `AWS error: ${error.message}` : ""
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
// if missing kms key this generate a new kms key id and returns new provider input
|
||||
const newProviderInput = await externalKms.generateInputKmsKey();
|
||||
sanitizedProviderInput = JSON.stringify(newProviderInput);
|
||||
|
@ -180,6 +180,8 @@ export const secretRotationQueueFactory = ({
|
||||
provider.template.client === TDbProviderClients.MsSqlServer
|
||||
? ({
|
||||
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
|
||||
// when ca is provided use that
|
||||
trustServerCertificate: !ca,
|
||||
cryptoCredentialsDetails: ca ? { ca } : {}
|
||||
} as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
@ -199,7 +199,29 @@ const envSchema = z
|
||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional())
|
||||
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
|
||||
|
||||
/* CORS ----------------------------------------------------------------------------- */
|
||||
|
||||
CORS_ALLOWED_ORIGINS: zpStr(
|
||||
z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (!val) return undefined;
|
||||
return JSON.parse(val) as string[];
|
||||
})
|
||||
),
|
||||
|
||||
CORS_ALLOWED_HEADERS: zpStr(
|
||||
z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (!val) return undefined;
|
||||
return JSON.parse(val) as string[];
|
||||
})
|
||||
)
|
||||
})
|
||||
// To ensure that basic encryption is always possible.
|
||||
.refine(
|
||||
|
@ -87,7 +87,16 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
|
||||
|
||||
await server.register<FastifyCorsOptions>(cors, {
|
||||
credentials: true,
|
||||
...(appCfg.CORS_ALLOWED_ORIGINS?.length
|
||||
? {
|
||||
origin: [...appCfg.CORS_ALLOWED_ORIGINS, ...(appCfg.SITE_URL ? [appCfg.SITE_URL] : [])]
|
||||
}
|
||||
: {
|
||||
origin: appCfg.SITE_URL || true
|
||||
}),
|
||||
...(appCfg.CORS_ALLOWED_HEADERS?.length && {
|
||||
allowedHeaders: appCfg.CORS_ALLOWED_HEADERS
|
||||
})
|
||||
});
|
||||
|
||||
await server.register(addErrorsToResponseSchemas);
|
||||
|
@ -32,13 +32,21 @@ export const getUserAgentType = (userAgent: string | undefined) => {
|
||||
export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
|
||||
server.decorateRequest("auditLogInfo", null);
|
||||
server.addHook("onRequest", async (req) => {
|
||||
if (!req.auth) return;
|
||||
const userAgent = req.headers["user-agent"] ?? "";
|
||||
const payload = {
|
||||
ipAddress: req.realIp,
|
||||
userAgent,
|
||||
userAgentType: getUserAgentType(userAgent)
|
||||
} as typeof req.auditLogInfo;
|
||||
|
||||
if (!req.auth) {
|
||||
payload.actor = {
|
||||
type: ActorType.UNKNOWN_USER,
|
||||
metadata: {}
|
||||
};
|
||||
req.auditLogInfo = payload;
|
||||
return;
|
||||
}
|
||||
if (req.auth.actor === ActorType.USER) {
|
||||
payload.actor = {
|
||||
type: ActorType.USER,
|
||||
|
@ -131,7 +131,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app),
|
||||
appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId),
|
||||
isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
|
||||
isActive: z.boolean().optional().describe(INTEGRATION.UPDATE.isActive),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSharingSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SecretSharingAccessType } from "@app/lib/types";
|
||||
import {
|
||||
publicEndpointLimit,
|
||||
@ -88,6 +89,21 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
orgId: req.permission?.orgId
|
||||
});
|
||||
|
||||
if (sharedSecret.secret?.orgId) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
orgId: sharedSecret.secret.orgId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.READ_SHARED_SECRET,
|
||||
metadata: {
|
||||
id: req.params.id,
|
||||
name: sharedSecret.secret.name || undefined,
|
||||
accessType: sharedSecret.secret.accessType
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sharedSecret;
|
||||
}
|
||||
});
|
||||
@ -151,6 +167,23 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
orgId: req.permission.orgId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.CREATE_SHARED_SECRET,
|
||||
metadata: {
|
||||
accessType: req.body.accessType,
|
||||
expiresAt: req.body.expiresAt,
|
||||
expiresAfterViews: req.body.expiresAfterViews,
|
||||
name: req.body.name,
|
||||
id: sharedSecret.id,
|
||||
usingPassword: !!req.body.password
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { id: sharedSecret.id };
|
||||
}
|
||||
});
|
||||
@ -181,6 +214,18 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
sharedSecretId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
orgId: req.permission.orgId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.DELETE_SHARED_SECRET,
|
||||
metadata: {
|
||||
id: sharedSecretId,
|
||||
name: deletedSharedSecret.name || undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { ...deletedSharedSecret };
|
||||
}
|
||||
});
|
||||
|
@ -39,7 +39,8 @@ export enum ActorType { // would extend to AWS, Azure, ...
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity",
|
||||
Machine = "machine",
|
||||
SCIM_CLIENT = "scimClient"
|
||||
SCIM_CLIENT = "scimClient",
|
||||
UNKNOWN_USER = "unknownUser"
|
||||
}
|
||||
|
||||
// This will be null unless the token-type is JWT
|
||||
|
@ -2,3 +2,11 @@ import picomatch from "picomatch";
|
||||
|
||||
export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) =>
|
||||
policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||
|
||||
export const doesAudValueMatchOidcPolicy = (fieldValue: string | string[], policyValue: string) => {
|
||||
if (Array.isArray(fieldValue)) {
|
||||
return fieldValue.some((entry) => entry === policyValue || picomatch.isMatch(entry, policyValue));
|
||||
}
|
||||
|
||||
return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
|
||||
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||
import { TOrgBotDALFactory } from "../org/org-bot-dal";
|
||||
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
|
||||
import { doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
|
||||
import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
|
||||
import {
|
||||
TAttachOidcAuthDTO,
|
||||
TGetOidcAuthDTO,
|
||||
@ -148,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (
|
||||
!identityOidcAuth.boundAudiences
|
||||
.split(", ")
|
||||
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
|
||||
.some((policyValue) => doesAudValueMatchOidcPolicy(tokenData.aud, policyValue))
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OIDC audience not allowed."
|
||||
|
@ -1289,7 +1289,10 @@ const syncSecretsAWSSecretManager = async ({
|
||||
|
||||
if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) {
|
||||
for await (const [key, value] of Object.entries(secrets)) {
|
||||
await processAwsSecret(key, value.value, value.secretMetadata);
|
||||
await processAwsSecret(key, value.value, value.secretMetadata).catch((error) => {
|
||||
error.secretKey = key;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets));
|
||||
|
@ -206,8 +206,13 @@ export const secretSharingServiceFactory = ({
|
||||
|
||||
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
|
||||
|
||||
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
|
||||
if (accessType === SecretSharingAccessType.Organization && orgId === undefined) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) {
|
||||
throw new ForbiddenRequestError();
|
||||
}
|
||||
|
||||
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
|
||||
// or can be safely sent to the client.
|
||||
|
@ -971,6 +971,8 @@ export const secretQueueFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { secretKey } = (err as { secretKey: string }) || {};
|
||||
|
||||
const message =
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
(err instanceof AxiosError
|
||||
@ -979,6 +981,8 @@ export const secretQueueFactory = ({
|
||||
: err?.message
|
||||
: (err as Error)?.message) || "Unknown error occurred.";
|
||||
|
||||
const errorLog = `${secretKey ? `[Secret Key: ${secretKey}] ` : ""}${message}`;
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId,
|
||||
actor: await $generateActor(actorId, isManual),
|
||||
@ -989,7 +993,7 @@ export const secretQueueFactory = ({
|
||||
isSynced: false,
|
||||
lastSyncJobId: job?.id ?? "",
|
||||
lastUsed: new Date(),
|
||||
syncMessage: message
|
||||
syncMessage: errorLog
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1001,13 +1005,13 @@ export const secretQueueFactory = ({
|
||||
|
||||
await integrationDAL.updateById(integration.id, {
|
||||
lastSyncJobId: job.id,
|
||||
syncMessage: message,
|
||||
syncMessage: errorLog,
|
||||
isSynced: false
|
||||
});
|
||||
|
||||
integrationsFailedToSync.push({
|
||||
integrationId: integration.id,
|
||||
syncMessage: message
|
||||
syncMessage: errorLog
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
---
|
||||
title: "On call summary template"
|
||||
sidebarTitle: "Summary template"
|
||||
---
|
||||
|
||||
```plain
|
||||
Date: MM/DD/YY-MM/DD/YY
|
||||
|
||||
Notable incidents:
|
||||
- [<open/resolved>] <details of the incident including who was impacted. what you did to mitigate/patch the issue>
|
||||
- Action items:
|
||||
- <what can we do to prevent this from happening in the future?>
|
||||
|
||||
Notable support:
|
||||
- [Customer company name] <details of the support inquiry>
|
||||
- Action items:
|
||||
- <what actions should be taken/has been taken to resolve this>
|
||||
- <what can we do to prevent this from happening in the future?>
|
||||
|
||||
Comments:
|
||||
<Any comments you have from your on call shift. Were there any pain points you experienced, etc?>
|
||||
```
|
78
company/documentation/engineering/oncall.mdx
Normal file
78
company/documentation/engineering/oncall.mdx
Normal file
@ -0,0 +1,78 @@
|
||||
---
|
||||
title: "On call rotation"
|
||||
sidebarTitle: "On call rotation"
|
||||
description: "Learn about call rotation at Infisical"
|
||||
---
|
||||
|
||||
Infisical is mission-critical software, which means minimizing service disruptions is a top priority.
|
||||
To make sure we can react to any issues that come up, we have an on-call rotation that helps us to provide responsive, 24x7x365 support to our customers.
|
||||
Being part of the on-call rotation is an opportunity to deepen the understanding of our infrastructure, deployment pipelines, and customer-facing systems.
|
||||
Having this broader understanding of our system not only helps us design better software but also enhances the overall stability of our platform.
|
||||
|
||||
### On-Call Overview
|
||||
|
||||
**Rotation Details**
|
||||
|
||||
Each engineer will be on call once a week, from **Thursday to Thursday**, including weekends.
|
||||
During this time, the on-call engineer is expected to be available at all times to respond to service disruption alerts.
|
||||
|
||||
While being on call, you are responsible for acting as the first line of defense for critical incidents and answering customer support inquiries.
|
||||
During your working hours, you must respond to all support tickets or involve relevant team members with sufficient context.
|
||||
Outside of working hours, you are expected to be available for any high-severity pager alerts and critical support inquiries by customers.
|
||||
|
||||
### Responsibilities While On Call
|
||||
|
||||
During your working hours, prioritize the following in this order:
|
||||
|
||||
1. **Responding to Alerts:**
|
||||
- Monitor and respond promptly to all PagerDuty alerts.
|
||||
- Investigate incidents, determine root causes, and mitigate issues.
|
||||
- Refer to runbooks or any relevant documentation to resolve alarms quickly.
|
||||
2. **Customer Support:**
|
||||
- Actively monitor all support inquiries in [**Pylon**](https://app.usepylon.com/issues) and respond to incoming tickets.
|
||||
- Debug and resolve customer issues. If you encounter a problem outside your expertise, collaborate with the relevant teammates to resolve it. This is an opportunity to learn and build context for future incidents.
|
||||
3. **Sprint work:**
|
||||
- Since the current on-call workload does not require all of your working hours, you are expected to work on the sprint items assigned to you.
|
||||
If the on-call workload increases significantly, inform Maidul to make adjustments.
|
||||
4. **Continuous Improvement:**
|
||||
- Take note of recurring patterns, inefficiencies, and opportunities where we can automate to reduce on-call burdens in the future.
|
||||
|
||||
<Warning>
|
||||
Outside of working hours, you are expected to be available and respond to any high-severity pager alerts and critical support inquiries by customers.
|
||||
</Warning>
|
||||
|
||||
### Before You Get On Call
|
||||
|
||||
- **Set Up PagerDuty:** Ensure you have the PagerDuty mobile app installed, configured, and notifications enabled for Infisical services.
|
||||
- **Access Required Tools:** Verify access to internal network, runbooks on Notion, [https://grafana.infisical.com](https://grafana.infisical.com/), access to aws accounts and any other access you may require.
|
||||
- **AWS Permissions:** You will be granted sufficient AWS permissions before the start of your on-call shift in case you need to access production accounts.
|
||||
|
||||
### At the End of Your Shift
|
||||
|
||||
- Post an on-call summary in the Slack channel `#on-call-summaries` at the end of your shift using the following [template](/documentation/engineering/oncall-summery-template). Include notable findings, support inquires and incidents you encountered.
|
||||
This will helps the rest of the team stay in the loop and open discussions on how to prevent similar issues in the future.
|
||||
- Do a **handoff meeting/slack huddle** with the next engineer on call to summarize any outstanding work, unresolved issues, or any incidents that require follow-up. Ensure the next on-call engineer is fully briefed so they can pick up where you left off. **Include Maidul in this hand off call.**
|
||||
|
||||
### When to escalate an incident
|
||||
|
||||
If you are paged for incident that you cannot resolve after attempting to debug and mitigate the issue, you should not hesitate to escalate and page others in.
|
||||
It’s better to get help sooner rather than later to minimize the impact on customers.
|
||||
|
||||
- **Paging relevant teammate:** If you’ve tried resolving an issue on your own and need additional help, page another engineer who might be relevant through PagerDuty.
|
||||
- **Escalating to Maidul:** You can page Maidul at any time if you think it would be helpful.
|
||||
|
||||
### How to be successful on you rotations
|
||||
|
||||
- Be on top of all changes that get merged into main. This will help you be aware of any changes that might cause issues.
|
||||
- When responding to support inquiries, double check your replies and make sure they are well written and typo-free. Always acknowledge inquiries quickly to make customers feel valued, and suggest a meeting or huddle if you need more clarity on their issues.
|
||||
- When customers raise support inquiries, always consider what could have been done to make the inquiry self-serve. Could adding a tooltip next to the relevant feature provide clarity? Maybe the documentation could be more detailed or better organized?
|
||||
- Document all of your notable support/findings/incidents/feature requests during on call so that it is easy to create your on call summary at the end of your on call shift.
|
||||
|
||||
### Resources
|
||||
|
||||
- [Pylon for support tickets](https://app.usepylon.com/issues)
|
||||
- [AWS Portal](https://infisical.awsapps.com/start/)
|
||||
- [View metrics on Grafana](https://grafana.infisical.com/)
|
||||
- [Metabase](https://analytics.internal.infisical.com/)
|
||||
- [Run books](https://www.notion.so/Runbooks-19e534883d6b4621b8c712194edbb687?pvs=21)
|
||||
- [On call summary template](/documentation/engineering/oncall-summery-template)
|
@ -62,7 +62,13 @@
|
||||
"handbook/time-off",
|
||||
"handbook/hiring",
|
||||
"handbook/meetings",
|
||||
"handbook/talking-to-customers"
|
||||
"handbook/talking-to-customers",
|
||||
{
|
||||
"group": "Engineering",
|
||||
"pages": [
|
||||
"documentation/engineering/oncall"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
sidebarTitle: "InfisicalDynamicSecret CRD"
|
||||
title: "InfisicalDynamicSecret CRD"
|
||||
title: "Using the InfisicalDynamicSecret CRD"
|
||||
description: "Learn how to generate dynamic secret leases in Infisical and sync them to your Kubernetes cluster."
|
||||
---
|
||||
## Overview
|
||||
@ -11,12 +11,14 @@ This means any Pod, Deployment, or other Kubernetes resource can make use of dyn
|
||||
This CRD offers the following features:
|
||||
- **Generate a dynamic secret lease** in Infisical and track its lifecycle.
|
||||
- **Write** the dynamic secret from Infisical to your cluster as native Kubernetes secret.
|
||||
- **Automatically rotate** the dyanmic secret value before it expires to make sure your cluster always has valid credentials.
|
||||
- **Automatically rotate** the dynamic secret value before it expires to make sure your cluster always has valid credentials.
|
||||
- **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload.
|
||||
|
||||
### Prerequisites
|
||||
- The operator is installed on to your Kubernetes cluster
|
||||
- You have already configured a dynamic secret in Infisical
|
||||
- A project within Infisical.
|
||||
- A [machine identity](/docs/documentation/platform/identities/overview) ready for use in Infisical that has permissions to create dynamic secret leases in the project.
|
||||
- You have already configured a dynamic secret in Infisical.
|
||||
- The operator is installed on to your Kubernetes cluster.
|
||||
|
||||
## Configure Dynamic Secret CRD
|
||||
|
||||
|
@ -5,10 +5,22 @@ description: "Learn how to use the InfisicalPushSecret CRD to push and manage se
|
||||
---
|
||||
|
||||
|
||||
## Push Secrets to Infisical
|
||||
## Overview
|
||||
|
||||
The **InfisicalPushSecret** CRD allows you to create secrets in your Kubernetes cluster and push them to Infisical.
|
||||
|
||||
|
||||
### Example usage
|
||||
This CRD offers the following features:
|
||||
- **Push Secrets** from a Kubernetes secret into Infisical.
|
||||
- **Manage secret lifecycle** of pushed secrets in Infisical. When the Kubernetes secret is updated, the operator will automatically update the secrets in Infisical. Optionally, when the Kubernetes secret is deleted, the operator will delete the secrets in Infisical automatically.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A project within Infisical.
|
||||
- A [machine identity](/docs/documentation/platform/identities/overview) ready for use in Infisical that has permissions to create secrets in your project.
|
||||
- The operator is installed on to your Kubernetes cluster.
|
||||
|
||||
## Example usage
|
||||
|
||||
Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical.
|
||||
|
||||
@ -89,7 +101,7 @@ After applying the soruce-secret.yaml file, you are ready to apply the Infisical
|
||||
After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical.
|
||||
|
||||
|
||||
### InfisicalPushSecret CRD properties
|
||||
## InfisicalPushSecret CRD properties
|
||||
|
||||
<Accordion title="hostAPI">
|
||||
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
|
||||
@ -272,7 +284,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="kubernetesAuth">
|
||||
The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment.
|
||||
The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalPushSecret resource. This authentication method can only be used within a Kubernetes environment.
|
||||
[Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth).
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
@ -326,7 +338,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="gcpIamAuth">
|
||||
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
|
||||
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalPushSecret resource. This authentication method can only be used both within and outside GCP environments.
|
||||
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
|
||||
|
||||
|
||||
@ -344,7 +356,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="gcpIdTokenAuth">
|
||||
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments.
|
||||
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalPushSecret resource. This authentication method can only be used within GCP environments.
|
||||
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
|
||||
|
||||
Valid fields:
|
||||
@ -389,7 +401,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
|
||||
</Accordion>
|
||||
|
||||
|
||||
### Applying the InfisicalPushSecret CRD to your cluster
|
||||
## Applying the InfisicalPushSecret CRD to your cluster
|
||||
|
||||
Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster.
|
||||
After applying, you should notice that the secrets have been pushed to Infisical.
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
sidebarTitle: "InfisicalSecret CRD"
|
||||
title: "InfisicalSecret CRD"
|
||||
title: "Using the InfisicalSecret CRD"
|
||||
description: "Learn how to use the InfisicalSecret CRD to fetch secrets from Infisical and store them as native Kubernetes secret resource"
|
||||
---
|
||||
|
||||
|
@ -34,6 +34,27 @@ Used to configure platform-specific security and operational settings
|
||||
this to `false`.
|
||||
</ParamField>
|
||||
|
||||
## CORS
|
||||
|
||||
Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications running on one domain to access resources from another domain.
|
||||
The following environment variables can be used to configure the Infisical Rest API to allow or restrict access to resources from different origins.
|
||||
|
||||
<ParamField query="CORS_ALLOWED_ORIGINS" type="string" optional>
|
||||
|
||||
Specify a list of origins that are allowed to access the Infisical API.
|
||||
|
||||
An example value would be `CORS_ALLOWED_ORIGINS=["https://example.com"]`.
|
||||
|
||||
Defaults to the same value as your `SITE_URL` environment variable.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CORS_ALLOWED_METHODS" type="string" optional>
|
||||
Array of HTTP methods allowed for CORS requests.
|
||||
|
||||
Defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header.
|
||||
</ParamField>
|
||||
|
||||
|
||||
## Data Layer
|
||||
|
||||
The platform utilizes Postgres to persist all of its data and Redis for caching and backgroud tasks
|
||||
@ -72,7 +93,7 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}]
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
## Email service
|
||||
## Email Service
|
||||
|
||||
Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features.
|
||||
|
||||
|
@ -58,7 +58,8 @@ export const leaveConfirmDefaultMessage =
|
||||
"Your changes will be lost if you leave the page. Are you sure you want to continue?";
|
||||
|
||||
export enum SessionStorageKeys {
|
||||
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN"
|
||||
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN",
|
||||
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL"
|
||||
}
|
||||
|
||||
export const secretTagsColors = [
|
||||
|
@ -3,6 +3,7 @@ import { EventType, UserAgentType } from "./enums";
|
||||
export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_SECRETS]: "List secrets",
|
||||
[EventType.GET_SECRET]: "Read secret",
|
||||
[EventType.DELETE_SECRETS]: "Delete secrets",
|
||||
[EventType.CREATE_SECRET]: "Create secret",
|
||||
[EventType.UPDATE_SECRET]: "Update secret",
|
||||
[EventType.DELETE_SECRET]: "Delete secret",
|
||||
@ -81,7 +82,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
"Update certificate template EST configuration",
|
||||
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
|
||||
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
|
||||
[EventType.INTEGRATION_SYNCED]: "Integration sync"
|
||||
[EventType.INTEGRATION_SYNCED]: "Integration sync",
|
||||
[EventType.CREATE_SHARED_SECRET]: "Create shared secret",
|
||||
[EventType.DELETE_SHARED_SECRET]: "Delete shared secret",
|
||||
[EventType.READ_SHARED_SECRET]: "Read shared secret"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
@ -2,7 +2,8 @@ export enum ActorType {
|
||||
PLATFORM = "platform",
|
||||
USER = "user",
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity"
|
||||
IDENTITY = "identity",
|
||||
UNKNOWN_USER = "unknownUser"
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
@ -17,6 +18,7 @@ export enum UserAgentType {
|
||||
|
||||
export enum EventType {
|
||||
GET_SECRETS = "get-secrets",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
GET_SECRET = "get-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
@ -94,5 +96,8 @@ export enum EventType {
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
INTEGRATION_SYNCED = "integration-synced"
|
||||
INTEGRATION_SYNCED = "integration-synced",
|
||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||
READ_SHARED_SECRET = "read-shared-secret"
|
||||
}
|
||||
|
@ -50,7 +50,11 @@ export interface PlatformActor {
|
||||
metadata: object;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor;
|
||||
export interface UnknownUserActor {
|
||||
type: ActorType.UNKNOWN_USER;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor | UnknownUserActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
|
@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
|
||||
import { organizationKeys } from "../organization/queries";
|
||||
import { setAuthToken } from "../reactQuery";
|
||||
@ -86,6 +87,21 @@ export const useSelectOrganization = () => {
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
}
|
||||
|
||||
if (data.token && !data.isMfaEnabled) {
|
||||
// We check if there is a pending callback after organization login success and redirect to it if valid
|
||||
const loginRedirectInfo = sessionStorage.getItem(
|
||||
SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL
|
||||
);
|
||||
sessionStorage.removeItem(SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL);
|
||||
|
||||
if (loginRedirectInfo) {
|
||||
const { expiry, data: redirectUrl } = JSON.parse(loginRedirectInfo);
|
||||
if (new Date() < new Date(expiry)) {
|
||||
window.location.assign(redirectUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@ -49,7 +49,6 @@ export const LogsFilter = ({
|
||||
isOrgAuditLogs,
|
||||
className,
|
||||
control,
|
||||
setValue,
|
||||
reset,
|
||||
watch
|
||||
}: Props) => {
|
||||
@ -63,12 +62,6 @@ export const LogsFilter = ({
|
||||
|
||||
const { data, isPending } = useGetAuditLogActorFilterOpts(workspaces?.[0]?.id ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (workspacesInOrg.length) {
|
||||
setValue("project", workspacesInOrg[0]);
|
||||
}
|
||||
}, [workspaces]);
|
||||
|
||||
const renderActorSelectItem = (actor: Actor) => {
|
||||
switch (actor.type) {
|
||||
case ActorType.USER:
|
||||
@ -129,6 +122,7 @@ export const LogsFilter = ({
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
onChange={onChange}
|
||||
placeholder="Select a project..."
|
||||
options={workspacesInOrg.map(({ name, id }) => ({ name, id }))}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { Td, Tr } from "@app/components/v2";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types";
|
||||
@ -37,6 +40,17 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
<p>Machine Identity</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.UNKNOWN_USER:
|
||||
return (
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<p>Unknown User</p>
|
||||
<Tooltip content="This action was performed by a user who was not authenticated at the time.">
|
||||
<FontAwesomeIcon className="text-mineshaft-400" icon={faQuestionCircle} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td />;
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useParams, useSearch } from "@tanstack/react-router";
|
||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||
import { AxiosError } from "axios";
|
||||
import { addSeconds, formatISO } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
|
||||
|
||||
@ -36,7 +39,6 @@ export const ViewSharedSecretByIDPage = () => {
|
||||
select: (el) => el.key
|
||||
});
|
||||
const [password, setPassword] = useState<string>();
|
||||
|
||||
const { hashedHex, key } = extractDetailsFromUrl(urlEncodedKey);
|
||||
|
||||
const {
|
||||
@ -50,10 +52,47 @@ export const ViewSharedSecretByIDPage = () => {
|
||||
password
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isUnauthorized =
|
||||
((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 401;
|
||||
|
||||
const isForbidden =
|
||||
((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 403;
|
||||
|
||||
const isInvalidCredential =
|
||||
((error as AxiosError)?.response?.data as { message: string })?.message ===
|
||||
"Invalid credentials";
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnauthorized && !isInvalidCredential) {
|
||||
// persist current URL in session storage so that we can come back to this after successful login
|
||||
sessionStorage.setItem(
|
||||
SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL,
|
||||
JSON.stringify({
|
||||
expiry: formatISO(addSeconds(new Date(), 60)),
|
||||
data: window.location.href
|
||||
})
|
||||
);
|
||||
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Login is required in order to access the shared secret."
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/login"
|
||||
});
|
||||
}
|
||||
|
||||
if (isForbidden) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "You do not have access to this shared secret."
|
||||
});
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const shouldShowPasswordPrompt =
|
||||
isInvalidCredential || (fetchSecret?.isPasswordProtected && !fetchSecret.secret);
|
||||
const isValidatingPassword = Boolean(password) && isFetching;
|
||||
@ -111,7 +150,7 @@ export const ViewSharedSecretByIDPage = () => {
|
||||
{!error && fetchSecret?.secret && (
|
||||
<SecretContainer secret={fetchSecret.secret} secretKey={key} />
|
||||
)}
|
||||
{error && !isInvalidCredential && <SecretErrorContainer />}
|
||||
{error && !isInvalidCredential && !isUnauthorized && <SecretErrorContainer />}
|
||||
</>
|
||||
)}
|
||||
<div className="m-auto my-8 flex w-full">
|
||||
|
@ -2,6 +2,8 @@ import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||
|
||||
import { ViewSharedSecretByIDPage } from "./ViewSharedSecretByIDPage";
|
||||
|
||||
const SharedSecretByIDPageQuerySchema = z.object({
|
||||
@ -9,9 +11,18 @@ const SharedSecretByIDPageQuerySchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/shared/secret/$secretId")({
|
||||
component: ViewSharedSecretByIDPage,
|
||||
validateSearch: zodValidator(SharedSecretByIDPageQuerySchema),
|
||||
component: ViewSharedSecretByIDPage,
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ key: "" })]
|
||||
},
|
||||
beforeLoad: async ({ context }) => {
|
||||
// we load the auth token because the view shared secret screen serves both public and authenticated users
|
||||
await context.queryClient
|
||||
.ensureQueryData({
|
||||
queryKey: authKeys.getAuthToken,
|
||||
queryFn: fetchAuthToken
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user