From 555143c9305c4fbe0e538e72f0b9cf3d48ecbe86 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 14:43:52 +0100 Subject: [PATCH 01/24] enhancement: Refactor server functions and files for better maintainability --- .github/ISSUE_TEMPLATE/bug-report.yml | 1 + .../ISSUE_TEMPLATE/enhancement-request.yml | 1 + .github/ISSUE_TEMPLATE/feature-request.yml | 3 ++- apps/shelve/server/api/vault.ts | 24 ++----------------- packages/types/src/Vault.ts | 20 ++++++++++++++++ packages/types/src/index.ts | 1 + 6 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 packages/types/src/Vault.ts diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 286e191f..390e2daa 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,6 +1,7 @@ name: "🐛 Bug report" description: Report a bug to help us improve the project. labels: ["bug"] +title: "fix: " body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/enhancement-request.yml b/.github/ISSUE_TEMPLATE/enhancement-request.yml index f65f37c2..21ae1a8e 100644 --- a/.github/ISSUE_TEMPLATE/enhancement-request.yml +++ b/.github/ISSUE_TEMPLATE/enhancement-request.yml @@ -1,6 +1,7 @@ name: "🌈 Enhancement request" description: Suggest an idea or enhancement for the project. labels: ["enhancement"] +title: "enhancement: " body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 28e533b8..1c34e83b 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,6 +1,7 @@ name: "🚀 Feature request" description: Suggest an idea or enhancement for the project. -labels: ["enhancement"] +labels: ["feature"] +title: "feat: " body: - type: markdown attributes: diff --git a/apps/shelve/server/api/vault.ts b/apps/shelve/server/api/vault.ts index c3f13c60..fc4bd89f 100644 --- a/apps/shelve/server/api/vault.ts +++ b/apps/shelve/server/api/vault.ts @@ -1,27 +1,7 @@ -import { H3Event } from 'h3' +import type { DecryptResponse, EncryptRequest, StoredData, TTLFormat } from '@shelve/types' +import type { H3Event } from 'h3' import { seal, unseal } from '@shelve/crypto' -type TTLFormat = '1d' | '7d' | '30d' - -type EncryptRequest = { - value: string - reads: number - ttl: TTLFormat -} - -type StoredData = { - encryptedValue: string - reads: number - createdAt: number - ttl: TTLFormat -} - -type DecryptResponse = { - decryptedValue: string - reads: number - ttl: string -} - class VaultService { private readonly storage: Storage diff --git a/packages/types/src/Vault.ts b/packages/types/src/Vault.ts new file mode 100644 index 00000000..2ff3d5e5 --- /dev/null +++ b/packages/types/src/Vault.ts @@ -0,0 +1,20 @@ +export type TTLFormat = '1d' | '7d' | '30d' + +export type EncryptRequest = { + value: string + reads: number + ttl: TTLFormat +} + +export type StoredData = { + encryptedValue: string + reads: number + createdAt: number + ttl: TTLFormat +} + +export type DecryptResponse = { + decryptedValue: string + reads: number + ttl: string +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 61607215..4fab352f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -5,3 +5,4 @@ export * from './Session' export * from './Team' export * from './Token' export * from './Cli' +export * from './Vault' From a98c515f6a43fb28eb12fc21e118ea2f9b77dfba Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 14:58:48 +0100 Subject: [PATCH 02/24] Update service arch --- .../server/api/admin/users/[userId].delete.ts | 4 +- .../server/api/admin/users/[userId].put.ts | 2 +- .../shelve/server/api/auth/currentUser.get.ts | 4 +- apps/shelve/server/api/github/repos.get.ts | 4 +- apps/shelve/server/api/github/upload.post.ts | 4 +- .../server/api/project/[id]/index.delete.ts | 4 +- .../server/api/project/[id]/index.get.ts | 4 +- .../server/api/project/[id]/index.put.ts | 4 +- .../api/project/[id]/team/[teamId].delete.ts | 4 +- .../api/project/[id]/team/[teamId].post.ts | 4 +- apps/shelve/server/api/project/index.get.ts | 4 +- apps/shelve/server/api/project/index.post.ts | 4 +- .../server/api/project/name/[name].get.ts | 3 +- .../server/api/teams/[teamId]/index.delete.ts | 4 +- .../api/teams/[teamId]/members/[id].delete.ts | 4 +- .../api/teams/[teamId]/members/index.post.ts | 4 +- apps/shelve/server/api/teams/index.get.ts | 4 +- apps/shelve/server/api/teams/index.post.ts | 4 +- apps/shelve/server/api/tokens/[id].delete.ts | 3 +- apps/shelve/server/api/tokens/[token].get.ts | 4 +- apps/shelve/server/api/tokens/index.get.ts | 4 +- apps/shelve/server/api/tokens/index.post.ts | 4 +- apps/shelve/server/api/user/index.delete.ts | 4 +- apps/shelve/server/api/user/index.put.ts | 4 +- .../server/api/user/teammate/index.get.ts | 4 +- .../server/api/variable/[id]/[env].delete.ts | 4 +- .../server/api/variable/[id]/[env].get.ts | 6 +- .../server/api/variable/index.delete.ts | 4 +- apps/shelve/server/api/variable/index.post.ts | 8 +- .../api/variable/project/[projectId].get.ts | 4 +- apps/shelve/server/api/vault.ts | 140 +----------------- .../github.service.ts} | 0 .../project.service.ts} | 0 .../resend.service.ts} | 0 .../teammate.service.ts} | 0 .../teams.service.ts} | 0 .../token.service.ts} | 0 .../user.service.ts} | 0 .../variable.service.ts} | 0 apps/shelve/server/services/vault.service.ts | 135 +++++++++++++++++ 40 files changed, 197 insertions(+), 200 deletions(-) rename apps/shelve/server/{app/githubService.ts => services/github.service.ts} (100%) rename apps/shelve/server/{app/projectService.ts => services/project.service.ts} (100%) rename apps/shelve/server/{app/resendService.ts => services/resend.service.ts} (100%) rename apps/shelve/server/{app/teammateService.ts => services/teammate.service.ts} (100%) rename apps/shelve/server/{app/teamsService.ts => services/teams.service.ts} (100%) rename apps/shelve/server/{app/tokenService.ts => services/token.service.ts} (100%) rename apps/shelve/server/{app/userService.ts => services/user.service.ts} (100%) rename apps/shelve/server/{app/variableService.ts => services/variable.service.ts} (100%) create mode 100644 apps/shelve/server/services/vault.service.ts diff --git a/apps/shelve/server/api/admin/users/[userId].delete.ts b/apps/shelve/server/api/admin/users/[userId].delete.ts index 211c3d40..4df37d41 100644 --- a/apps/shelve/server/api/admin/users/[userId].delete.ts +++ b/apps/shelve/server/api/admin/users/[userId].delete.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { deleteUser } from '~~/server/app/userService' +import type { H3Event } from 'h3' +import { deleteUser } from '~~/server/services/user.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/admin/users/[userId].put.ts b/apps/shelve/server/api/admin/users/[userId].put.ts index df8c67df..19998b3b 100644 --- a/apps/shelve/server/api/admin/users/[userId].put.ts +++ b/apps/shelve/server/api/admin/users/[userId].put.ts @@ -1,4 +1,4 @@ -import { H3Event } from 'h3' +import type { H3Event } from 'h3' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/auth/currentUser.get.ts b/apps/shelve/server/api/auth/currentUser.get.ts index 2becffbc..c88dba91 100644 --- a/apps/shelve/server/api/auth/currentUser.get.ts +++ b/apps/shelve/server/api/auth/currentUser.get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getUserByAuthToken } from '~~/server/app/tokenService' +import type { H3Event } from 'h3' +import { getUserByAuthToken } from '~~/server/services/token.service' export default eventHandler(async (event: H3Event) => { const authToken = getCookie(event, 'authToken') || '' diff --git a/apps/shelve/server/api/github/repos.get.ts b/apps/shelve/server/api/github/repos.get.ts index 1aa44939..b179ec43 100644 --- a/apps/shelve/server/api/github/repos.get.ts +++ b/apps/shelve/server/api/github/repos.get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getUserRepos } from '~~/server/app/githubService' +import type { H3Event } from 'h3' +import { getUserRepos } from '~~/server/services/github.service' export default defineEventHandler(async (event: H3Event) => { return await getUserRepos(event) diff --git a/apps/shelve/server/api/github/upload.post.ts b/apps/shelve/server/api/github/upload.post.ts index f9c3a5c5..0f6438b2 100644 --- a/apps/shelve/server/api/github/upload.post.ts +++ b/apps/shelve/server/api/github/upload.post.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { uploadFile } from '~~/server/app/githubService' +import type { H3Event } from 'h3' +import { uploadFile } from '~~/server/services/github.service' export default defineEventHandler(async (event: H3Event) => { const formData = await readMultipartFormData(event) diff --git a/apps/shelve/server/api/project/[id]/index.delete.ts b/apps/shelve/server/api/project/[id]/index.delete.ts index c2676ef8..63b65e6f 100644 --- a/apps/shelve/server/api/project/[id]/index.delete.ts +++ b/apps/shelve/server/api/project/[id]/index.delete.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { deleteProject } from '~~/server/app/projectService' +import type { H3Event } from 'h3' +import { deleteProject } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/project/[id]/index.get.ts b/apps/shelve/server/api/project/[id]/index.get.ts index 53487206..5e7df19b 100644 --- a/apps/shelve/server/api/project/[id]/index.get.ts +++ b/apps/shelve/server/api/project/[id]/index.get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getProjectById } from '~~/server/app/projectService' +import type { H3Event } from 'h3' +import { getProjectById } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string diff --git a/apps/shelve/server/api/project/[id]/index.put.ts b/apps/shelve/server/api/project/[id]/index.put.ts index 2ce0f91b..b1b250e8 100644 --- a/apps/shelve/server/api/project/[id]/index.put.ts +++ b/apps/shelve/server/api/project/[id]/index.put.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { updateProject } from '~~/server/app/projectService' +import type { H3Event } from 'h3' +import { updateProject } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/project/[id]/team/[teamId].delete.ts b/apps/shelve/server/api/project/[id]/team/[teamId].delete.ts index bb5483df..03170d25 100644 --- a/apps/shelve/server/api/project/[id]/team/[teamId].delete.ts +++ b/apps/shelve/server/api/project/[id]/team/[teamId].delete.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { removeTeamFromProject } from '~~/server/app/projectService' +import type { H3Event } from 'h3' +import { removeTeamFromProject } from '~~/server/services/project.service' export default defineEventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string diff --git a/apps/shelve/server/api/project/[id]/team/[teamId].post.ts b/apps/shelve/server/api/project/[id]/team/[teamId].post.ts index 193e7e9c..3699f3cf 100644 --- a/apps/shelve/server/api/project/[id]/team/[teamId].post.ts +++ b/apps/shelve/server/api/project/[id]/team/[teamId].post.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { addTeamToProject } from '~~/server/app/projectService' +import type { H3Event } from 'h3' +import { addTeamToProject } from '~~/server/services/project.service' export default defineEventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string diff --git a/apps/shelve/server/api/project/index.get.ts b/apps/shelve/server/api/project/index.get.ts index f16a932e..85e78be1 100644 --- a/apps/shelve/server/api/project/index.get.ts +++ b/apps/shelve/server/api/project/index.get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getProjectsByUserId } from '~~/server/app/projectService' +import type { H3Event } from 'h3' +import { getProjectsByUserId } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/project/index.post.ts b/apps/shelve/server/api/project/index.post.ts index d1a904d8..1762cc9e 100644 --- a/apps/shelve/server/api/project/index.post.ts +++ b/apps/shelve/server/api/project/index.post.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { createProject } from '~~/server/app/projectService' +import type { H3Event } from 'h3' +import { createProject } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/project/name/[name].get.ts b/apps/shelve/server/api/project/name/[name].get.ts index 7db40756..1971ac0f 100644 --- a/apps/shelve/server/api/project/name/[name].get.ts +++ b/apps/shelve/server/api/project/name/[name].get.ts @@ -1,5 +1,4 @@ -import { H3Event } from 'h3' - +import type { H3Event } from 'h3' export default eventHandler(async (event: H3Event) => { const paramName = getRouterParam(event, 'name') diff --git a/apps/shelve/server/api/teams/[teamId]/index.delete.ts b/apps/shelve/server/api/teams/[teamId]/index.delete.ts index 2425a4ce..dfa09f14 100644 --- a/apps/shelve/server/api/teams/[teamId]/index.delete.ts +++ b/apps/shelve/server/api/teams/[teamId]/index.delete.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { deleteTeam } from '~~/server/app/teamsService' +import type { H3Event } from 'h3' +import { deleteTeam } from '~~/server/services/teams.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/teams/[teamId]/members/[id].delete.ts b/apps/shelve/server/api/teams/[teamId]/members/[id].delete.ts index aed52b56..0569b78c 100644 --- a/apps/shelve/server/api/teams/[teamId]/members/[id].delete.ts +++ b/apps/shelve/server/api/teams/[teamId]/members/[id].delete.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { removeMember } from '~~/server/app/teamsService' +import type { H3Event } from 'h3' +import { removeMember } from '~~/server/services/teams.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/teams/[teamId]/members/index.post.ts b/apps/shelve/server/api/teams/[teamId]/members/index.post.ts index 739cc390..7d34f74e 100644 --- a/apps/shelve/server/api/teams/[teamId]/members/index.post.ts +++ b/apps/shelve/server/api/teams/[teamId]/members/index.post.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { upsertMember } from '~~/server/app/teamsService' +import type { H3Event } from 'h3' +import { upsertMember } from '~~/server/services/teams.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/teams/index.get.ts b/apps/shelve/server/api/teams/index.get.ts index 8627a002..2a465bb8 100644 --- a/apps/shelve/server/api/teams/index.get.ts +++ b/apps/shelve/server/api/teams/index.get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getTeamByUserId } from '~~/server/app/teamsService' +import type { H3Event } from 'h3' +import { getTeamByUserId } from '~~/server/services/teams.service' export default eventHandler((event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/teams/index.post.ts b/apps/shelve/server/api/teams/index.post.ts index c97042b6..26bfbb59 100644 --- a/apps/shelve/server/api/teams/index.post.ts +++ b/apps/shelve/server/api/teams/index.post.ts @@ -1,6 +1,6 @@ -import { H3Event } from 'h3' +import type { H3Event } from 'h3' import type { CreateTeamInput } from '@shelve/types' -import { createTeam } from '~~/server/app/teamsService' +import { createTeam } from '~~/server/services/teams.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/tokens/[id].delete.ts b/apps/shelve/server/api/tokens/[id].delete.ts index 7497480f..104a43d8 100644 --- a/apps/shelve/server/api/tokens/[id].delete.ts +++ b/apps/shelve/server/api/tokens/[id].delete.ts @@ -1,5 +1,4 @@ -import { H3Event } from 'h3' - +import type { H3Event } from 'h3' export default defineEventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/tokens/[token].get.ts b/apps/shelve/server/api/tokens/[token].get.ts index 1058eec0..ca9f58a1 100644 --- a/apps/shelve/server/api/tokens/[token].get.ts +++ b/apps/shelve/server/api/tokens/[token].get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getUserByAuthToken } from '~~/server/app/tokenService' +import type { H3Event } from 'h3' +import { getUserByAuthToken } from '~~/server/services/token.service' export default defineEventHandler((event: H3Event) => { const token = getRouterParam(event, 'token') as string diff --git a/apps/shelve/server/api/tokens/index.get.ts b/apps/shelve/server/api/tokens/index.get.ts index 1e613f65..e40e9ca5 100644 --- a/apps/shelve/server/api/tokens/index.get.ts +++ b/apps/shelve/server/api/tokens/index.get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getTokensByUserId } from '~~/server/app/tokenService' +import type { H3Event } from 'h3' +import { getTokensByUserId } from '~~/server/services/token.service' export default defineEventHandler((event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/tokens/index.post.ts b/apps/shelve/server/api/tokens/index.post.ts index 46ab7eb4..5d9a6505 100644 --- a/apps/shelve/server/api/tokens/index.post.ts +++ b/apps/shelve/server/api/tokens/index.post.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { createToken } from '~~/server/app/tokenService' +import type { H3Event } from 'h3' +import { createToken } from '~~/server/services/token.service' export default defineEventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/user/index.delete.ts b/apps/shelve/server/api/user/index.delete.ts index a3f3aa57..6482f6b5 100644 --- a/apps/shelve/server/api/user/index.delete.ts +++ b/apps/shelve/server/api/user/index.delete.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { deleteUser } from '~~/server/app/userService' +import type { H3Event } from 'h3' +import { deleteUser } from '~~/server/services/user.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/user/index.put.ts b/apps/shelve/server/api/user/index.put.ts index be554e51..bd16fe37 100644 --- a/apps/shelve/server/api/user/index.put.ts +++ b/apps/shelve/server/api/user/index.put.ts @@ -1,6 +1,6 @@ -import { H3Event } from 'h3' +import type { H3Event } from 'h3' import type { UpdateUserInput } from '@shelve/types' -import { updateUser } from '~~/server/app/userService' +import { updateUser } from '~~/server/services/user.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/user/teammate/index.get.ts b/apps/shelve/server/api/user/teammate/index.get.ts index 440e0cf0..7d0e4aaf 100644 --- a/apps/shelve/server/api/user/teammate/index.get.ts +++ b/apps/shelve/server/api/user/teammate/index.get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getTeammatesByUserId } from '~~/server/app/teammateService' +import type { H3Event } from 'h3' +import { getTeammatesByUserId } from '~~/server/services/teammate.service' export default eventHandler((event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/variable/[id]/[env].delete.ts b/apps/shelve/server/api/variable/[id]/[env].delete.ts index 53f61e48..fc187ab1 100644 --- a/apps/shelve/server/api/variable/[id]/[env].delete.ts +++ b/apps/shelve/server/api/variable/[id]/[env].delete.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { deleteVariable } from '~~/server/app/variableService' +import type { H3Event } from 'h3' +import { deleteVariable } from '~~/server/services/variable.service' export default eventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string diff --git a/apps/shelve/server/api/variable/[id]/[env].get.ts b/apps/shelve/server/api/variable/[id]/[env].get.ts index 283e35aa..544341e6 100644 --- a/apps/shelve/server/api/variable/[id]/[env].get.ts +++ b/apps/shelve/server/api/variable/[id]/[env].get.ts @@ -1,6 +1,6 @@ -import { Environment } from '@shelve/types' -import { H3Event } from 'h3' -import { getVariablesByProjectId } from '~~/server/app/variableService' +import type { Environment } from '@shelve/types' +import type { H3Event } from 'h3' +import { getVariablesByProjectId } from '~~/server/services/variable.service' export default eventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string diff --git a/apps/shelve/server/api/variable/index.delete.ts b/apps/shelve/server/api/variable/index.delete.ts index 13817b57..1c5043f6 100644 --- a/apps/shelve/server/api/variable/index.delete.ts +++ b/apps/shelve/server/api/variable/index.delete.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { deleteVariables } from '~~/server/app/variableService' +import type { H3Event } from 'h3' +import { deleteVariables } from '~~/server/services/variable.service' export default defineEventHandler(async (event: H3Event) => { const { user } = event.context diff --git a/apps/shelve/server/api/variable/index.post.ts b/apps/shelve/server/api/variable/index.post.ts index 43ba670a..fb715344 100644 --- a/apps/shelve/server/api/variable/index.post.ts +++ b/apps/shelve/server/api/variable/index.post.ts @@ -1,7 +1,7 @@ -import { type VariablesCreateInput } from '@shelve/types' -import { H3Event } from 'h3' -import { upsertVariable } from '~~/server/app/variableService' -import { getProjectById } from '~~/server/app/projectService' +import type { VariablesCreateInput } from '@shelve/types' +import type { H3Event } from 'h3' +import { upsertVariable } from '~~/server/services/variable.service' +import { getProjectById } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const variablesCreateInput = await readBody(event) as VariablesCreateInput diff --git a/apps/shelve/server/api/variable/project/[projectId].get.ts b/apps/shelve/server/api/variable/project/[projectId].get.ts index 21466820..6d004f37 100644 --- a/apps/shelve/server/api/variable/project/[projectId].get.ts +++ b/apps/shelve/server/api/variable/project/[projectId].get.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getVariablesByProjectId } from '~~/server/app/variableService' +import type { H3Event } from 'h3' +import { getVariablesByProjectId } from '~~/server/services/variable.service' export default eventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'projectId') as string diff --git a/apps/shelve/server/api/vault.ts b/apps/shelve/server/api/vault.ts index fc4bd89f..9f3d7046 100644 --- a/apps/shelve/server/api/vault.ts +++ b/apps/shelve/server/api/vault.ts @@ -1,147 +1,11 @@ -import type { DecryptResponse, EncryptRequest, StoredData, TTLFormat } from '@shelve/types' import type { H3Event } from 'h3' -import { seal, unseal } from '@shelve/crypto' - -class VaultService { - - private readonly storage: Storage - private readonly encryptionKey: string - private readonly siteUrl: string - private readonly PREFIX = 'vault:' - - private readonly TTL_MAP = { - '1d': 24 * 60 * 60, // 1 day in seconds - '7d': 7 * 24 * 60 * 60, // 7 days in seconds - '30d': 30 * 24 * 60 * 60 // 30 days in seconds - } - - constructor() { - const config = useRuntimeConfig() - this.encryptionKey = config.private.encryptionKey - this.siteUrl = config.public.siteUrl - this.storage = useStorage('vault') - } - - private generateKey(id: string): string { - return `${this.PREFIX}${id}` - } - - private generateRandomId(): string { - return Math.random().toString(36).slice(2) - } - - private calculateTimeLeft(createdAt: number, ttl: TTLFormat): number { - const ttlInSeconds = this.TTL_MAP[ttl] - const now = Date.now() - const expiresAt = createdAt + (ttlInSeconds * 1000) - return Math.max(0, Math.floor((expiresAt - now) / 1000)) - } - - private formatTimeLeft(seconds: number): string { - if (seconds <= 0) return 'Expired' - - const days = Math.floor(seconds / (24 * 60 * 60)) - const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60)) - const minutes = Math.floor((seconds % (60 * 60)) / 60) - - const parts = [] - - if (days > 0) { - parts.push(`${days}d`) - } - if (hours > 0) { - parts.push(`${hours}h`) - } - if (minutes > 0) { - parts.push(`${minutes}m`) - } - - // If less than a minute left - if (parts.length === 0) { - return 'Less than 1 minute' - } - - return parts.join(' ') - } - - async decrypt(id: string): Promise { - const key = this.generateKey(id) - const storedData = await this.storage.getItem(key) - - if (!storedData) { - throw createError({ - statusCode: 400, - statusMessage: 'Invalid id or link has expired' - }) - } - - const { encryptedValue, reads, createdAt, ttl } = storedData - const timeLeft = this.calculateTimeLeft(createdAt, ttl) - - if (timeLeft <= 0) { - await this.storage.removeItem(key) - throw createError({ - statusCode: 400, - statusMessage: 'Link has expired' - }) - } - - if (reads <= 0) { - await this.storage.removeItem(key) - throw createError({ - statusCode: 400, - statusMessage: 'Maximum number of reads reached' - }) - } - - const decryptedValue = await unseal(encryptedValue, this.encryptionKey) - - const updatedReads = reads - 1 - await this.storage.setItem(key, { - ...storedData, - reads: updatedReads - }) - - if (updatedReads === 0) { - await this.storage.removeItem(key) - } - - return { - decryptedValue, - reads: updatedReads, - ttl: this.formatTimeLeft(timeLeft) - } - } - - async encrypt(data: EncryptRequest): Promise { - const encryptedValue = await seal(data.value, this.encryptionKey) - const randomId = this.generateRandomId() - const key = this.generateKey(randomId) - - const storedData: StoredData = { - encryptedValue, - reads: data.reads, - createdAt: Date.now(), - ttl: data.ttl - } - - await this.storage.setItem(key, storedData) - return this.generateShareUrl(randomId) - } - - private generateShareUrl(id: string): string { - return `${this.siteUrl}/vault?id=${id}` - } - -} +import { VaultService } from '~~/server/services/vault.service' export default defineEventHandler(async (event: H3Event) => { const vault = new VaultService() const { id } = await getQuery(event) - if (id) { - return vault.decrypt(id) - } + if (id) return vault.decrypt(id) const body = await readBody(event) return vault.encrypt(body) diff --git a/apps/shelve/server/app/githubService.ts b/apps/shelve/server/services/github.service.ts similarity index 100% rename from apps/shelve/server/app/githubService.ts rename to apps/shelve/server/services/github.service.ts diff --git a/apps/shelve/server/app/projectService.ts b/apps/shelve/server/services/project.service.ts similarity index 100% rename from apps/shelve/server/app/projectService.ts rename to apps/shelve/server/services/project.service.ts diff --git a/apps/shelve/server/app/resendService.ts b/apps/shelve/server/services/resend.service.ts similarity index 100% rename from apps/shelve/server/app/resendService.ts rename to apps/shelve/server/services/resend.service.ts diff --git a/apps/shelve/server/app/teammateService.ts b/apps/shelve/server/services/teammate.service.ts similarity index 100% rename from apps/shelve/server/app/teammateService.ts rename to apps/shelve/server/services/teammate.service.ts diff --git a/apps/shelve/server/app/teamsService.ts b/apps/shelve/server/services/teams.service.ts similarity index 100% rename from apps/shelve/server/app/teamsService.ts rename to apps/shelve/server/services/teams.service.ts diff --git a/apps/shelve/server/app/tokenService.ts b/apps/shelve/server/services/token.service.ts similarity index 100% rename from apps/shelve/server/app/tokenService.ts rename to apps/shelve/server/services/token.service.ts diff --git a/apps/shelve/server/app/userService.ts b/apps/shelve/server/services/user.service.ts similarity index 100% rename from apps/shelve/server/app/userService.ts rename to apps/shelve/server/services/user.service.ts diff --git a/apps/shelve/server/app/variableService.ts b/apps/shelve/server/services/variable.service.ts similarity index 100% rename from apps/shelve/server/app/variableService.ts rename to apps/shelve/server/services/variable.service.ts diff --git a/apps/shelve/server/services/vault.service.ts b/apps/shelve/server/services/vault.service.ts new file mode 100644 index 00000000..b75f1a87 --- /dev/null +++ b/apps/shelve/server/services/vault.service.ts @@ -0,0 +1,135 @@ +import type { DecryptResponse, EncryptRequest, StoredData, TTLFormat } from '@shelve/types' +import { seal, unseal } from '@shelve/crypto' + +export class VaultService { + + private readonly storage: Storage + private readonly encryptionKey: string + private readonly siteUrl: string + private readonly PREFIX = 'vault:' + + private readonly TTL_MAP = { + '1d': 24 * 60 * 60, // 1 day in seconds + '7d': 7 * 24 * 60 * 60, // 7 days in seconds + '30d': 30 * 24 * 60 * 60 // 30 days in seconds + } + + constructor() { + const config = useRuntimeConfig() + this.encryptionKey = config.private.encryptionKey + this.siteUrl = config.public.siteUrl + this.storage = useStorage('vault') + } + + private generateKey(id: string): string { + return `${this.PREFIX}${id}` + } + + private generateRandomId(): string { + return Math.random().toString(36).slice(2) + } + + private calculateTimeLeft(createdAt: number, ttl: TTLFormat): number { + const ttlInSeconds = this.TTL_MAP[ttl] + const now = Date.now() + const expiresAt = createdAt + (ttlInSeconds * 1000) + return Math.max(0, Math.floor((expiresAt - now) / 1000)) + } + + private formatTimeLeft(seconds: number): string { + if (seconds <= 0) return 'Expired' + + const days = Math.floor(seconds / (24 * 60 * 60)) + const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60)) + const minutes = Math.floor((seconds % (60 * 60)) / 60) + + const parts = [] + + if (days > 0) { + parts.push(`${days}d`) + } + if (hours > 0) { + parts.push(`${hours}h`) + } + if (minutes > 0) { + parts.push(`${minutes}m`) + } + + // If less than a minute left + if (parts.length === 0) { + return 'Less than 1 minute' + } + + return parts.join(' ') + } + + async decrypt(id: string): Promise { + const key = this.generateKey(id) + const storedData = await this.storage.getItem(key) + + if (!storedData) { + throw createError({ + statusCode: 400, + statusMessage: 'Invalid id or link has expired' + }) + } + + const { encryptedValue, reads, createdAt, ttl } = storedData + const timeLeft = this.calculateTimeLeft(createdAt, ttl) + + if (timeLeft <= 0) { + await this.storage.removeItem(key) + throw createError({ + statusCode: 400, + statusMessage: 'Link has expired' + }) + } + + if (reads <= 0) { + await this.storage.removeItem(key) + throw createError({ + statusCode: 400, + statusMessage: 'Maximum number of reads reached' + }) + } + + const decryptedValue = await unseal(encryptedValue, this.encryptionKey) + + const updatedReads = reads - 1 + await this.storage.setItem(key, { + ...storedData, + reads: updatedReads + }) + + if (updatedReads === 0) { + await this.storage.removeItem(key) + } + + return { + decryptedValue, + reads: updatedReads, + ttl: this.formatTimeLeft(timeLeft) + } + } + + async encrypt(data: EncryptRequest): Promise { + const encryptedValue = await seal(data.value, this.encryptionKey) + const randomId = this.generateRandomId() + const key = this.generateKey(randomId) + + const storedData: StoredData = { + encryptedValue, + reads: data.reads, + createdAt: Date.now(), + ttl: data.ttl + } + + await this.storage.setItem(key, storedData) + return this.generateShareUrl(randomId) + } + + private generateShareUrl(id: string): string { + return `${this.siteUrl}/vault?id=${id}` + } + +} From fd26a0169856af182cadb188f9545216acbd5d5b Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 14:59:54 +0100 Subject: [PATCH 03/24] fix --- apps/shelve/server/middleware/1.serverAuth.ts | 4 ++-- apps/shelve/server/middleware/2.serverAdmin.ts | 2 +- apps/shelve/server/routes/auth/github.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/shelve/server/middleware/1.serverAuth.ts b/apps/shelve/server/middleware/1.serverAuth.ts index a0198e99..d6fc047e 100644 --- a/apps/shelve/server/middleware/1.serverAuth.ts +++ b/apps/shelve/server/middleware/1.serverAuth.ts @@ -1,5 +1,5 @@ -import { H3Event } from 'h3' -import { getUserByAuthToken } from '~~/server/app/tokenService' +import type { H3Event } from 'h3' +import { getUserByAuthToken } from '~~/server/services/token.service' export default defineEventHandler(async (event: H3Event) => { const protectedRoutes = [ diff --git a/apps/shelve/server/middleware/2.serverAdmin.ts b/apps/shelve/server/middleware/2.serverAdmin.ts index ff23b31b..aea9dd22 100644 --- a/apps/shelve/server/middleware/2.serverAdmin.ts +++ b/apps/shelve/server/middleware/2.serverAdmin.ts @@ -1,5 +1,5 @@ import { Role } from '@shelve/types' -import { H3Event } from 'h3' +import type { H3Event } from 'h3' export default defineEventHandler((event: H3Event) => { const protectedRoutes = ['/api/admin'] diff --git a/apps/shelve/server/routes/auth/github.ts b/apps/shelve/server/routes/auth/github.ts index a1f565a8..da374866 100644 --- a/apps/shelve/server/routes/auth/github.ts +++ b/apps/shelve/server/routes/auth/github.ts @@ -1,4 +1,4 @@ -import { upsertUser } from '~~/server/app/userService' +import { upsertUser } from '~~/server/services/user.service' export default defineOAuthGitHubEventHandler({ config: { From eedcaaa5faec8f60e1862e0a449fde69eb8692d5 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 15:08:58 +0100 Subject: [PATCH 04/24] switch crypto package to publish --- packages/crypto/build.config.ts | 12 ++++++++++++ packages/crypto/eslint.config.js | 12 +----------- packages/crypto/package.json | 19 +++++++++++-------- 3 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 packages/crypto/build.config.ts diff --git a/packages/crypto/build.config.ts b/packages/crypto/build.config.ts new file mode 100644 index 00000000..4aedf54e --- /dev/null +++ b/packages/crypto/build.config.ts @@ -0,0 +1,12 @@ +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + declaration: true, + rollup: { + inlineDependencies: true, + resolve: { + exportConditions: ['production', 'node'], + }, + }, + entries: ['src/index'], +}) diff --git a/packages/crypto/eslint.config.js b/packages/crypto/eslint.config.js index 3fdf1e79..8f36791f 100644 --- a/packages/crypto/eslint.config.js +++ b/packages/crypto/eslint.config.js @@ -1,13 +1,3 @@ import { createConfig } from "@hrcd/eslint-config" -export default createConfig({ - typescript: { - strict: true - }, - features: { - jsdoc: { - enable: true, - strict: true - } - } -}) +export default createConfig({}) diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 59981623..fef4b0e3 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -2,18 +2,21 @@ "name": "@shelve/crypto", "version": "1.0.0", "type": "module", - "private": true, + "publishConfig": { + "access": "public" + }, "scripts": { - "dev": "bun run src/index.ts" + "dev": "bun run src/index.ts", + "build": "unbuild", + "release": "bun run build && npm publish" }, - "files": [ - "index.ts" - ], "exports": { - ".": "./src/index.ts" + ".": "./dist/index.mjs" }, - "main": "index.ts", - "types": "index.ts", + "types": "./dist/index.d.ts", + "files": [ + "./dist" + ], "devDependencies": { "@types/crypto-js": "^4.2.2", "typescript": "latest" From 2a0b902ba749b214457a5d16fab05fed375dc93d Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 15:11:33 +0100 Subject: [PATCH 05/24] switch types package to publish --- packages/crypto/package.json | 3 ++- packages/types/build.config.ts | 12 ++++++++++++ packages/types/package.json | 25 ++++++++++++++++--------- 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 packages/types/build.config.ts diff --git a/packages/crypto/package.json b/packages/crypto/package.json index fef4b0e3..52da9b78 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -19,7 +19,8 @@ ], "devDependencies": { "@types/crypto-js": "^4.2.2", - "typescript": "latest" + "typescript": "latest", + "unbuild": "^2.0.0" }, "dependencies": { "crypto-js": "^4.2.0", diff --git a/packages/types/build.config.ts b/packages/types/build.config.ts new file mode 100644 index 00000000..4aedf54e --- /dev/null +++ b/packages/types/build.config.ts @@ -0,0 +1,12 @@ +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + declaration: true, + rollup: { + inlineDependencies: true, + resolve: { + exportConditions: ['production', 'node'], + }, + }, + entries: ['src/index'], +}) diff --git a/packages/types/package.json b/packages/types/package.json index 23c1c17e..b315d0fa 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,17 +1,24 @@ { "name": "@shelve/types", - "version": "0.0.0", + "version": "1.0.0", "type": "module", - "private": true, - "files": [ - "index.ts" - ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "bun run src/index.ts", + "build": "unbuild", + "release": "bun run build && npm publish" + }, "exports": { - ".": "./src/index.ts" + ".": "./dist/index.mjs" }, - "main": "index.ts", - "types": "index.ts", + "types": "./dist/index.d.ts", + "files": [ + "./dist" + ], "devDependencies": { - "typescript": "latest" + "typescript": "latest", + "unbuild": "^2.0.0" } } From 070ab47a757cd84c2be1dca846c37ede69992857 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 15:16:43 +0100 Subject: [PATCH 06/24] fix build --- apps/shelve/server/services/teams.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/shelve/server/services/teams.service.ts b/apps/shelve/server/services/teams.service.ts index 8d695500..c72bff8a 100644 --- a/apps/shelve/server/services/teams.service.ts +++ b/apps/shelve/server/services/teams.service.ts @@ -1,5 +1,5 @@ import { type CreateTeamInput, Role, TeamRole } from '@shelve/types' -import { deleteCachedUserProjects } from '~~/server/app/projectService' +import { deleteCachedUserProjects } from '~~/server/services/project.service' export async function createTeam(createTeamInput: CreateTeamInput, userId: number) { await deleteCachedTeamByUserId(userId) From 08d34dbd536f7d66a80f4b4725c1d1c5e09374b9 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 15:19:21 +0100 Subject: [PATCH 07/24] Improve turbo config --- turbo.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/turbo.json b/turbo.json index 3cc1bf2c..420af560 100644 --- a/turbo.json +++ b/turbo.json @@ -3,10 +3,9 @@ "globalDependencies": ["**/.env"], "tasks": { "build": { - "dependsOn": ["^build"], "outputs": [ - "dist/**", - ".output/**" + "**/dist/**", + "**/.output/**" ] }, "generate" : { From 316d48a7fc1248f6cdee8a3e2f831dd7eb732026 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 15:31:05 +0100 Subject: [PATCH 08/24] Add remote turbo cache to ci --- .github/workflows/build.yml | 4 ++++ .github/workflows/continuous-release.yml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a2483c1..f7552d25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,10 @@ permissions: jobs: autofix: runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index 8f9d25df..110f4eb1 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -4,6 +4,9 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} steps: - name: Checkout code From 0208448f10311e66796083f154de01379e92505b Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 15:38:12 +0100 Subject: [PATCH 09/24] wip --- turbo.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/turbo.json b/turbo.json index 420af560..6438aedd 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,6 @@ { "$schema": "https://turbo.build/schema.json", - "globalDependencies": ["**/.env"], + "globalDependencies": [".env"], "tasks": { "build": { "outputs": [ @@ -11,8 +11,8 @@ "generate" : { "dependsOn": ["^generate"], "outputs": [ - "dist/**", - ".output/**" + "**/dist/**", + "**/.output/**" ] }, "dev": { From 19feb9d047bb1ffedd67a80df11f26967e801207 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 15:44:37 +0100 Subject: [PATCH 10/24] wip --- apps/shelve/package.json | 4 ++-- packages/cli/package.json | 6 +++--- packages/crypto/package.json | 3 ++- packages/types/package.json | 3 ++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/shelve/package.json b/apps/shelve/package.json index 4a5e4b47..23869bc4 100644 --- a/apps/shelve/package.json +++ b/apps/shelve/package.json @@ -31,8 +31,8 @@ "@nuxtjs/seo": "^2.0.0-rc.23", "@prisma/client": "^5.21.1", "@shelve/cli": "^2.4.0", - "@shelve/crypto": "workspace:*", - "@shelve/types": "workspace:*", + "@shelve/crypto": "*", + "@shelve/types": "*", "@tsparticles/engine": "^3.5.0", "@tsparticles/slim": "^3.5.0", "@vitejs/plugin-vue": "^5.1.4", diff --git a/packages/cli/package.json b/packages/cli/package.json index 3fda1cb2..62a4b0fd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -46,13 +46,13 @@ "npm-registry-fetch": "^18.0.2", "nypm": "^0.3.12", "ofetch": "^1.4.1", - "semver": "^7.6.3", - "unbuild": "^2.0.0" + "semver": "^7.6.3" }, "devDependencies": { - "@shelve/types": "workspace:*", + "@shelve/types": "*", "@types/bun": "^1.1.12", "@types/npm-registry-fetch": "^8.0.7", + "unbuild": "^2.0.0", "@types/semver": "^7.5.8", "eslint": "^9.13.0", "release-it": "^17.10.0", diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 52da9b78..eb1b19f5 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -10,12 +10,13 @@ "build": "unbuild", "release": "bun run build && npm publish" }, + "main": "./dist/index.mjs", "exports": { ".": "./dist/index.mjs" }, "types": "./dist/index.d.ts", "files": [ - "./dist" + "dist" ], "devDependencies": { "@types/crypto-js": "^4.2.2", diff --git a/packages/types/package.json b/packages/types/package.json index b315d0fa..c8c7cb52 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -10,12 +10,13 @@ "build": "unbuild", "release": "bun run build && npm publish" }, + "main": "./dist/index.mjs", "exports": { ".": "./dist/index.mjs" }, "types": "./dist/index.d.ts", "files": [ - "./dist" + "dist" ], "devDependencies": { "typescript": "latest", From 3d8c7dce6e75abfc2e54d077c74c3d476808b0ca Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 15:47:42 +0100 Subject: [PATCH 11/24] wip --- packages/crypto/package.json | 1 + packages/types/package.json | 1 + turbo.json | 8 ++++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/crypto/package.json b/packages/crypto/package.json index eb1b19f5..e8165ceb 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -7,6 +7,7 @@ }, "scripts": { "dev": "bun run src/index.ts", + "dev:prepare": "unbuild", "build": "unbuild", "release": "bun run build && npm publish" }, diff --git a/packages/types/package.json b/packages/types/package.json index c8c7cb52..82215fe6 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -7,6 +7,7 @@ }, "scripts": { "dev": "bun run src/index.ts", + "dev:prepare": "unbuild", "build": "unbuild", "release": "bun run build && npm publish" }, diff --git a/turbo.json b/turbo.json index 6438aedd..2a93b22f 100644 --- a/turbo.json +++ b/turbo.json @@ -1,8 +1,9 @@ { "$schema": "https://turbo.build/schema.json", - "globalDependencies": [".env"], + "globalDependencies": ["**/.env"], "tasks": { "build": { + "dependsOn": ["^build"], "outputs": [ "**/dist/**", "**/.output/**" @@ -20,7 +21,10 @@ "persistent": true }, "dev:prepare": { - "cache": false + "cache": false, + "outputs": [ + "**/dist/**" + ] }, "lint": {}, "lint:fix": {}, From b60595e206a30047755c54c37a2a2f207fb94897 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:51:38 +0000 Subject: [PATCH 12/24] chore: apply automated lint fixes --- bun.lockb | Bin 680360 -> 681808 bytes packages/cli/package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index b5503fc33dc3e6d14044e2dd1f5b80d566871358..f9952bbfb0bc044d42c754beabc887600b7408b4 100755 GIT binary patch delta 41780 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 delta 41124 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 diff --git a/packages/cli/package.json b/packages/cli/package.json index 62a4b0fd..c881f194 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,10 +52,10 @@ "@shelve/types": "*", "@types/bun": "^1.1.12", "@types/npm-registry-fetch": "^8.0.7", - "unbuild": "^2.0.0", "@types/semver": "^7.5.8", "eslint": "^9.13.0", "release-it": "^17.10.0", + "unbuild": "^2.0.0", "vitest": "^2.1.3" } } From 9cd46bf1c0fb4df4b52d5e7b77c9d4c8ea821674 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:06:44 +0100 Subject: [PATCH 13/24] wip --- bun.lockb | Bin 681808 -> 682168 bytes package.json | 16 ++++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bun.lockb b/bun.lockb index f9952bbfb0bc044d42c754beabc887600b7408b4..18846941928a65d1962ed0191140c3cf76cf6594 100755 GIT binary patch delta 92023 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

OY}aw% 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 diff --git a/package.json b/package.json index b88dc0ff..914c541c 100644 --- a/package.json +++ b/package.json @@ -4,18 +4,18 @@ "type": "module", "repository": "https://github.com/HugoRCD/shelve", "scripts": { - "build": "turbo build", + "build": "turbo run build", "build:app": "turbo run build --filter=@shelve/app", "build:cli": "turbo run build --filter=@shelve/cli", - "dev": "turbo dev --filter=@shelve/app", - "dev:app": "turbo dev --filter=@shelve/app", - "dev:cli": "turbo dev --filter=@shelve/cli", + "dev": "turbo run dev --filter=@shelve/app", + "dev:app": "turbo run dev --filter=@shelve/app", + "dev:cli": "turbo run dev --filter=@shelve/cli", "dev:prepare": "turbo run dev:prepare", "generate": "turbo run generate", - "lint": "turbo lint", - "lint:fix": "turbo lint:fix", - "test": "turbo test", - "typecheck": "turbo typecheck" + "lint": "turbo run lint", + "lint:fix": "turbo run lint:fix", + "test": "turbo run test", + "typecheck": "turbo run typecheck" }, "devDependencies": { "@hrcd/eslint-config": "edge", From 35e8e28e9bf570b54ad7f508c6e0e6686e08bc71 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:07:25 +0000 Subject: [PATCH 14/24] chore: apply automated lint fixes --- apps/shelve/app/components/OTP.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/shelve/app/components/OTP.vue b/apps/shelve/app/components/OTP.vue index 58615095..782783c9 100644 --- a/apps/shelve/app/components/OTP.vue +++ b/apps/shelve/app/components/OTP.vue @@ -15,7 +15,6 @@ const digits = reactive<[string | null]>([null]) const otp = defineModel({ type: String }) for (let i = 0; i < props.digitCount; i++) { - // eslint-disable-next-line vue/no-ref-object-reactivity-loss digits[i] = otp.value![i] || null } From c268e39f5e3e84018910999c175546b31d5d3815 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:20:07 +0100 Subject: [PATCH 15/24] user service refacto --- .../server/api/admin/users/[userId].delete.ts | 5 +- apps/shelve/server/api/user/index.delete.ts | 5 +- apps/shelve/server/api/user/index.put.ts | 5 +- apps/shelve/server/routes/auth/github.ts | 5 +- apps/shelve/server/services/user.service.ts | 165 ++++++++++++------ 5 files changed, 127 insertions(+), 58 deletions(-) diff --git a/apps/shelve/server/api/admin/users/[userId].delete.ts b/apps/shelve/server/api/admin/users/[userId].delete.ts index 4df37d41..aa3734cc 100644 --- a/apps/shelve/server/api/admin/users/[userId].delete.ts +++ b/apps/shelve/server/api/admin/users/[userId].delete.ts @@ -1,12 +1,13 @@ import type { H3Event } from 'h3' -import { deleteUser } from '~~/server/services/user.service' +import { UserService } from '~~/server/services/user.service' export default eventHandler(async (event: H3Event) => { + const userService = new UserService() const { user } = event.context const id = getRouterParam(event, 'userId') as string if (!id) throw createError({ statusCode: 400, statusMessage: 'missing params' }) if (user.id === parseInt(id)) throw createError({ statusCode: 400, statusMessage: 'you can\'t delete your own account' }) - await deleteUser(parseInt(id)) + await userService.deleteUser(parseInt(id)) return { statusCode: 200, message: 'user deleted', diff --git a/apps/shelve/server/api/user/index.delete.ts b/apps/shelve/server/api/user/index.delete.ts index 6482f6b5..76ed49b6 100644 --- a/apps/shelve/server/api/user/index.delete.ts +++ b/apps/shelve/server/api/user/index.delete.ts @@ -1,9 +1,10 @@ import type { H3Event } from 'h3' -import { deleteUser } from '~~/server/services/user.service' +import { UserService } from '~~/server/services/user.service' export default eventHandler(async (event: H3Event) => { + const userService = new UserService() const { user } = event.context - await deleteUser(user.id) + await userService.deleteUser(user.id) return { statusCode: 200, message: 'User deleted', diff --git a/apps/shelve/server/api/user/index.put.ts b/apps/shelve/server/api/user/index.put.ts index bd16fe37..e73f8d0b 100644 --- a/apps/shelve/server/api/user/index.put.ts +++ b/apps/shelve/server/api/user/index.put.ts @@ -1,14 +1,15 @@ import type { H3Event } from 'h3' import type { UpdateUserInput } from '@shelve/types' -import { updateUser } from '~~/server/services/user.service' +import { UserService } from '~~/server/services/user.service' export default eventHandler(async (event: H3Event) => { + const userService = new UserService() const { user } = event.context const { authToken } = event.context const updateUserInput = await readBody(event) as UpdateUserInput updateUserInput.username = updateUserInput.username?.trim() updateUserInput.email = updateUserInput.email?.trim() - const updatedUser = await updateUser(user, updateUserInput, authToken) + const updatedUser = await userService.updateUser(user, updateUserInput, authToken) await setUserSession(event, { user: { username: updatedUser.username, diff --git a/apps/shelve/server/routes/auth/github.ts b/apps/shelve/server/routes/auth/github.ts index da374866..14980cd0 100644 --- a/apps/shelve/server/routes/auth/github.ts +++ b/apps/shelve/server/routes/auth/github.ts @@ -1,4 +1,4 @@ -import { upsertUser } from '~~/server/services/user.service' +import { UserService } from '~~/server/services/user.service' export default defineOAuthGitHubEventHandler({ config: { @@ -6,8 +6,9 @@ export default defineOAuthGitHubEventHandler({ scope: ['repo', 'user:email'], }, async onSuccess(event, { user, tokens }) { + const userService = new UserService() try { - const _user = await upsertUser({ + const _user = await userService.upsertUser({ email: user.email, avatar: user.avatar_url, username: user.login, diff --git a/apps/shelve/server/services/user.service.ts b/apps/shelve/server/services/user.service.ts index ae7e9a6c..8943adaf 100644 --- a/apps/shelve/server/services/user.service.ts +++ b/apps/shelve/server/services/user.service.ts @@ -1,61 +1,126 @@ import type { publicUser, User, CreateUserInput, UpdateUserInput } from '@shelve/types' +import { PrismaClient } from '@prisma/client' -export async function upsertUser(createUserInput: CreateUserInput): Promise { - const foundUser = await prisma.user.findUnique({ - where: { - username: createUserInput.username, - }, - }) - const newUsername = foundUser ? `${createUserInput.username}_#${Math.floor(Math.random() * 1000)}` : createUserInput.username - const user = await prisma.user.upsert({ - where: { - email: createUserInput.email, - }, - update: { - updatedAt: new Date(), - }, - create: { - ...createUserInput, - username: newUsername, - }, - }) - return formatUser(user) -} +export class UserService { -export function getUserByEmail(email: string): Promise { - return prisma.user.findUnique({ - where: { - email, - } - }) -} + private readonly prisma: PrismaClient + private readonly storage: Storage -export async function deleteUser(userId: number): Promise { - await prisma.user.delete({ - where: { - id: userId, - }, - }) -} + constructor() { + this.prisma = usePrisma() + this.storage = useStorage('redis') + } -export async function updateUser(user: User, updateUserInput: UpdateUserInput, authToken: string): Promise { - const newUsername = updateUserInput.username - if (newUsername && newUsername !== user.username) { - const usernameTaken = await prisma.user.findFirst({ + /** + * Creates or updates a user + */ + async upsertUser(createUserInput: CreateUserInput): Promise { + const foundUser = await this.prisma.user.findUnique({ where: { + username: createUserInput.username, + }, + }) + + const newUsername = this.generateUniqueUsername(createUserInput.username, foundUser) + + const user = await this.prisma.user.upsert({ + where: { + email: createUserInput.email, + }, + update: { + updatedAt: new Date(), + }, + create: { + ...createUserInput, username: newUsername, }, }) - if (usernameTaken) throw createError({ statusCode: 400, message: 'Username already taken' }) - } - const updatedUser = await prisma.user.update({ - where: { id: user.id }, - data: updateUserInput, - }) - await removeCachedUserToken(authToken) - return formatUser(updatedUser) -} -export async function removeCachedUserToken(authToken: string): Promise { - await useStorage('redis').removeItem(`nitro:functions:getUserByAuthToken:authToken:${authToken}.json`) + return this.formatUser(user) + } + + /** + * Retrieves a user by email + */ + async getUserByEmail(email: string): Promise { + return this.prisma.user.findUnique({ + where: { email }, + }) + } + + /** + * Deletes a user by ID + */ + async deleteUser(userId: number): Promise { + await this.prisma.user.delete({ + where: { id: userId }, + }) + } + + /** + * Updates a user's information + */ + async updateUser(user: User, updateUserInput: UpdateUserInput, authToken: string): Promise { + const newUsername = updateUserInput.username + + if (newUsername && newUsername !== user.username) { + await this.validateUsername(newUsername) + } + + const updatedUser = await this.prisma.user.update({ + where: { id: user.id }, + data: updateUserInput, + }) + + await this.removeCachedUserToken(authToken) + return this.formatUser(updatedUser) + } + + /** + * Removes a cached user token + */ + private async removeCachedUserToken(authToken: string): Promise { + const cacheKey = this.generateCacheKey(authToken) + await this.storage.removeItem(cacheKey) + } + + /** + * Generates a unique username if the original is taken + */ + private generateUniqueUsername(username: string, existingUser: User | null): string { + if (!existingUser) return username + return `${username}_#${Math.floor(Math.random() * 1000)}` + } + + /** + * Validates if a username is available + */ + private async validateUsername(username: string): Promise { + const usernameTaken = await this.prisma.user.findFirst({ + where: { username }, + }) + + if (usernameTaken) { + throw createError({ + statusCode: 400, + message: 'Username already taken' + }) + } + } + + /** + * Generates a cache key for user tokens + */ + private generateCacheKey(authToken: string): string { + return `nitro:functions:getUserByAuthToken:authToken:${authToken}.json` + } + + /** + * Formats a user object for public consumption + */ + private formatUser(user: User): publicUser { + // Implement your user formatting logic here + return user as publicUser + } + } From dd3410de069ff7ff2f423d4a1326b8b55f7d58e2 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:21:39 +0100 Subject: [PATCH 16/24] fix prisma error --- apps/shelve/server/services/user.service.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/shelve/server/services/user.service.ts b/apps/shelve/server/services/user.service.ts index 8943adaf..2142a233 100644 --- a/apps/shelve/server/services/user.service.ts +++ b/apps/shelve/server/services/user.service.ts @@ -1,13 +1,10 @@ import type { publicUser, User, CreateUserInput, UpdateUserInput } from '@shelve/types' -import { PrismaClient } from '@prisma/client' export class UserService { - private readonly prisma: PrismaClient private readonly storage: Storage constructor() { - this.prisma = usePrisma() this.storage = useStorage('redis') } @@ -15,7 +12,7 @@ export class UserService { * Creates or updates a user */ async upsertUser(createUserInput: CreateUserInput): Promise { - const foundUser = await this.prisma.user.findUnique({ + const foundUser = await prisma.user.findUnique({ where: { username: createUserInput.username, }, @@ -23,7 +20,7 @@ export class UserService { const newUsername = this.generateUniqueUsername(createUserInput.username, foundUser) - const user = await this.prisma.user.upsert({ + const user = await prisma.user.upsert({ where: { email: createUserInput.email, }, @@ -42,8 +39,8 @@ export class UserService { /** * Retrieves a user by email */ - async getUserByEmail(email: string): Promise { - return this.prisma.user.findUnique({ + getUserByEmail(email: string): Promise { + return prisma.user.findUnique({ where: { email }, }) } @@ -52,7 +49,7 @@ export class UserService { * Deletes a user by ID */ async deleteUser(userId: number): Promise { - await this.prisma.user.delete({ + await prisma.user.delete({ where: { id: userId }, }) } @@ -67,7 +64,7 @@ export class UserService { await this.validateUsername(newUsername) } - const updatedUser = await this.prisma.user.update({ + const updatedUser = await prisma.user.update({ where: { id: user.id }, data: updateUserInput, }) @@ -96,7 +93,7 @@ export class UserService { * Validates if a username is available */ private async validateUsername(username: string): Promise { - const usernameTaken = await this.prisma.user.findFirst({ + const usernameTaken = await prisma.user.findFirst({ where: { username }, }) From 007f7c402f83b050b84dab437dd5ebfcfac74e11 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:29:22 +0100 Subject: [PATCH 17/24] Token service refacto --- .../shelve/server/api/auth/currentUser.get.ts | 5 +- apps/shelve/server/api/tokens/[id].delete.ts | 9 +- apps/shelve/server/api/tokens/[token].get.ts | 5 +- apps/shelve/server/api/tokens/index.get.ts | 5 +- apps/shelve/server/api/tokens/index.post.ts | 5 +- apps/shelve/server/middleware/1.serverAuth.ts | 5 +- .../shelve/server/middleware/2.serverAdmin.ts | 2 +- apps/shelve/server/services/token.service.ts | 202 +++++++++++------- apps/shelve/server/services/user.service.ts | 9 - 9 files changed, 149 insertions(+), 98 deletions(-) diff --git a/apps/shelve/server/api/auth/currentUser.get.ts b/apps/shelve/server/api/auth/currentUser.get.ts index c88dba91..2d464c00 100644 --- a/apps/shelve/server/api/auth/currentUser.get.ts +++ b/apps/shelve/server/api/auth/currentUser.get.ts @@ -1,8 +1,9 @@ import type { H3Event } from 'h3' -import { getUserByAuthToken } from '~~/server/services/token.service' +import { TokenService } from '~~/server/services/token.service' export default eventHandler(async (event: H3Event) => { + const tokenService = new TokenService() const authToken = getCookie(event, 'authToken') || '' - return await getUserByAuthToken(authToken) + return await tokenService.getUserByAuthToken(authToken) }) diff --git a/apps/shelve/server/api/tokens/[id].delete.ts b/apps/shelve/server/api/tokens/[id].delete.ts index 104a43d8..8135a2f4 100644 --- a/apps/shelve/server/api/tokens/[id].delete.ts +++ b/apps/shelve/server/api/tokens/[id].delete.ts @@ -1,15 +1,12 @@ import type { H3Event } from 'h3' +import { TokenService } from '~~/server/services/token.service' export default defineEventHandler(async (event: H3Event) => { + const tokenService = new TokenService() const { user } = event.context const id = getRouterParam(event, 'id') as string if (!id) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - await prisma.token.delete({ - where: { - id: +id, - userId: user.id, - }, - }) + await tokenService.deleteUserToken(+id, user.id) }) diff --git a/apps/shelve/server/api/tokens/[token].get.ts b/apps/shelve/server/api/tokens/[token].get.ts index ca9f58a1..6defda7c 100644 --- a/apps/shelve/server/api/tokens/[token].get.ts +++ b/apps/shelve/server/api/tokens/[token].get.ts @@ -1,9 +1,10 @@ import type { H3Event } from 'h3' -import { getUserByAuthToken } from '~~/server/services/token.service' +import { TokenService } from '~~/server/services/token.service' export default defineEventHandler((event: H3Event) => { + const tokenService = new TokenService() const token = getRouterParam(event, 'token') as string if (!token) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - return getUserByAuthToken(token) + return tokenService.getUserByAuthToken(token) }) diff --git a/apps/shelve/server/api/tokens/index.get.ts b/apps/shelve/server/api/tokens/index.get.ts index e40e9ca5..179190c6 100644 --- a/apps/shelve/server/api/tokens/index.get.ts +++ b/apps/shelve/server/api/tokens/index.get.ts @@ -1,8 +1,9 @@ import type { H3Event } from 'h3' -import { getTokensByUserId } from '~~/server/services/token.service' +import { TokenService } from '~~/server/services/token.service' export default defineEventHandler((event: H3Event) => { + const tokenService = new TokenService() const { user } = event.context - return getTokensByUserId(user.id) + return tokenService.getTokensByUserId(user.id) }) diff --git a/apps/shelve/server/api/tokens/index.post.ts b/apps/shelve/server/api/tokens/index.post.ts index 5d9a6505..bacf8220 100644 --- a/apps/shelve/server/api/tokens/index.post.ts +++ b/apps/shelve/server/api/tokens/index.post.ts @@ -1,9 +1,10 @@ import type { H3Event } from 'h3' -import { createToken } from '~~/server/services/token.service' +import { TokenService } from '~~/server/services/token.service' export default defineEventHandler(async (event: H3Event) => { + const tokenService = new TokenService() const { user } = event.context const body = await readBody(event) - return createToken({ name: body.name, userId: user.id }) + return tokenService.createToken({ name: body.name, userId: user.id }) }) diff --git a/apps/shelve/server/middleware/1.serverAuth.ts b/apps/shelve/server/middleware/1.serverAuth.ts index d6fc047e..aac58521 100644 --- a/apps/shelve/server/middleware/1.serverAuth.ts +++ b/apps/shelve/server/middleware/1.serverAuth.ts @@ -1,5 +1,5 @@ import type { H3Event } from 'h3' -import { getUserByAuthToken } from '~~/server/services/token.service' +import { TokenService } from '~~/server/services/token.service' export default defineEventHandler(async (event: H3Event) => { const protectedRoutes = [ @@ -16,11 +16,12 @@ export default defineEventHandler(async (event: H3Event) => { if (!protectedRoutes.some((route) => event.path?.startsWith(route))) { return } + const tokenService = new TokenService() const authToken = getCookie(event, 'authToken') if (authToken) { - const user = await getUserByAuthToken(authToken) + const user = await tokenService.getUserByAuthToken(authToken) if (!user) throw createError({ statusCode: 401, statusMessage: 'Invalid token' }) event.context.user = user return diff --git a/apps/shelve/server/middleware/2.serverAdmin.ts b/apps/shelve/server/middleware/2.serverAdmin.ts index aea9dd22..97487f77 100644 --- a/apps/shelve/server/middleware/2.serverAdmin.ts +++ b/apps/shelve/server/middleware/2.serverAdmin.ts @@ -1,5 +1,5 @@ -import { Role } from '@shelve/types' import type { H3Event } from 'h3' +import { Role } from '@shelve/types' export default defineEventHandler((event: H3Event) => { const protectedRoutes = ['/api/admin'] diff --git a/apps/shelve/server/services/token.service.ts b/apps/shelve/server/services/token.service.ts index eb854b4b..573e63f2 100644 --- a/apps/shelve/server/services/token.service.ts +++ b/apps/shelve/server/services/token.service.ts @@ -1,90 +1,148 @@ import { seal, unseal } from '@shelve/crypto' +import type { Token, User } from '@shelve/types' -const { encryptionKey } = useRuntimeConfig().private - -function updateUsedAt(tokenId: string) { - return prisma.token.update({ - where: { - id: tokenId, - }, - data: { - updatedAt: new Date(), - }, - }) -} +export class TokenService { -export async function getUserByAuthToken(authToken: string) { - const userId = +authToken.split('_')[1] + private readonly encryptionKey: string + private readonly TOKEN_PREFIX = 'she_' + private readonly TOKEN_LENGTH = 25 - const userTokens = await prisma.token.findMany({ - where: { - userId - }, - }) + constructor() { + this.encryptionKey = useRuntimeConfig().private.encryptionKey + } - let foundToken + /** + * Get user by authentication token + */ + async getUserByAuthToken(authToken: string): Promise { + const userId = this.extractUserId(authToken) + const userTokens = await this.getUserTokens(userId) + const foundToken = await this.findMatchingToken(userTokens, authToken) - for (const token of userTokens) { - token.token = await unseal(token.token, encryptionKey) - if (token.token === authToken) { - foundToken = token - break + if (!foundToken) { + throw createError({ + statusCode: 401, + message: 'Invalid token' + }) } + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + username: true, + email: true, + role: true, + }, + }) + + await this.updateUsedAt(foundToken.id) + return user } - if (!foundToken) throw new Error('Invalid token') - - const user = await prisma.user.findUnique({ - where: { - id: userId, - }, - select: { - id: true, - username: true, - email: true, - role: true, - }, - }) - - await updateUsedAt(foundToken.id) - return user -} + /** + * Get all tokens for a user + */ + async getTokensByUserId(userId: number): Promise { + const tokens = await prisma.token.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + }) -export async function getTokensByUserId(userId: number) { - const tokens = await prisma.token.findMany({ - where: { - userId, - }, - orderBy: { - createdAt: 'desc', - }, - }) - for (const token of tokens) { - token.token = await unseal(token.token, encryptionKey) + return Promise.all(tokens.map(async (token) => ({ + ...token, + token: await unseal(token.token, this.encryptionKey) + }))) } - return tokens -} -function generateUserToken(userId) { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - let token = '' + /** + * Create a new token for a user + */ + async createToken({ name, userId }: { name: string; userId: number }): Promise { + const token = this.generateUserToken(userId) + const encryptedToken = await seal(token, this.encryptionKey) - const userIdHash = Array.from(userId).reduce((acc, char) => acc + char.charCodeAt(0), 0) + await prisma.token.create({ + data: { + token: encryptedToken, + name, + userId + }, + }) + } - for (let i = 0; i < 25; i++) { - const randomIndex = (Math.floor(Math.random() * characters.length) + userIdHash) % characters.length - token += characters.charAt(randomIndex) + /** + * Update token's last used timestamp + */ + private async updateUsedAt(tokenId: string): Promise { + await prisma.token.update({ + where: { id: tokenId }, + data: { updatedAt: new Date() }, + }) } - return `she_${userId}_${token}` -} + /** + * Delete a token for a user + */ + private deleteUserToken(id: string, userId: number): Promise { + return prisma.token.delete({ + where: { + id, + userId, + }, + }) + } + + /** + * Generate a new token for a user + */ + private generateUserToken(userId: number): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let token = '' + const userIdHash = this.calculateUserIdHash(userId) + + for (let i = 0; i < this.TOKEN_LENGTH; i++) { + const randomIndex = (Math.floor(Math.random() * characters.length) + userIdHash) % characters.length + token += characters.charAt(randomIndex) + } + + return `${this.TOKEN_PREFIX}${userId}_${token}` + } + + /** + * Calculate hash from user ID + */ + private calculateUserIdHash(userId: number): number { + return Array.from(String(userId)).reduce((acc, char) => acc + char.charCodeAt(0), 0) + } + + /** + * Extract user ID from token + */ + private extractUserId(authToken: string): number { + return +authToken.split('_')[1] + } + + /** + * Get all tokens for a user + */ + private getUserTokens(userId: number): Promise { + return prisma.token.findMany({ + where: { userId } + }) + } + + /** + * Find matching token from list + */ + private async findMatchingToken(tokens: Token[], authToken: string): Promise { + for (const token of tokens) { + const decryptedToken = await unseal(token.token, this.encryptionKey) + if (decryptedToken === authToken) { + return token + } + } + return null + } -export async function createToken({ name, userId }: { name: string, userId: number }) { - await prisma.token.create({ - data: { - token: await seal(generateUserToken(userId), encryptionKey), - name, - userId - }, - }) } diff --git a/apps/shelve/server/services/user.service.ts b/apps/shelve/server/services/user.service.ts index 2142a233..e4a2db8c 100644 --- a/apps/shelve/server/services/user.service.ts +++ b/apps/shelve/server/services/user.service.ts @@ -36,15 +36,6 @@ export class UserService { return this.formatUser(user) } - /** - * Retrieves a user by email - */ - getUserByEmail(email: string): Promise { - return prisma.user.findUnique({ - where: { email }, - }) - } - /** * Deletes a user by ID */ From 10139290f812933fa5ed509503c63fbac971e195 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:33:32 +0100 Subject: [PATCH 18/24] Teammate service refacto --- .../server/api/user/teammate/index.get.ts | 5 ++- .../server/services/teammate.service.ts | 45 +++++++++++-------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/shelve/server/api/user/teammate/index.get.ts b/apps/shelve/server/api/user/teammate/index.get.ts index 7d0e4aaf..b6eb9a5a 100644 --- a/apps/shelve/server/api/user/teammate/index.get.ts +++ b/apps/shelve/server/api/user/teammate/index.get.ts @@ -1,7 +1,8 @@ import type { H3Event } from 'h3' -import { getTeammatesByUserId } from '~~/server/services/teammate.service' +import { TeammateService } from '~~/server/services/teammate.service' export default eventHandler((event: H3Event) => { + const teammateService = new TeammateService() const { user } = event.context - return getTeammatesByUserId(user.id) + return teammateService.getTeammatesByUserId(user.id) }) diff --git a/apps/shelve/server/services/teammate.service.ts b/apps/shelve/server/services/teammate.service.ts index d2caf03f..b31766eb 100644 --- a/apps/shelve/server/services/teammate.service.ts +++ b/apps/shelve/server/services/teammate.service.ts @@ -1,20 +1,29 @@ -export function getTeammatesByUserId(userId: number) { - return prisma.teammate.findMany({ - where: { - userId: userId, - }, - orderBy: { - updatedAt: 'desc', - }, - take: 4, - include: { - teammate: { - select: { - avatar: true, - email: true, - username: true, - }, +export class TeammateService { + + private readonly DEFAULT_LIMIT = 4 + + /** + * Get recent teammates for a user + */ + getTeammatesByUserId(userId: number, limit: number = this.DEFAULT_LIMIT) { + return prisma.teammate.findMany({ + where: { + userId: userId, + }, + orderBy: { + updatedAt: 'desc', + }, + take: limit, + include: { + teammate: { + select: { + avatar: true, + email: true, + username: true, + }, + } } - } - }) + }) + } + } From 06e924caa3143f440812b63f062e54c94b8b36ed Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:36:43 +0100 Subject: [PATCH 19/24] Resend service refacto --- apps/shelve/server/services/resend.service.ts | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/apps/shelve/server/services/resend.service.ts b/apps/shelve/server/services/resend.service.ts index b919a9c9..14ef2282 100644 --- a/apps/shelve/server/services/resend.service.ts +++ b/apps/shelve/server/services/resend.service.ts @@ -2,30 +2,50 @@ import { Resend } from 'resend' import { render } from '@vue-email/render' import verifyOtp from '~~/server/emails/verifyOtp.vue' -export async function sendOtp(email: string, otp: string) { - const runtimeConfig = useRuntimeConfig() - const resend = new Resend(runtimeConfig.private.resendApiKey) - const { siteUrl } = runtimeConfig.public - let template - try { - template = await render(verifyOtp, { - otp, - redirectUrl: `${siteUrl}/login?email=${email}&otp=${otp}`, - }) - } catch (error) { - template = `

OTP: ${otp}

` +export class EmailService { + + private readonly resend: Resend + private readonly siteUrl: string + private readonly SENDER = 'HugoRCD ' + + constructor() { + const config = useRuntimeConfig() + this.resend = new Resend(config.private.resendApiKey) + this.siteUrl = config.public.siteUrl + } + + /** + * Send OTP verification email + */ + async sendOtp(email: string, otp: string): Promise { + const template = await this.generateTemplate(email, otp) + + try { + await this.resend.emails.send({ + from: this.SENDER, + to: [email], + subject: 'Welcome to Shelve!', + html: template, + }).then((response) => { + console.log('Email sent: ', response) + }) + } catch (error) { + console.log('Error sending email: ', error) + } } - try { - await resend.emails.send({ - from: 'HugoRCD ', - to: [email], - subject: 'Welcome to Shelve!', - html: template, - }).then((response) => { - console.log('Email sent: ', response) - }) - } catch (error) { - console.log('Error sending email: ', error) + /** + * Generate email template + */ + private async generateTemplate(email: string, otp: string): Promise { + try { + return await render(verifyOtp, { + otp, + redirectUrl: `${this.siteUrl}/login?email=${email}&otp=${otp}`, + }) + } catch (error) { + return `

OTP: ${otp}

` + } } + } From 55e5d1c2ca50765b8661f3959df32f65f76808c1 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:39:14 +0100 Subject: [PATCH 20/24] Github service refacto --- apps/shelve/server/api/github/repos.get.ts | 5 +- apps/shelve/server/api/github/upload.post.ts | 5 +- apps/shelve/server/services/github.service.ts | 102 ++++++++++++------ 3 files changed, 74 insertions(+), 38 deletions(-) diff --git a/apps/shelve/server/api/github/repos.get.ts b/apps/shelve/server/api/github/repos.get.ts index b179ec43..069c13fb 100644 --- a/apps/shelve/server/api/github/repos.get.ts +++ b/apps/shelve/server/api/github/repos.get.ts @@ -1,6 +1,7 @@ import type { H3Event } from 'h3' -import { getUserRepos } from '~~/server/services/github.service' +import { GitHubService } from '~~/server/services/github.service' export default defineEventHandler(async (event: H3Event) => { - return await getUserRepos(event) + const githubService = new GitHubService() + return await githubService.getUserRepos(event) }) diff --git a/apps/shelve/server/api/github/upload.post.ts b/apps/shelve/server/api/github/upload.post.ts index 0f6438b2..42618299 100644 --- a/apps/shelve/server/api/github/upload.post.ts +++ b/apps/shelve/server/api/github/upload.post.ts @@ -1,5 +1,5 @@ import type { H3Event } from 'h3' -import { uploadFile } from '~~/server/services/github.service' +import { GitHubService } from '~~/server/services/github.service' export default defineEventHandler(async (event: H3Event) => { const formData = await readMultipartFormData(event) @@ -17,9 +17,10 @@ export default defineEventHandler(async (event: H3Event) => { statusMessage: 'No file provided' }) } + const gitHubService = new GitHubService() const file = new File([fileField.data], fileField.filename, { type: fileField.type }) const repoName = 'astra' - return await uploadFile(event, file, repoName) + return await gitHubService.uploadFile(event, file, repoName) }) diff --git a/apps/shelve/server/services/github.service.ts b/apps/shelve/server/services/github.service.ts index 5159a7bb..5c564e16 100644 --- a/apps/shelve/server/services/github.service.ts +++ b/apps/shelve/server/services/github.service.ts @@ -1,41 +1,75 @@ import { H3Event } from 'h3' -export async function getUserRepos(event: H3Event) { - const { user, secure } = await getUserSession(event) - const { githubToken } = secure - - const repos = await $fetch('https://api.github.com/user/repos?per_page=100', { - headers: { - Authorization: `token ${githubToken}`, - }, - }) - console.log(`Found ${repos.length} repositories for user ${user.username}`) - console.log(repos.map(repo => { - return { +type GitHubRepo = { + name: string + owner: string +} + +export class GitHubService { + + private readonly GITHUB_API = 'https://api.github.com' + private readonly REPOS_PER_PAGE = 100 + private readonly DEFAULT_BRANCH = 'main' + private readonly DEFAULT_COMMIT_MESSAGE = 'push from shelve' + + /** + * Get user's GitHub repositories + */ + async getUserRepos(event: H3Event): Promise { + const { user, secure } = await getUserSession(event) + const { githubToken } = secure + + const repos = await $fetch(`${this.GITHUB_API}/user/repos?per_page=${this.REPOS_PER_PAGE}`, { + headers: { + Authorization: `token ${githubToken}`, + }, + }) + + console.log(`Found ${repos.length} repositories for user ${user.username}`) + console.log(repos.map(repo => ({ name: repo.name, owner: repo.owner.login, - } - })) + }))) - return repos -} + return repos + } + + /** + * Upload file to GitHub repository + */ + async uploadFile(event: H3Event, file: File, repoName: string): Promise { + const { user, secure } = await getUserSession(event) + const { githubToken } = secure + + const content = await this.getFileContent(file) + const uploadUrl = this.buildUploadUrl(user.username, repoName, file.name) + + return await $fetch(uploadUrl, { + method: 'PUT', + headers: { + Authorization: `token ${githubToken}`, + }, + body: { + message: this.DEFAULT_COMMIT_MESSAGE, + content, + branch: this.DEFAULT_BRANCH + } + }) + } + + /** + * Convert file to base64 + */ + private async getFileContent(file: File): Promise { + const fileContent = await file.arrayBuffer() + return Buffer.from(fileContent).toString('base64') + } + + /** + * Build GitHub API upload URL + */ + private buildUploadUrl(username: string, repoName: string, fileName: string): string { + return `${this.GITHUB_API}/repos/${username}/${repoName}/contents/${fileName}` + } -export async function uploadFile(event: H3Event, file: File, repoName: string) { - const { user, secure } = await getUserSession(event) - const { githubToken } = secure - - const fileContent = await file.arrayBuffer() - const content = Buffer.from(fileContent).toString('base64') - - return await $fetch(`https://api.github.com/repos/${ user.username }/${ repoName }/contents/${ file.name }`, { - method: 'PUT', - headers: { - Authorization: `token ${ githubToken }`, - }, - body: { - message: 'push from shelve', - content: content, - branch: 'master' - } - }) } From bf15294d29b11a05af61e0844c63353bbf6953bf Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:45:54 +0100 Subject: [PATCH 21/24] Team service refacto --- .../server/api/teams/[teamId]/index.delete.ts | 5 +- .../server/api/teams/[teamId]/index.put.ts | 16 - .../api/teams/[teamId]/members/[id].delete.ts | 5 +- .../api/teams/[teamId]/members/index.post.ts | 5 +- apps/shelve/server/api/teams/index.get.ts | 5 +- apps/shelve/server/api/teams/index.post.ts | 5 +- apps/shelve/server/services/teams.service.ts | 448 +++++++++--------- packages/types/src/Team.ts | 17 +- 8 files changed, 253 insertions(+), 253 deletions(-) delete mode 100644 apps/shelve/server/api/teams/[teamId]/index.put.ts diff --git a/apps/shelve/server/api/teams/[teamId]/index.delete.ts b/apps/shelve/server/api/teams/[teamId]/index.delete.ts index dfa09f14..c9ad3721 100644 --- a/apps/shelve/server/api/teams/[teamId]/index.delete.ts +++ b/apps/shelve/server/api/teams/[teamId]/index.delete.ts @@ -1,11 +1,12 @@ import type { H3Event } from 'h3' -import { deleteTeam } from '~~/server/services/teams.service' +import { TeamService } from '~~/server/services/teams.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context const id = getRouterParam(event, 'teamId') as string if (!id) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - await deleteTeam({ + const teamService = new TeamService() + await teamService.deleteTeam({ teamId: parseInt(id), userId: user.id, userRole: user.role, diff --git a/apps/shelve/server/api/teams/[teamId]/index.put.ts b/apps/shelve/server/api/teams/[teamId]/index.put.ts deleted file mode 100644 index a15598dd..00000000 --- a/apps/shelve/server/api/teams/[teamId]/index.put.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -import { updateTeam } from "~/server/app/teamsService"; -import { H3Event } from "h3"; - -export default eventHandler(async (event: H3Event) => { - const user = event.context.user; - const id = getRouterParam(event, "teamId") as string; - if (!id) throw createError({ statusCode: 400, statusMessage: "Missing params" }); - const teamUpdateInput = await readBody(event); - await updateTeam(teamUpdateInput, user.id); - return { - statusCode: 200, - message: "Team updated", - }; -}); -*/ diff --git a/apps/shelve/server/api/teams/[teamId]/members/[id].delete.ts b/apps/shelve/server/api/teams/[teamId]/members/[id].delete.ts index 0569b78c..0a59a0f3 100644 --- a/apps/shelve/server/api/teams/[teamId]/members/[id].delete.ts +++ b/apps/shelve/server/api/teams/[teamId]/members/[id].delete.ts @@ -1,12 +1,13 @@ import type { H3Event } from 'h3' -import { removeMember } from '~~/server/services/teams.service' +import { TeamService } from '~~/server/services/teams.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context const teamId = getRouterParam(event, 'teamId') as string const memberId = getRouterParam(event, 'id') as string if (!teamId || !memberId) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - await removeMember(parseInt(teamId), parseInt(memberId), user.id) + const teamService = new TeamService() + await teamService.removeMember(+teamId, +memberId, user.id) return { statusCode: 200, message: 'Member removed', diff --git a/apps/shelve/server/api/teams/[teamId]/members/index.post.ts b/apps/shelve/server/api/teams/[teamId]/members/index.post.ts index 7d34f74e..a5364384 100644 --- a/apps/shelve/server/api/teams/[teamId]/members/index.post.ts +++ b/apps/shelve/server/api/teams/[teamId]/members/index.post.ts @@ -1,10 +1,11 @@ import type { H3Event } from 'h3' -import { upsertMember } from '~~/server/services/teams.service' +import { TeamService } from '~~/server/services/teams.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context const teamId = getRouterParam(event, 'teamId') as string if (!teamId) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) + const teamService = new TeamService() const addMemberInput = await readBody(event) - return await upsertMember(parseInt(teamId), addMemberInput, user.id) + return await teamService.upsertMember(+teamId, addMemberInput, user.id) }) diff --git a/apps/shelve/server/api/teams/index.get.ts b/apps/shelve/server/api/teams/index.get.ts index 2a465bb8..56b189ed 100644 --- a/apps/shelve/server/api/teams/index.get.ts +++ b/apps/shelve/server/api/teams/index.get.ts @@ -1,7 +1,8 @@ import type { H3Event } from 'h3' -import { getTeamByUserId } from '~~/server/services/teams.service' +import { TeamService } from '~~/server/services/teams.service' export default eventHandler((event: H3Event) => { + const teamService = new TeamService() const { user } = event.context - return getTeamByUserId(user.id) + return teamService.getTeamByUserId(user.id) }) diff --git a/apps/shelve/server/api/teams/index.post.ts b/apps/shelve/server/api/teams/index.post.ts index 26bfbb59..e0ab8d80 100644 --- a/apps/shelve/server/api/teams/index.post.ts +++ b/apps/shelve/server/api/teams/index.post.ts @@ -1,11 +1,12 @@ import type { H3Event } from 'h3' import type { CreateTeamInput } from '@shelve/types' -import { createTeam } from '~~/server/services/teams.service' +import { TeamService } from '~~/server/services/teams.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context const createTeamInput = await readBody(event) as CreateTeamInput if (!createTeamInput.name) throw createError({ statusCode: 400, statusMessage: 'Cannot create team without name' }) + const teamService = new TeamService() createTeamInput.name = createTeamInput.name.trim() - return await createTeam(createTeamInput, user.id) + return await teamService.createTeam(createTeamInput, user.id) }) diff --git a/apps/shelve/server/services/teams.service.ts b/apps/shelve/server/services/teams.service.ts index c72bff8a..b88b7f06 100644 --- a/apps/shelve/server/services/teams.service.ts +++ b/apps/shelve/server/services/teams.service.ts @@ -1,253 +1,263 @@ -import { type CreateTeamInput, Role, TeamRole } from '@shelve/types' +import type { CreateTeamInput, Team, Member } from '@shelve/types' +import { Role, TeamRole } from '@shelve/types' import { deleteCachedUserProjects } from '~~/server/services/project.service' -export async function createTeam(createTeamInput: CreateTeamInput, userId: number) { - await deleteCachedTeamByUserId(userId) - return prisma.team.create({ - data: { - name: createTeamInput.name, - members: { - create: { - role: TeamRole.OWNER, - user: { - connect: { - id: userId, +export class TeamService { + + private readonly storage: Storage + private readonly CACHE_TTL = 60 * 60 // 1 hour + private readonly CACHE_PREFIX = 'nitro:functions:getTeamByUserId:userId:' + + constructor() { + this.storage = useStorage('cache') + } + + /** + * Create a new team + */ + async createTeam(createTeamInput: CreateTeamInput, userId: number): Promise { + await this.deleteCachedTeamByUserId(userId) + return prisma.team.create({ + data: { + name: createTeamInput.name, + members: { + create: { + role: TeamRole.OWNER, + user: { + connect: { id: userId }, }, - }, - } - }, - }, - include: { - members: { - include: { - user: { - select: { - id: true, - username: true, - email: true, - avatar: true, - } } - } - } - } - }) -} + }, + }, + include: this.getTeamInclude() + }) + } -export async function upsertTeammate(userId: number, teammateId: number, isUpdated: boolean) { - const updateOrCreateTeammate = (userId: number, teammateId: number) => { - return prisma.teammate.upsert({ + /** + * Upsert team member + */ + async upsertMember(teamId: number, addMemberInput: { email: string; role: TeamRole }, requesterId: number): Promise { + const team = await this.validateTeamAccess(teamId, requesterId) + const user = await this.findUserByEmail(addMemberInput.email) + + await this.deleteCachedTeamByUserId(requesterId) + await deleteCachedUserProjects(requesterId) + + const member = await prisma.member.upsert({ where: { - userId_teammateId: { - userId, - teammateId, - }, + id: team.members.find((member) => member.userId === user.id)?.id || -1, }, update: { - updatedAt: new Date(), - count: { - increment: isUpdated ? 0 : 1, - }, + role: addMemberInput.role, }, create: { - userId, - teammateId, + role: addMemberInput.role, + teamId, + userId: user.id, }, + include: { + user: { + select: { + id: true, + username: true, + email: true, + avatar: true, + } + } + } }) + + const isUpdated = new Date(member.createdAt).getTime() !== new Date(member.updatedAt).getTime() + await this.upsertTeammate(requesterId, user.id, isUpdated) + return member } - await Promise.all([ - updateOrCreateTeammate(userId, teammateId), - updateOrCreateTeammate(teammateId, userId), - ]) -} + /** + * Remove team member + */ + async removeMember(teamId: number, memberId: number, requesterId: number): Promise { + await this.validateTeamAccess(teamId, requesterId) + await this.deleteCachedTeamByUserId(requesterId) + await deleteCachedUserProjects(requesterId) -export async function upsertMember(teamId: number, addMemberInput: { - email: string; - role: TeamRole -}, requesterId: number) { - const team = await prisma.team.findFirst({ - where: { - id: teamId, - members: { - some: { - userId: requesterId, - role: { - in: [TeamRole.ADMIN, TeamRole.OWNER], + const member = await this.getMemberById(memberId) + await this.deleteTeammate(member.userId, requesterId) + await this.deleteTeammate(requesterId, member.userId) + + return prisma.member.delete({ + where: { id: memberId }, + }) + } + + /** + * Delete team + */ + async deleteTeam(deleteTeamInput: DeleteTeamInput): Promise { + const { teamId, userId, userRole } = deleteTeamInput + const team = await this.validateTeamOwnership(teamId, userId) + + if (userRole !== Role.ADMIN && !team) { + throw createError({ statusCode: 401, statusMessage: 'unauthorized' }) + } + + await this.deleteCachedTeamByUserId(userId) + await this.cleanupTeammates(teamId, userId) + + return prisma.team.delete({ + where: { id: teamId }, + }) + } + + /** + * Get teams by user ID + */ + getTeamByUserId(userId: number): Promise { + return cachedFunction(() => { + return prisma.team.findMany({ + where: { + members: { + some: { userId } } }, - }, - }, - include: { - members: true, - } - }) - if (!team) throw createError({ statusCode: 401, statusMessage: 'unauthorized' }) - const user = await prisma.user.findFirst({ - where: { - email: addMemberInput.email.trim(), - }, - }) - if (!user) throw createError({ statusCode: 400, statusMessage: 'user not found' }) - await deleteCachedTeamByUserId(requesterId) - await deleteCachedUserProjects(requesterId) - const member = await prisma.member.upsert({ - where: { - id: team.members.find((member) => member.userId === user.id)?.id || -1, - }, - update: { - role: addMemberInput.role, - }, - create: { - role: addMemberInput.role, - teamId, - userId: user.id, - }, - include: { - user: { - select: { - id: true, - username: true, - email: true, - avatar: true, - } - } + include: this.getTeamInclude() + }) + }, { + maxAge: this.CACHE_TTL, + name: 'getTeamByUserId', + getKey: (userId: number) => `userId:${userId}`, + })(userId) + } + + /** + * Private helper methods + */ + private async upsertTeammate(userId: number, teammateId: number, isUpdated: boolean): Promise { + const updateOrCreateTeammate = (userId: number, teammateId: number) => { + return prisma.teammate.upsert({ + where: { + userId_teammateId: { userId, teammateId }, + }, + update: { + updatedAt: new Date(), + count: { + increment: isUpdated ? 0 : 1, + }, + }, + create: { + userId, + teammateId, + }, + }) } - }) - const isUpdated: boolean = new Date(member.createdAt).getTime() !== new Date(member.updatedAt).getTime() - await upsertTeammate(requesterId, user.id, isUpdated) - return member -} -async function isTeamMate(userId: number, requesterId: number): Promise { - const foundedTeamMate = await prisma.teammate.findFirst({ - where: { - userId: { - equals: userId, - }, - teammateId: { - equals: requesterId, - }, - }, - }) - return !!foundedTeamMate -} + await Promise.all([ + updateOrCreateTeammate(userId, teammateId), + updateOrCreateTeammate(teammateId, userId), + ]) + } -export async function deleteTeammate(userId: number, requesterId: number) { - const isTeammate = await isTeamMate(userId, requesterId) - if (!isTeammate) return - const updatedUser = await prisma.teammate.update({ - where: { - userId_teammateId: { - userId: requesterId, - teammateId: userId, - }, - }, - data: { - count: { - decrement: 1, - }, - }, - select: { - count: true, - }, - }) - if (updatedUser.count === 0) { - await prisma.teammate.delete({ + private async deleteTeammate(userId: number, requesterId: number): Promise { + if (!await this.isTeamMate(userId, requesterId)) return + + const updatedUser = await prisma.teammate.update({ where: { userId_teammateId: { userId: requesterId, teammateId: userId, }, }, + data: { + count: { decrement: 1 }, + }, + select: { count: true }, }) + + if (updatedUser.count === 0) { + await prisma.teammate.delete({ + where: { + userId_teammateId: { + userId: requesterId, + teammateId: userId, + }, + }, + }) + } } -} -export async function removeMember(teamId: number, memberId: number, requesterId: number) { - const team = await prisma.team.findFirst({ - where: { - id: teamId, - members: { - some: { - userId: requesterId, - role: { - in: [TeamRole.ADMIN, TeamRole.OWNER], + private async isTeamMate(userId: number, requesterId: number): Promise { + const foundedTeamMate = await prisma.teammate.findFirst({ + where: { + userId: { equals: userId }, + teammateId: { equals: requesterId }, + }, + }) + return !!foundedTeamMate + } + + private async validateTeamAccess(teamId: number, requesterId: number): Promise { + const team = await prisma.team.findFirst({ + where: { + id: teamId, + members: { + some: { + userId: requesterId, + role: { in: [TeamRole.ADMIN, TeamRole.OWNER] }, } }, }, - }, - }) - if (!team) throw createError({ statusCode: 401, statusMessage: 'unauthorized' }) - - await deleteCachedTeamByUserId(requesterId) - await deleteCachedUserProjects(requesterId) - - const member = await prisma.member.findFirst({ - where: { - id: memberId, - }, - select: { - userId: true, - }, - }) - await deleteTeammate(member.userId, requesterId) - await deleteTeammate(requesterId, member.userId) - return prisma.member.delete({ - where: { - id: memberId, - }, - }) -} - -type DeleteTeamInput = { - teamId: number - userId: number - userRole: Role -} + include: { members: true }, + }) + if (!team) throw createError({ statusCode: 401, statusMessage: 'unauthorized' }) + return team + } -export async function deleteTeam(deleteTeamInput: DeleteTeamInput) { - const { teamId, userId, userRole } = deleteTeamInput - const team = await prisma.team.findFirst({ - where: { - id: teamId, - members: { - some: { - userId, - role: TeamRole.OWNER, + private validateTeamOwnership(teamId: number, userId: number): Promise { + return prisma.team.findFirst({ + where: { + id: teamId, + members: { + some: { + userId, + role: TeamRole.OWNER, + } }, }, - }, - }) - if (userRole !== Role.ADMIN && !team) throw createError({ statusCode: 401, statusMessage: 'unauthorized' }) - await deleteCachedTeamByUserId(userId) - const allMembers = await prisma.member.findMany({ - where: { - teamId, - }, - }) - for (const member of allMembers) { - if (member.userId === userId) continue - await deleteTeammate(member.userId, userId) - await deleteTeammate(userId, member.userId) + }) } - return prisma.team.delete({ - where: { - id: teamId, - }, - }) -} -export const getTeamByUserId = cachedFunction((userId: number) => { - return prisma.team.findMany({ - where: { - members: { - some: { - userId: userId, - }, - } - }, - include: { + private async findUserByEmail(email: string) { + const user = await prisma.user.findFirst({ + where: { email: email.trim() }, + }) + if (!user) throw createError({ statusCode: 400, statusMessage: 'user not found' }) + return user + } + + private getMemberById(memberId: number) { + return prisma.member.findFirst({ + where: { id: memberId }, + select: { userId: true }, + }) + } + + private async cleanupTeammates(teamId: number, userId: number): Promise { + const allMembers = await prisma.member.findMany({ + where: { teamId }, + }) + + for (const member of allMembers) { + if (member.userId === userId) continue + await this.deleteTeammate(member.userId, userId) + await this.deleteTeammate(userId, member.userId) + } + } + + private deleteCachedTeamByUserId(userId: number): Promise { + return this.storage.removeItem(`${this.CACHE_PREFIX}${userId}.json`) + } + + private getTeamInclude() { + return { members: { include: { user: { @@ -261,13 +271,7 @@ export const getTeamByUserId = cachedFunction((userId: number) => { } } } - }) -}, { - maxAge: 60 * 60, - name: 'getTeamByUserId', - getKey: (userId: number) => `userId:${userId}`, -}) - -export function deleteCachedTeamByUserId(userId: number) { - return useStorage('cache').removeItem(`nitro:functions:getTeamByUserId:userId:${userId}.json`) + } + } + diff --git a/packages/types/src/Team.ts b/packages/types/src/Team.ts index fa761edc..b94c805b 100644 --- a/packages/types/src/Team.ts +++ b/packages/types/src/Team.ts @@ -1,10 +1,11 @@ -import type { Project } from "./Project"; -import type { User } from "./User"; +import { Role } from '../dist/index' +import type { Project } from './Project' +import type { User } from './User' export enum TeamRole { - OWNER = "owner", - ADMIN = "admin", - DEVELOPER = "developer", + OWNER = 'owner', + ADMIN = 'admin', + DEVELOPER = 'developer', } export type Member = { @@ -37,3 +38,9 @@ export type UpdateTeamInput = { members: Member[]; projects: Project[]; } + +type DeleteTeamInput = { + teamId: number + userId: number + userRole: Role +} From 228659a4e8deb19ea2c797eeb54bf976a6795ad9 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:53:04 +0100 Subject: [PATCH 22/24] Project service refacto --- apps/shelve/.config/shelve.config.json | 2 +- .../server/api/project/[id]/index.delete.ts | 5 +- .../server/api/project/[id]/index.get.ts | 5 +- .../server/api/project/[id]/index.put.ts | 5 +- .../api/project/[id]/team/[teamId].delete.ts | 5 +- .../api/project/[id]/team/[teamId].post.ts | 5 +- apps/shelve/server/api/project/index.get.ts | 5 +- apps/shelve/server/api/project/index.post.ts | 5 +- .../shelve/server/services/project.service.ts | 332 +++++++++++------- apps/shelve/server/services/teams.service.ts | 8 +- 10 files changed, 229 insertions(+), 148 deletions(-) diff --git a/apps/shelve/.config/shelve.config.json b/apps/shelve/.config/shelve.config.json index b17fa141..658aa118 100644 --- a/apps/shelve/.config/shelve.config.json +++ b/apps/shelve/.config/shelve.config.json @@ -1,4 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/HugoRCD/shelve/main/packages/types/shelveConfigSchema.json", "project": "shelve" -} \ No newline at end of file +} diff --git a/apps/shelve/server/api/project/[id]/index.delete.ts b/apps/shelve/server/api/project/[id]/index.delete.ts index 63b65e6f..3eb75b70 100644 --- a/apps/shelve/server/api/project/[id]/index.delete.ts +++ b/apps/shelve/server/api/project/[id]/index.delete.ts @@ -1,11 +1,12 @@ import type { H3Event } from 'h3' -import { deleteProject } from '~~/server/services/project.service' +import { ProjectService } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context const id = getRouterParam(event, 'id') as string if (!id) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - await deleteProject(id, user.id) + const projectService = new ProjectService() + await projectService.deleteProject(id, user.id) return { statusCode: 200, message: 'Project deleted', diff --git a/apps/shelve/server/api/project/[id]/index.get.ts b/apps/shelve/server/api/project/[id]/index.get.ts index 5e7df19b..e805c865 100644 --- a/apps/shelve/server/api/project/[id]/index.get.ts +++ b/apps/shelve/server/api/project/[id]/index.get.ts @@ -1,8 +1,9 @@ import type { H3Event } from 'h3' -import { getProjectById } from '~~/server/services/project.service' +import { ProjectService } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string if (!id) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - return await getProjectById(parseInt(id)) + const projectService = new ProjectService() + return await projectService.getProjectById(+id) }) diff --git a/apps/shelve/server/api/project/[id]/index.put.ts b/apps/shelve/server/api/project/[id]/index.put.ts index b1b250e8..38ac7662 100644 --- a/apps/shelve/server/api/project/[id]/index.put.ts +++ b/apps/shelve/server/api/project/[id]/index.put.ts @@ -1,13 +1,14 @@ import type { H3Event } from 'h3' -import { updateProject } from '~~/server/services/project.service' +import { ProjectService } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { const { user } = event.context const id = getRouterParam(event, 'id') as string if (!id) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) + const projectService = new ProjectService() const projectUpdateInput = await readBody(event) delete projectUpdateInput.variables delete projectUpdateInput.team projectUpdateInput.name = projectUpdateInput.name.trim() - return await updateProject(projectUpdateInput, parseInt(id), user.id) + return await projectService.updateProject(projectUpdateInput, +id, user.id) }) diff --git a/apps/shelve/server/api/project/[id]/team/[teamId].delete.ts b/apps/shelve/server/api/project/[id]/team/[teamId].delete.ts index 03170d25..97850655 100644 --- a/apps/shelve/server/api/project/[id]/team/[teamId].delete.ts +++ b/apps/shelve/server/api/project/[id]/team/[teamId].delete.ts @@ -1,11 +1,12 @@ import type { H3Event } from 'h3' -import { removeTeamFromProject } from '~~/server/services/project.service' +import { ProjectService } from '~~/server/services/project.service' export default defineEventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string const teamId = getRouterParam(event, 'teamId') as string if (!id || !teamId) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - await removeTeamFromProject(+id, +teamId) + const projectService = new ProjectService() + await projectService.removeTeamFromProject(+id, +teamId) return { statusCode: 200, message: 'Team removed from project', diff --git a/apps/shelve/server/api/project/[id]/team/[teamId].post.ts b/apps/shelve/server/api/project/[id]/team/[teamId].post.ts index 3699f3cf..79ea4bc5 100644 --- a/apps/shelve/server/api/project/[id]/team/[teamId].post.ts +++ b/apps/shelve/server/api/project/[id]/team/[teamId].post.ts @@ -1,11 +1,12 @@ import type { H3Event } from 'h3' -import { addTeamToProject } from '~~/server/services/project.service' +import { ProjectService } from '~~/server/services/project.service' export default defineEventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string const teamId = getRouterParam(event, 'teamId') as string if (!id || !teamId) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - await addTeamToProject(+id, +teamId) + const projectService = new ProjectService() + await projectService.addTeamToProject(+id, +teamId) return { statusCode: 200, message: 'Team added to project', diff --git a/apps/shelve/server/api/project/index.get.ts b/apps/shelve/server/api/project/index.get.ts index 85e78be1..b0d19a8b 100644 --- a/apps/shelve/server/api/project/index.get.ts +++ b/apps/shelve/server/api/project/index.get.ts @@ -1,8 +1,9 @@ import type { H3Event } from 'h3' -import { getProjectsByUserId } from '~~/server/services/project.service' +import { ProjectService } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { + const projectService = new ProjectService() const { user } = event.context if (!user) throw createError({ statusCode: 401, message: 'Unauthorized' }) - return await getProjectsByUserId(user.id) + return await projectService.getProjectsByUserId(user.id) }) diff --git a/apps/shelve/server/api/project/index.post.ts b/apps/shelve/server/api/project/index.post.ts index 1762cc9e..0649cf00 100644 --- a/apps/shelve/server/api/project/index.post.ts +++ b/apps/shelve/server/api/project/index.post.ts @@ -1,10 +1,11 @@ import type { H3Event } from 'h3' -import { createProject } from '~~/server/services/project.service' +import { ProjectService } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { + const projectService = new ProjectService() const { user } = event.context const projectCreateInput = await readBody(event) delete projectCreateInput.variables projectCreateInput.name = projectCreateInput.name.trim() - return await createProject(projectCreateInput, user.id) + return await projectService.createProject(projectCreateInput, user.id) }) diff --git a/apps/shelve/server/services/project.service.ts b/apps/shelve/server/services/project.service.ts index 47195507..805119ff 100644 --- a/apps/shelve/server/services/project.service.ts +++ b/apps/shelve/server/services/project.service.ts @@ -1,65 +1,210 @@ import type { CreateProjectInput, ProjectUpdateInput, Team } from '@shelve/types' -type CreateProjectInputWithAll = CreateProjectInput & { ownerId: number, team?: { connect: { id: number } } } +export class ProjectService { -export async function createProject(project: CreateProjectInput, userId: number) { - await deleteCachedUserProjects(userId) - const projectAlreadyExists = await isProjectAlreadyExists(project.name, userId) - if (projectAlreadyExists) throw new Error('Project already exists') + private readonly storage: Storage + private readonly CACHE_TTL = 60 * 60 // 1 hour + private readonly CACHE_PREFIX = { + projects: 'nitro:functions:getProjectsByUserId:userId:', + project: 'nitro:functions:getProjectById:projectId:' + } + + constructor() { + this.storage = useStorage('cache') + } - const projectData = { - ...project, - ownerId: userId, - users: { connect: { id: userId } } - } as CreateProjectInputWithAll + /** + * Create a new project + */ + async createProject(project: CreateProjectInput, userId: number): Promise { + await this.deleteCachedUserProjects(userId) + await this.validateProjectName(project.name, userId) - if (project.team) { - projectData.team = { connect: { id: project.team.id } } as Team & { connect: { id: number } } + const projectData = this.buildProjectData(project, userId) + return prisma.project.create({ data: projectData }) } - return prisma.project.create({ data: projectData }) -} + /** + * Update existing project + */ + async updateProject(project: ProjectUpdateInput, projectId: number, userId: number): Promise { + await this.deleteCachedUserProjects(userId) + await this.deleteCachedProjectById(projectId) + + const existingProject = await this.findProjectById(projectId) + + if (existingProject.name !== project.name) { + await this.validateProjectName(project.name, userId) + } + + return prisma.project.update({ + where: { id: projectId }, + data: project, + }) + } + + /** + * Get project by ID + */ + async getProjectById(id: number): Promise { + return cachedFunction(() => { + return prisma.project.findUnique({ + where: { id }, + include: this.getProjectInclude() + }) + }, { + maxAge: this.CACHE_TTL, + name: 'getProjectById', + getKey: (id: number) => `projectId:${id}`, + })(id) + } -async function isProjectAlreadyExists(name: string, userId: number): Promise { - const project = await prisma.project.findFirst({ - where: { - name: { - equals: name, - mode: 'insensitive', + /** + * Get all projects for a user + */ + async getProjectsByUserId(userId: number): Promise { + return cachedFunction(async () => { + const [projects, teams] = await Promise.all([ + this.getUserProjects(userId), + this.getUserTeamProjects(userId) + ]) + + const teamProjects = teams.map(team => team.projects) + return this.removeDuplicateProjects([...projects, ...teamProjects].flat()) + }, { + maxAge: this.CACHE_TTL, + name: 'getProjectsByUserId', + getKey: (userId: number) => `userId:${userId}`, + })(userId) + } + + /** + * Add team to project + */ + async addTeamToProject(projectId: number, teamId: number): Promise { + await this.deleteCachedProjectById(projectId) + return prisma.project.update({ + where: { id: projectId }, + data: { + team: { + connect: { id: teamId } + } + } + }) + } + + /** + * Remove team from project + */ + async removeTeamFromProject(projectId: number, teamId: number): Promise { + await this.deleteCachedProjectById(projectId) + return prisma.project.update({ + where: { id: projectId }, + data: { + team: { + disconnect: { id: teamId } + } + } + }) + } + + /** + * Delete project + */ + async deleteProject(id: string, userId: number): Promise { + await this.deleteCachedUserProjects(userId) + await this.deleteCachedProjectById(parseInt(id)) + + return prisma.project.delete({ + where: { + id: parseInt(id), + ownerId: userId, + } + }) + } + + /** + * Private helper methods + */ + private async validateProjectName(name: string, userId: number): Promise { + const exists = await this.isProjectAlreadyExists(name, userId) + if (exists) { + throw createError({ + statusCode: 400, + message: 'Project already exists' + }) + } + } + + private async isProjectAlreadyExists(name: string, userId: number): Promise { + const project = await prisma.project.findFirst({ + where: { + name: { + equals: name, + mode: 'insensitive', + }, + ownerId: userId, }, + }) + return !!project + } + + private async findProjectById(id: number): Promise { + const project = await prisma.project.findFirst({ + where: { id }, + }) + if (!project) { + throw createError({ + statusCode: 404, + message: 'Project not found' + }) + } + return project + } + + private buildProjectData(project: CreateProjectInput, userId: number): CreateProjectInputWithAll { + const projectData = { + ...project, ownerId: userId, - }, - }) - return !!project -} + users: { connect: { id: userId } } + } as CreateProjectInputWithAll -export async function updateProject(project: ProjectUpdateInput, projectId: number, userId: number) { - await deleteCachedUserProjects(userId) - await deleteCachedProjectById(projectId) - const findProject = await prisma.project.findFirst({ - where: { - id: projectId, - }, - }) - if (!findProject) throw new Error('Project not found') - if (findProject.name !== project.name) { - const projectAlreadyExists = await isProjectAlreadyExists(project.name, userId) - if (projectAlreadyExists) throw new Error('Project already exists') - } - return prisma.project.update({ - where: { - id: projectId, - }, - data: project, - }) -} + if (project.team) { + projectData.team = { + connect: { id: project.team.id } + } as Team & { connect: { id: number } } + } -export const getProjectById = cachedFunction((id: number) => { - return prisma.project.findUnique({ - where: { - id, - }, - include: { + return projectData + } + + private async getUserProjects(userId: number) { + return prisma.project.findMany({ + where: { ownerId: userId }, + }) + } + + private async getUserTeamProjects(userId: number) { + return prisma.team.findMany({ + where: { + members: { + some: { userId } + } + }, + include: { + projects: true, + } + }) + } + + private removeDuplicateProjects(projects: Project[]): Project[] { + return projects.filter((project, index, self) => + self.findIndex(p => p.id === project.id) === index + ) + } + + private getProjectInclude() { + return { team: { include: { members: { @@ -77,87 +222,14 @@ export const getProjectById = cachedFunction((id: number) => { } } } - }) -}, { - maxAge: 60 * 60, - name: 'getProjectById', - getKey: (id: number) => `projectId:${id}`, -}) - -export async function addTeamToProject(projectId: number, teamId: number) { - await deleteCachedProjectById(projectId) - return prisma.project.update({ - where: { - id: projectId, - }, - data: { - team: { - connect: { - id: teamId, - } - } - } - }) -} + } -export async function removeTeamFromProject(projectId: number, teamId: number) { - await deleteCachedProjectById(projectId) - return prisma.project.update({ - where: { - id: projectId, - }, - data: { - team: { - disconnect: { - id: teamId, - } - } - } - }) -} + private async deleteCachedUserProjects(userId: number): Promise { + await this.storage.removeItem(`${this.CACHE_PREFIX.projects}${userId}.json`) + } -export const getProjectsByUserId = cachedFunction(async (userId: number) => { - const [projects, teams] = await Promise.all([ - prisma.project.findMany({ - where: { - ownerId: userId, - }, - }), - prisma.team.findMany({ - where: { - members: { - some: { - userId, - } - } - }, - include: { - projects: true, - } - }) - ]) - const teamProjects = teams.map(team => team.projects) - return [...projects, ...teamProjects].flat().filter((project, index, self) => self.findIndex(p => p.id === project.id) === index) -}, { - maxAge: 60 * 60, - name: 'getProjectsByUserId', - getKey: (userId: number) => `userId:${userId}`, -}) - -export async function deleteProject(id: string, userId: number) { - await deleteCachedUserProjects(userId) - await deleteCachedProjectById(id) - return prisma.project.delete({ - where: { - id: parseInt(id), - ownerId: userId, - } - }) -} + private async deleteCachedProjectById(id: number): Promise { + await this.storage.removeItem(`${this.CACHE_PREFIX.project}${id}.json`) + } -export async function deleteCachedUserProjects(userId: number) { - return await useStorage('cache').removeItem(`nitro:functions:getProjectsByUserId:userId:${userId}.json`) -} -export async function deleteCachedProjectById(id: number) { - return await useStorage('cache').removeItem(`nitro:functions:getProjectById:projectId:${id}.json`) } diff --git a/apps/shelve/server/services/teams.service.ts b/apps/shelve/server/services/teams.service.ts index b88b7f06..d4860b66 100644 --- a/apps/shelve/server/services/teams.service.ts +++ b/apps/shelve/server/services/teams.service.ts @@ -1,6 +1,6 @@ import type { CreateTeamInput, Team, Member } from '@shelve/types' import { Role, TeamRole } from '@shelve/types' -import { deleteCachedUserProjects } from '~~/server/services/project.service' +import { ProjectService } from '~~/server/services/project.service' export class TeamService { @@ -37,11 +37,12 @@ export class TeamService { * Upsert team member */ async upsertMember(teamId: number, addMemberInput: { email: string; role: TeamRole }, requesterId: number): Promise { + const projectService = new ProjectService() const team = await this.validateTeamAccess(teamId, requesterId) const user = await this.findUserByEmail(addMemberInput.email) await this.deleteCachedTeamByUserId(requesterId) - await deleteCachedUserProjects(requesterId) + await projectService.deleteCachedUserProjects(requesterId) const member = await prisma.member.upsert({ where: { @@ -76,9 +77,10 @@ export class TeamService { * Remove team member */ async removeMember(teamId: number, memberId: number, requesterId: number): Promise { + const projectService = new ProjectService() await this.validateTeamAccess(teamId, requesterId) await this.deleteCachedTeamByUserId(requesterId) - await deleteCachedUserProjects(requesterId) + await projectService.deleteCachedUserProjects(requesterId) const member = await this.getMemberById(memberId) await this.deleteTeammate(member.userId, requesterId) From bc4aef34ee5d8b6f2b0184def878142abf493c42 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 16:57:20 +0100 Subject: [PATCH 23/24] Variable service refacto --- .../server/api/variable/[id]/[env].delete.ts | 5 +- .../server/api/variable/[id]/[env].get.ts | 5 +- .../server/api/variable/index.delete.ts | 5 +- apps/shelve/server/api/variable/index.post.ts | 10 +- .../api/variable/project/[projectId].get.ts | 5 +- .../server/services/variable.service.ts | 282 +++++++++++------- 6 files changed, 200 insertions(+), 112 deletions(-) diff --git a/apps/shelve/server/api/variable/[id]/[env].delete.ts b/apps/shelve/server/api/variable/[id]/[env].delete.ts index fc187ab1..b8972c5c 100644 --- a/apps/shelve/server/api/variable/[id]/[env].delete.ts +++ b/apps/shelve/server/api/variable/[id]/[env].delete.ts @@ -1,11 +1,12 @@ import type { H3Event } from 'h3' -import { deleteVariable } from '~~/server/services/variable.service' +import { VariableService } from '~~/server/services/variable.service' export default eventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string const env = getRouterParam(event, 'env') as string if (!id && !env) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - await deleteVariable(parseInt(id), env) + const variableService = new VariableService() + await variableService.deleteVariable(+id, env) return { statusCode: 200, message: 'Variable deleted', diff --git a/apps/shelve/server/api/variable/[id]/[env].get.ts b/apps/shelve/server/api/variable/[id]/[env].get.ts index 544341e6..1d794166 100644 --- a/apps/shelve/server/api/variable/[id]/[env].get.ts +++ b/apps/shelve/server/api/variable/[id]/[env].get.ts @@ -1,10 +1,11 @@ import type { Environment } from '@shelve/types' import type { H3Event } from 'h3' -import { getVariablesByProjectId } from '~~/server/services/variable.service' +import { VariableService } from '~~/server/services/variable.service' export default eventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'id') as string const env = getRouterParam(event, 'env') as Environment if (!id && !env) throw createError({ statusCode: 400, statusMessage: 'Missing params' }) - return await getVariablesByProjectId(parseInt(id), env) + const variableService = new VariableService() + return await variableService.getVariablesByProjectId(+id, env) }) diff --git a/apps/shelve/server/api/variable/index.delete.ts b/apps/shelve/server/api/variable/index.delete.ts index 1c5043f6..36da8008 100644 --- a/apps/shelve/server/api/variable/index.delete.ts +++ b/apps/shelve/server/api/variable/index.delete.ts @@ -1,11 +1,12 @@ import type { H3Event } from 'h3' -import { deleteVariables } from '~~/server/services/variable.service' +import { VariableService } from '~~/server/services/variable.service' export default defineEventHandler(async (event: H3Event) => { + const variableService = new VariableService() const { user } = event.context const body = await readBody(event) const variablesId = body.variables - await deleteVariables(variablesId, user) + await variableService.deleteVariables(variablesId, user) return { statusCode: 200, message: 'Variables deleted', diff --git a/apps/shelve/server/api/variable/index.post.ts b/apps/shelve/server/api/variable/index.post.ts index fb715344..5a181477 100644 --- a/apps/shelve/server/api/variable/index.post.ts +++ b/apps/shelve/server/api/variable/index.post.ts @@ -1,11 +1,13 @@ import type { VariablesCreateInput } from '@shelve/types' import type { H3Event } from 'h3' -import { upsertVariable } from '~~/server/services/variable.service' -import { getProjectById } from '~~/server/services/project.service' +import { VariableService } from '~~/server/services/variable.service' +import { ProjectService } from '~~/server/services/project.service' export default eventHandler(async (event: H3Event) => { + const variableService = new VariableService() + const projectService = new ProjectService() const variablesCreateInput = await readBody(event) as VariablesCreateInput - const project = await getProjectById(variablesCreateInput.projectId) + const project = await projectService.getProjectById(variablesCreateInput.projectId) if (!project) throw createError({ statusCode: 400, statusMessage: 'Project not found' }) - return await upsertVariable(variablesCreateInput) + return await variableService.upsertVariable(variablesCreateInput) }) diff --git a/apps/shelve/server/api/variable/project/[projectId].get.ts b/apps/shelve/server/api/variable/project/[projectId].get.ts index 6d004f37..0dc3d75d 100644 --- a/apps/shelve/server/api/variable/project/[projectId].get.ts +++ b/apps/shelve/server/api/variable/project/[projectId].get.ts @@ -1,8 +1,9 @@ import type { H3Event } from 'h3' -import { getVariablesByProjectId } from '~~/server/services/variable.service' +import { VariableService } from '~~/server/services/variable.service' export default eventHandler(async (event: H3Event) => { const id = getRouterParam(event, 'projectId') as string if (!id) throw createError({ statusCode: 400, statusMessage: 'missing params' }) - return await getVariablesByProjectId(parseInt(id)) + const variableService = new VariableService() + return await variableService.getVariablesByProjectId(+id) }) diff --git a/apps/shelve/server/services/variable.service.ts b/apps/shelve/server/services/variable.service.ts index 24a89706..aca319de 100644 --- a/apps/shelve/server/services/variable.service.ts +++ b/apps/shelve/server/services/variable.service.ts @@ -1,130 +1,212 @@ import type { Environment, User, Variable, VariablesCreateInput } from '@shelve/types' import { unseal, seal } from '@shelve/crypto' -const varAssociation = { - production: 'production', - preview: 'preview', - staging: 'preview', - development: 'development', - prod: 'production', - dev: 'development', -} +export class VariableService { -const { encryptionKey } = useRuntimeConfig().private + private readonly encryptionKey: string -function getEnvString(env: string) { - return env.split('|').map((env) => varAssociation[env as Environment]).join('|') -} + private readonly ENV_ASSOCIATION = { + production: 'production', + preview: 'preview', + staging: 'preview', + development: 'development', + prod: 'production', + dev: 'development', + } as const -async function encryptVariable(variables: VariablesCreateInput['variables'], autoUppercase?: boolean): Promise { - for (const variable of variables) { - if (autoUppercase) variable.key = variable.key.toUpperCase() - const encryptedValue = await seal(variable.value, encryptionKey) - delete variable.index - variable.environment = getEnvString(variable.environment) - variable.value = encryptedValue + constructor() { + this.encryptionKey = useRuntimeConfig().private.encryptionKey } - return variables -} -async function decryptVariable(variables: VariablesCreateInput['variables']): Promise { - for (const variable of variables) { - const decryptedValue = await unseal(variable.value, encryptionKey) - variable.environment = getEnvString(variable.environment) - variable.value = decryptedValue + /** + * Upsert variables + */ + async upsertVariable(variablesCreateInput: VariablesCreateInput): Promise { + const encryptedVariables = await this.encryptVariables( + variablesCreateInput.variables, + variablesCreateInput.autoUppercase + ) + + if (variablesCreateInput.variables.length === 1) { + return this.upsertSingleVariable(encryptedVariables[0]) + } + + return this.upsertMultipleVariables( + encryptedVariables, + variablesCreateInput.projectId, + variablesCreateInput.method || 'merge', + variablesCreateInput.environment + ) } - return variables -} -export async function upsertVariable(variablesCreateInput: VariablesCreateInput) { - const encryptedVariables = await encryptVariable(variablesCreateInput.variables, variablesCreateInput.autoUppercase) + /** + * Get variables by project ID + */ + async getVariablesByProjectId(projectId: number, environment?: Environment): Promise { + const where = this.buildVariableQuery(projectId, environment) + const variables = await prisma.variables.findMany({ + where, + orderBy: { updatedAt: 'desc' } + }) + return this.decryptVariables(variables) + } - if (variablesCreateInput.variables.length === 1) { // use on main form variable/update - const [variableCreateInput] = encryptedVariables - if (!variableCreateInput) throw new Error('Invalid variable') - return prisma.variables.upsert({ - where: { id: variableCreateInput.id || -1 }, - update: variableCreateInput, - create: variableCreateInput, + /** + * Delete single variable + */ + async deleteVariable(id: number, environment: string): Promise { + const envs = environment.split('|').map((env) => decodeURIComponent(env)) + await prisma.variables.delete({ + where: { + id, + environment: { in: envs }, + }, }) - } // use on edit form variable/update or with CLI push command - const method = variablesCreateInput.method || 'merge' - if (method === 'overwrite') { - await prisma.variables.deleteMany({ where: { - projectId: variablesCreateInput.projectId, - environment: varAssociation[variablesCreateInput.environment] - } }) - return prisma.variables.createMany({ - data: encryptedVariables, - skipDuplicates: true, + } + + /** + * Delete multiple variables + */ + async deleteVariables(variablesId: number[], user: User): Promise { + await this.validateVariablesOwnership(variablesId, user) + await prisma.variables.deleteMany({ + where: { id: { in: variablesId } } }) } - const existingVariables = await prisma.variables.findMany({ - where: { - projectId: variablesCreateInput.projectId, - key: { - in: encryptedVariables.map(variable => variable.key) + + /** + * Private helper methods + */ + private async encryptVariables( + variables: VariablesCreateInput['variables'], + autoUppercase?: boolean + ): Promise { + return Promise.all(variables.map(async (variable) => { + const processed = { ...variable } + if (autoUppercase) { + processed.key = processed.key.toUpperCase() } - }, - select: { - id: true, - } - }) - for (const variable of encryptedVariables) { - if (existingVariables.some(existingVariable => existingVariable.id === variable.id)) { - await prisma.variables.update({ - where: { id: variable.id }, - data: variable, + processed.value = await seal(processed.value, this.encryptionKey) + processed.environment = this.getEnvString(processed.environment) + delete processed.index + return processed + })) + } + + private async decryptVariables( + variables: VariablesCreateInput['variables'] + ): Promise { + return Promise.all(variables.map(async (variable) => { + const processed = { ...variable } + processed.value = await unseal(processed.value, this.encryptionKey) + processed.environment = this.getEnvString(processed.environment) + return processed + })) + } + + private getEnvString(env: string): string { + return env.split('|') + .map((env) => this.ENV_ASSOCIATION[env as Environment]) + .join('|') + } + + private async upsertSingleVariable(variable: Variable): Promise { + if (!variable) { + throw createError({ + statusCode: 400, + message: 'Invalid variable' }) } - await prisma.variables.create({ - data: variable, + + return prisma.variables.upsert({ + where: { id: variable.id || -1 }, + update: variable, + create: variable, }) } - return encryptedVariables -} -export async function getVariablesByProjectId(projectId: number, environment?: Environment): Promise { - const options = environment ? { - projectId, - environment: { - contains: varAssociation[environment], + private async upsertMultipleVariables( + variables: Variable[], + projectId: number, + method: 'merge' | 'overwrite', + environment: Environment + ): Promise { + if (method === 'overwrite') { + await this.deleteExistingVariables(projectId, environment) + await prisma.variables.createMany({ + data: variables, + skipDuplicates: true, + }) + return variables } - } : { projectId } - const variables = await prisma.variables.findMany({ where: options, orderBy: { updatedAt: 'desc' } }) - return await decryptVariable(variables) -} -export async function deleteVariable(id: number, environment: string): Promise { - const envs = environment.split('|').map((env) => decodeURIComponent(env)) - await prisma.variables.delete({ - where: { - id, - environment: { - in: envs, + return this.mergeVariables(variables, projectId) + } + + private async deleteExistingVariables(projectId: number, environment: Environment): Promise { + await prisma.variables.deleteMany({ + where: { + projectId, + environment: this.ENV_ASSOCIATION[environment] + } + }) + } + + private async mergeVariables(variables: Variable[], projectId: number): Promise { + const existingVariables = await prisma.variables.findMany({ + where: { + projectId, + key: { in: variables.map(v => v.key) } }, - }, - }) -} + select: { id: true } + }) + + for (const variable of variables) { + if (existingVariables.some(ev => ev.id === variable.id)) { + await prisma.variables.update({ + where: { id: variable.id }, + data: variable, + }) + } else { + await prisma.variables.create({ + data: variable, + }) + } + } + + return variables + } -export async function deleteVariables(variablesId: number[], user: User): Promise { - const variables = await prisma.variables.findMany({ - where: { - id: { - in: variablesId + private buildVariableQuery(projectId: number, environment?: Environment) { + if (!environment) return { projectId } + return { + projectId, + environment: { + contains: this.ENV_ASSOCIATION[environment], } - }, - select: { - id: true, - project: { - select: { - ownerId: true + } + } + + private async validateVariablesOwnership(variablesId: number[], user: User): Promise { + const variables = await prisma.variables.findMany({ + where: { + id: { in: variablesId } + }, + select: { + id: true, + project: { + select: { ownerId: true } } } + }) + + const hasPermission = variables.every(v => v.project.ownerId === user.id) + if (!hasPermission) { + throw createError({ + statusCode: 403, + message: 'You do not have permission to delete these variables' + }) } - }) - if (!variables.every(variable => variable.project.ownerId === user.id)) { - throw new Error('You do not have permission to delete these variables') } - await prisma.variables.deleteMany({ where: { id: { in: variables.map(variable => variable.id) } } }) + } From c98d5769e3e9949e56ff9ff8f0e0a456ed44c761 Mon Sep 17 00:00:00 2001 From: HugoRCD Date: Fri, 1 Nov 2024 17:06:16 +0100 Subject: [PATCH 24/24] improve types packages --- .gitignore | 3 +++ apps/shelve/server/services/teams.service.ts | 2 +- packages/cli/src/utils/config.ts | 2 +- packages/types/package.json | 2 +- packages/types/src/Cli.ts | 2 +- packages/types/src/Project.ts | 6 ++--- packages/types/src/Session.ts | 28 -------------------- packages/types/src/Team.ts | 4 +-- packages/types/src/User.ts | 4 +-- packages/types/src/index.ts | 1 - 10 files changed, 14 insertions(+), 40 deletions(-) delete mode 100644 packages/types/src/Session.ts diff --git a/.gitignore b/.gitignore index 73ff5f86..79d46ac9 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ dist .idea .config/shelve* + +*/dist/ +*/.output/ diff --git a/apps/shelve/server/services/teams.service.ts b/apps/shelve/server/services/teams.service.ts index d4860b66..e22e3f98 100644 --- a/apps/shelve/server/services/teams.service.ts +++ b/apps/shelve/server/services/teams.service.ts @@ -1,4 +1,4 @@ -import type { CreateTeamInput, Team, Member } from '@shelve/types' +import type { CreateTeamInput, DeleteTeamInput, Team, Member } from '@shelve/types' import { Role, TeamRole } from '@shelve/types' import { ProjectService } from '~~/server/services/project.service' diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts index a7154d7d..5d4573ad 100644 --- a/packages/cli/src/utils/config.ts +++ b/packages/cli/src/utils/config.ts @@ -2,7 +2,7 @@ import fs from 'fs' import { intro, isCancel, outro, select } from '@clack/prompts' import { loadConfig, setupDotenv, type ConfigLayer } from 'c12' import consola from 'consola' -import { SHELVE_JSON_SCHEMA, ShelveConfig } from '@shelve/types' +import { SHELVE_JSON_SCHEMA, type ShelveConfig } from '@shelve/types' import { getProjects } from './project' import { getKeyValue } from './env' import { onCancel } from './index' diff --git a/packages/types/package.json b/packages/types/package.json index 82215fe6..74311f16 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@shelve/types", - "version": "1.0.0", + "version": "1.0.1", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/types/src/Cli.ts b/packages/types/src/Cli.ts index 1635cb10..bcdc5cc0 100644 --- a/packages/types/src/Cli.ts +++ b/packages/types/src/Cli.ts @@ -1,4 +1,4 @@ -import { Env, Environment } from './Variables' +import type { Env, Environment } from './Variables' export const SHELVE_JSON_SCHEMA = 'https://raw.githubusercontent.com/HugoRCD/shelve/main/packages/types/shelveConfigSchema.json' diff --git a/packages/types/src/Project.ts b/packages/types/src/Project.ts index 7f13b8e1..1789c185 100644 --- a/packages/types/src/Project.ts +++ b/packages/types/src/Project.ts @@ -1,6 +1,6 @@ -import type { Variable } from "./Variables"; -import type { User } from "./User"; -import type { Team } from "./Team"; +import type { Variable } from './Variables' +import type { User } from './User' +import type { Team } from './Team' export type Project = { id: number; diff --git a/packages/types/src/Session.ts b/packages/types/src/Session.ts deleted file mode 100644 index 07e4400e..00000000 --- a/packages/types/src/Session.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type Session = { - id: number; - userId: number; - authToken: string; - current: boolean; - device: string; - location?: string; - isCli: boolean; - createdAt: Date; - updatedAt: Date; -}; - -export type SessionWithCurrent = Session & { - current: boolean; -}; - -export type DeviceInfo = { - userAgent: string; - location?: string; - isCli?: boolean; -}; - -export type CreateSessionInput = { - email: string; - password?: string; - otp: string; - deviceInfo: DeviceInfo; -}; diff --git a/packages/types/src/Team.ts b/packages/types/src/Team.ts index b94c805b..2ce87f5a 100644 --- a/packages/types/src/Team.ts +++ b/packages/types/src/Team.ts @@ -1,4 +1,4 @@ -import { Role } from '../dist/index' +import { Role } from './User' import type { Project } from './Project' import type { User } from './User' @@ -39,7 +39,7 @@ export type UpdateTeamInput = { projects: Project[]; } -type DeleteTeamInput = { +export type DeleteTeamInput = { teamId: number userId: number userRole: Role diff --git a/packages/types/src/User.ts b/packages/types/src/User.ts index 7da5fe4e..1a87bb32 100644 --- a/packages/types/src/User.ts +++ b/packages/types/src/User.ts @@ -1,6 +1,6 @@ export enum Role { - ADMIN = "admin", - USER = "user", + ADMIN = 'admin', + USER = 'user', } export type User = { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 4fab352f..1e708871 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,7 +1,6 @@ export * from './Variables' export * from './User' export * from './Project' -export * from './Session' export * from './Team' export * from './Token' export * from './Cli'