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,
},
});
}