diff --git a/app/auth-portal/src/pages/WorkspaceDetailsPage.vue b/app/auth-portal/src/pages/WorkspaceDetailsPage.vue index 2b5e2e31ff..2d1c517e75 100644 --- a/app/auth-portal/src/pages/WorkspaceDetailsPage.vue +++ b/app/auth-portal/src/pages/WorkspaceDetailsPage.vue @@ -37,6 +37,21 @@ tooltipPlacement="top" @click="openApiTokens" /> + { draftWorkspace.isFavourite = isFavourite; }; +const hideWorkspace = async (isHidden: boolean) => { + if (!props.workspaceId) return; + + await workspacesStore.SET_HIDDEN(props.workspaceId, isHidden); + + draftWorkspace.isHidden = isHidden; +}; const deleteWorkspace = async () => { const res = await workspacesStore.DELETE_WORKSPACE(props.workspaceId); diff --git a/app/auth-portal/src/store/workspaces.store.ts b/app/auth-portal/src/store/workspaces.store.ts index b39767241f..c8266d52ed 100644 --- a/app/auth-portal/src/store/workspaces.store.ts +++ b/app/auth-portal/src/store/workspaces.store.ts @@ -26,6 +26,7 @@ export type Workspace = { invitedAt: Date; isDefault: boolean; isFavourite: boolean; + isHidden: boolean; quarantinedAt: Date; }; @@ -209,6 +210,15 @@ export const useWorkspacesStore = defineStore("workspaces", { }, }); }, + async SET_HIDDEN(workspaceId: string, isHidden: boolean) { + return new ApiRequest<{ user: User }>({ + method: "patch", + url: `/workspaces/${workspaceId}/setHidden`, + params: { + isHidden, + }, + }); + }, async SET_DEFAULT_WORKSPACE(workspaceId: string) { return new ApiRequest<{ user: User }>({ method: "patch", diff --git a/bin/auth-api/prisma/migrations/20250224195419_add_is_hidden/migration.sql b/bin/auth-api/prisma/migrations/20250224195419_add_is_hidden/migration.sql new file mode 100644 index 0000000000..eb7dc6e91a --- /dev/null +++ b/bin/auth-api/prisma/migrations/20250224195419_add_is_hidden/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "workspaces" ADD COLUMN "is_hidden" BOOLEAN NOT NULL DEFAULT false; diff --git a/bin/auth-api/prisma/schema.prisma b/bin/auth-api/prisma/schema.prisma index 5e2ff6a2d7..0897027dc0 100644 --- a/bin/auth-api/prisma/schema.prisma +++ b/bin/auth-api/prisma/schema.prisma @@ -2,183 +2,187 @@ // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_DATABASE_URL") - shadowDatabaseUrl = env("SHADOW_DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") } model User { - /// SI's id for the user (ULID) - id String @id @db.Char(26) - /// Auth0's id - auth0Id String? @unique @map("auth0_id") - /// raw json blob of Auth0 data - auth0Details Json? @map("auth0_details") - /// single name string we can use as label for the user - nickname String? - /// user's email - email String - /// whether email has been verified - emailVerified Boolean @default(false) @map("email_verified") - /// user's first name - firstName String? @map("first_name") - /// user's last name - lastName String? @map("last_name") - /// public url to profile photo - pictureUrl String? @map("picture_url") - - /// user's discord username/tag - ex: coolbeans#1234 - discordUsername String? @map("discord_username") - /// user's github username - githubUsername String? @map("github_username") - - /// data about where user is in onboarding - onboardingDetails Json? @map("onboarding_details") - - /// When a user signed up - signupAt DateTime? @map("signup_at") - - /// array of workspaces the user created - CreatedWorkspaces Workspace[] - /// array of the workspaces that the user has access to - WorkspaceMembers WorkspaceMembers[] - TosAgreement TosAgreement[] - - /// Timestamp of the latest account quarantine. `undefined` if not quarantined - quarantinedAt DateTime? @map("quarantined_at") - - /// Timestamp of the latest account suspension. `undefined` if not suspended - suspendedAt DateTime? @map("suspended_at") - - /// List of automation tokens a user has created - tokens AuthToken[] - - @@index(fields: [email]) - @@map("users") + /// SI's id for the user (ULID) + id String @id @db.Char(26) + /// Auth0's id + auth0Id String? @unique @map("auth0_id") + /// raw json blob of Auth0 data + auth0Details Json? @map("auth0_details") + /// single name string we can use as label for the user + nickname String? + /// user's email + email String + /// whether email has been verified + emailVerified Boolean @default(false) @map("email_verified") + /// user's first name + firstName String? @map("first_name") + /// user's last name + lastName String? @map("last_name") + /// public url to profile photo + pictureUrl String? @map("picture_url") + + /// user's discord username/tag - ex: coolbeans#1234 + discordUsername String? @map("discord_username") + /// user's github username + githubUsername String? @map("github_username") + + /// data about where user is in onboarding + onboardingDetails Json? @map("onboarding_details") + + /// When a user signed up + signupAt DateTime? @map("signup_at") + + /// array of workspaces the user created + CreatedWorkspaces Workspace[] + /// array of the workspaces that the user has access to + WorkspaceMembers WorkspaceMembers[] + TosAgreement TosAgreement[] + + /// Timestamp of the latest account quarantine. `undefined` if not quarantined + quarantinedAt DateTime? @map("quarantined_at") + + /// Timestamp of the latest account suspension. `undefined` if not suspended + suspendedAt DateTime? @map("suspended_at") + + /// List of automation tokens a user has created + tokens AuthToken[] + + @@index(fields: [email]) + @@map("users") } enum InstanceEnvType { - LOCAL - PRIVATE - SI + LOCAL + PRIVATE + SI } model Workspace { - /// SI's id for the workspace (ULID) - id String @id @db.Char(26) - /// type of instance (local, private, si sass) - instanceEnvType InstanceEnvType @map("instance_env_type") - /// url of instance - instanceUrl String? @map("instance_url") - /// label for the workspace - displayName String @map("display_name") + /// SI's id for the workspace (ULID) + id String @id @db.Char(26) + /// type of instance (local, private, si sass) + instanceEnvType InstanceEnvType @map("instance_env_type") + /// url of instance + instanceUrl String? @map("instance_url") + /// label for the workspace + displayName String @map("display_name") - /// id of user who created workspace - creatorUserId String @map("creator_user_id") - /// user who created workspace - creatorUser User @relation(fields: [creatorUserId], references: [id]) + /// id of user who created workspace + creatorUserId String @map("creator_user_id") + /// user who created workspace + creatorUser User @relation(fields: [creatorUserId], references: [id]) - // The list of users that have access to this workspace - UserMemberships WorkspaceMembers[] + // The list of users that have access to this workspace + UserMemberships WorkspaceMembers[] - // The time in which the workspace was deleted - deletedAt DateTime? @map("deleted_at") + // The time in which the workspace was deleted + deletedAt DateTime? @map("deleted_at") - /// secret token for the workspace (ULID) - token String? @db.Char(26) + /// secret token for the workspace (ULID) + token String? @db.Char(26) - /// Whether the workspace is the default or not - isDefault Boolean @default(false) @map("is_default") + /// Whether the workspace is the default or not + isDefault Boolean @default(false) @map("is_default") - /// Timestamp of the latest workspace quarantine. `undefined` if not quarantined - quarantinedAt DateTime? @map("quarantined_at") + /// Timestamp of the latest workspace quarantine. `undefined` if not quarantined + quarantinedAt DateTime? @map("quarantined_at") - /// A description of the workspace - defaults to empty - description String? + /// A description of the workspace - defaults to empty + description String? - /// Denotes whether this will show up in a users favourite workspaces list - isFavourite Boolean @default(false) @map("is_favourite") + /// Denotes whether this will show up in a users favourite workspaces list + isFavourite Boolean @default(false) @map("is_favourite") - tokens AuthToken[] + /// Denotes whether this will show up in the workspaces list in app.systeminit.com + /// If it's true, then it will not be shown in the workspaces list + isHidden Boolean @default(false) @map("is_hidden") - @@index(fields: [creatorUserId]) - @@map("workspaces") + tokens AuthToken[] + + @@index(fields: [creatorUserId]) + @@map("workspaces") } enum RoleType { - OWNER - APPROVER - EDITOR + OWNER + APPROVER + EDITOR } model WorkspaceMembers { - id String @id @db.Char(26) + id String @id @db.Char(26) - // id of the User - userId String @map("user_id") - user User @relation(fields: [userId], references: [id]) + // id of the User + userId String @map("user_id") + user User @relation(fields: [userId], references: [id]) - // id of the Workspace - workspaceId String @map("workspace_id") - workspace Workspace @relation(fields: [workspaceId], references: [id]) + // id of the Workspace + workspaceId String @map("workspace_id") + workspace Workspace @relation(fields: [workspaceId], references: [id]) - // Role of the user - roleType RoleType @map("role_type") + // Role of the user + roleType RoleType @map("role_type") - // Invitation to workspace date - invitedAt DateTime? @map("invited_at") + // Invitation to workspace date + invitedAt DateTime? @map("invited_at") - @@unique([userId, workspaceId]) + @@unique([userId, workspaceId]) } model TosAgreement { - /// id of agreement - not really used for anything... - id String @id @db.Char(26) - userId String @map("user_id") - User User @relation(fields: [userId], references: [id]) - /// TOS version ID agreed to (these are sortable to find latest) - tosVersionId String @map("tos_version_id") - /// timestamp when they agreed to the TOS - timestamp DateTime - /// IP address of user when they agreed - ipAddress String @map("ip_address") - - @@index(fields: [userId]) - @@map("tos_agreements") + /// id of agreement - not really used for anything... + id String @id @db.Char(26) + userId String @map("user_id") + User User @relation(fields: [userId], references: [id]) + /// TOS version ID agreed to (these are sortable to find latest) + tosVersionId String @map("tos_version_id") + /// timestamp when they agreed to the TOS + timestamp DateTime + /// IP address of user when they agreed + ipAddress String @map("ip_address") + + @@index(fields: [userId]) + @@map("tos_agreements") } /// A JWT authenticating a user and granting permissions to a particular workspace model AuthToken { - // Token ULID used to look up and revoke the token - id String @id @db.Char(26) - // Display name of token for Auth Portal UI - name String? - /// User this token authenticates - user User @relation(fields: [userId], references: [id]) - userId String @db.Char(26) - /// Workspace this token grants access to - workspace Workspace @relation(fields: [workspaceId], references: [id]) - workspaceId String @db.Char(26) - /// When this token was created - createdAt DateTime @default(now()) - /// When this token is set to expire (could be in the past). - /// Null if it doesn't have a direct expiration date - expiresAt DateTime? - /// When this token was revoked. This is separate from expiration dates, - /// which are baked into the token and only set on token creation. - /// Null if the token has not been revoked. - revokedAt DateTime? - /// Json claims in token - /// - role ("web" | "automation") - claims Json - - /// When this token was last used - lastUsedAt DateTime? - /// From where this token was last used - lastUsedIp String? + // Token ULID used to look up and revoke the token + id String @id @db.Char(26) + // Display name of token for Auth Portal UI + name String? + /// User this token authenticates + user User @relation(fields: [userId], references: [id]) + userId String @db.Char(26) + /// Workspace this token grants access to + workspace Workspace @relation(fields: [workspaceId], references: [id]) + workspaceId String @db.Char(26) + /// When this token was created + createdAt DateTime @default(now()) + /// When this token is set to expire (could be in the past). + /// Null if it doesn't have a direct expiration date + expiresAt DateTime? + /// When this token was revoked. This is separate from expiration dates, + /// which are baked into the token and only set on token creation. + /// Null if the token has not been revoked. + revokedAt DateTime? + /// Json claims in token + /// - role ("web" | "automation") + claims Json + + /// When this token was last used + lastUsedAt DateTime? + /// From where this token was last used + lastUsedIp String? } diff --git a/bin/auth-api/src/routes/admin.routes.ts b/bin/auth-api/src/routes/admin.routes.ts index 2c16dae245..761ebe500e 100644 --- a/bin/auth-api/src/routes/admin.routes.ts +++ b/bin/auth-api/src/routes/admin.routes.ts @@ -142,6 +142,7 @@ router.patch("/workspaces/:workspaceId/quarantine", async (ctx) => { quarantinedAt, workspace.description, workspace.isFavourite, + workspace.isHidden, ); ctx.body = await getUserWorkspaces(authUser.id); diff --git a/bin/auth-api/src/routes/workspace.routes.ts b/bin/auth-api/src/routes/workspace.routes.ts index 4b854d878b..ea18570962 100644 --- a/bin/auth-api/src/routes/workspace.routes.ts +++ b/bin/auth-api/src/routes/workspace.routes.ts @@ -167,6 +167,7 @@ router.patch("/workspaces/:workspaceId", async (ctx) => { workspace.quarantinedAt, reqBody.description, workspace.isFavourite, + workspace.isHidden, ); tracker.trackEvent(authUser, "workspace_updated", { @@ -385,6 +386,46 @@ router.patch("/workspaces/:workspaceId/favourite", async (ctx) => { workspace.quarantinedAt, workspace.description, reqBody.isFavourite, + workspace.isHidden, + ); + + ctx.body = await getUserWorkspaces(authUser.id); +}); + +router.patch("/workspaces/:workspaceId/setHidden", async (ctx) => { + extractAuthUser(ctx, true); + const { authUser, workspace } = await authorizeWorkspaceRoute(ctx); + + const reqBody = validate( + ctx.request.body, + z.object({ + isHidden: z.boolean(), + }), + ); + + const hiddenDate = new Date(); + if (reqBody.isHidden) { + tracker.trackEvent(authUser, "hide_workspace", { + hiddenBy: authUser.email, + hiddenDate, + workspaceId: workspace.id, + }); + } else { + tracker.trackEvent(authUser, "unhide_workspace", { + unHiddenBy: authUser.email, + unHiddenDate: hiddenDate, + workspaceId: workspace.id, + }); + } + + await patchWorkspace( + workspace.id, + workspace.instanceUrl, + workspace.displayName, + workspace.quarantinedAt, + workspace.description, + workspace.isFavourite, + reqBody.isHidden, ); ctx.body = await getUserWorkspaces(authUser.id); diff --git a/bin/auth-api/src/services/workspaces.service.ts b/bin/auth-api/src/services/workspaces.service.ts index 60ad6733b1..1f5b091015 100644 --- a/bin/auth-api/src/services/workspaces.service.ts +++ b/bin/auth-api/src/services/workspaces.service.ts @@ -75,6 +75,7 @@ export async function patchWorkspace( quarantinedAt: Date | null, description: string | null, isFavourite: boolean, + isHidden: boolean, ) { return prisma.workspace.update({ where: { id }, @@ -84,6 +85,7 @@ export async function patchWorkspace( quarantinedAt, description, isFavourite, + isHidden, }, }); }