diff --git a/bun.lockb b/bun.lockb index d72c73a..27f67e2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/AppFileUpload.vue b/components/AppFileUpload.vue new file mode 100644 index 0000000..a31dcaf --- /dev/null +++ b/components/AppFileUpload.vue @@ -0,0 +1,44 @@ + + + diff --git a/components/Releases.vue b/components/Releases.vue index 5e69208..ebd5b48 100644 --- a/components/Releases.vue +++ b/components/Releases.vue @@ -1,3 +1,56 @@ \ No newline at end of file + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/UploadArtifact.vue b/components/UploadArtifact.vue new file mode 100644 index 0000000..612ea26 --- /dev/null +++ b/components/UploadArtifact.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/package.json b/package.json index 068f183..cea6b45 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "format:check": "prettier --check \"**/*.{js,vue,d.ts}\"" }, "dependencies": { + "@aws-sdk/client-s3": "^3.540.0", + "@aws-sdk/s3-request-presigner": "^3.540.0", "@hebilicious/vue-query-nuxt": "^0.3.0", "@pinia/nuxt": "^0.5.1", "@prisma/client": "^5.11.0", @@ -24,17 +26,20 @@ "h3": "^1.11.1", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", + "moment": "^2.30.1", "pg": "^8.11.3", "pinia": "^2.1.7", "primeflex": "^3.3.0", "primeicons": "^6.0.1", - "primevue": "^3.49.1" + "primevue": "^3.49.1", + "uuid": "^9.0.1" }, "devDependencies": { "@babel/eslint-parser": "^7.18.9", "@types/jsonwebtoken": "^9.0.6", "@types/lodash": "^4.17.0", "@types/pg": "^8.11.4", + "@types/uuid": "^9.0.8", "autoprefixer": "^10.4.19", "eslint": "^8.30.0", "eslint-config-prettier": "^8.5.0", diff --git a/pages/orgs/[orgName]/apps/[appId].vue b/pages/orgs/[orgName]/apps/[appId].vue index 6bee0dd..6d0865b 100644 --- a/pages/orgs/[orgName]/apps/[appId].vue +++ b/pages/orgs/[orgName]/apps/[appId].vue @@ -1,22 +1,20 @@ \ No newline at end of file diff --git a/prisma/migrations/20240402001549_/migration.sql b/prisma/migrations/20240402001549_/migration.sql new file mode 100644 index 0000000..235792d --- /dev/null +++ b/prisma/migrations/20240402001549_/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "Artifacts" ( + "id" SERIAL NOT NULL, + "fileObjectId" TEXT NOT NULL, + "versionName" TEXT NOT NULL, + "versionCode" TEXT NOT NULL, + "releaseNotes" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "appsId" INTEGER NOT NULL, + + CONSTRAINT "Artifacts_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Artifacts_fileObjectId_key" ON "Artifacts"("fileObjectId"); + +-- AddForeignKey +ALTER TABLE "Artifacts" ADD CONSTRAINT "Artifacts_appsId_fkey" FOREIGN KEY ("appsId") REFERENCES "Apps"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240403222737_/migration.sql b/prisma/migrations/20240403222737_/migration.sql new file mode 100644 index 0000000..c5c7587 --- /dev/null +++ b/prisma/migrations/20240403222737_/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - You are about to drop the column `fileObjectId` on the `Artifacts` table. All the data in the column will be lost. + - A unique constraint covering the columns `[fileObjectKey]` on the table `Artifacts` will be added. If there are existing duplicate values, this will fail. + - Added the required column `fileObjectKey` to the `Artifacts` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "Artifacts_fileObjectId_key"; + +-- AlterTable +ALTER TABLE "Artifacts" DROP COLUMN "fileObjectId", +ADD COLUMN "fileObjectKey" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Artifacts_fileObjectKey_key" ON "Artifacts"("fileObjectKey"); diff --git a/prisma/migrations/20240403223916_/migration.sql b/prisma/migrations/20240403223916_/migration.sql new file mode 100644 index 0000000..5db5b81 --- /dev/null +++ b/prisma/migrations/20240403223916_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Artifacts" ALTER COLUMN "releaseNotes" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0700376..3e66ac9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,7 +10,7 @@ datasource db { // } generator clientWorker { - provider = "prisma-client-js" + provider = "prisma-client-js" // previewFeatures = ["driverAdapters"] } @@ -46,6 +46,20 @@ model Apps { Organization Organizations @relation(fields: [organizationsId], references: [id]) organizationsId Int + Artifacts Artifacts[] @@unique([organizationsId, name]) } + +model Artifacts { + id Int @id @default(autoincrement()) + fileObjectKey String @unique + versionName String + versionCode String + releaseNotes String? + createdAt DateTime + updatedAt DateTime + + apps Apps @relation(fields: [appsId], references: [id]) + appsId Int +} diff --git a/server/api/artifacts/list-artifacts.get.ts b/server/api/artifacts/list-artifacts.get.ts new file mode 100644 index 0000000..bbdca40 --- /dev/null +++ b/server/api/artifacts/list-artifacts.get.ts @@ -0,0 +1,27 @@ +import { getStorageKeys } from "~/server/utils/utils" + +export default defineEventHandler(async (event) => { + const prisma = event.context.prisma + const { appName, orgName } = getQuery(event) + return await prisma.artifacts.findMany({ + include: { + apps: true, + }, + orderBy: { + createdAt: 'desc', + }, + where: { + apps: { + name: appName?.toString(), + Organization: { + name: orgName?.toString(), + OrganizationsPeople: { + every: { + userId: event.context.auth.userId, + }, + }, + }, + }, + }, + }) +}) diff --git a/server/api/artifacts/upload-artifact-url.post.ts b/server/api/artifacts/upload-artifact-url.post.ts new file mode 100644 index 0000000..e36fe52 --- /dev/null +++ b/server/api/artifacts/upload-artifact-url.post.ts @@ -0,0 +1,46 @@ +import { getStorageKeys } from "~/server/utils/utils" + +export default defineEventHandler(async (event) => { + const { key, appName, orgName } = await readBody(event) + const { temp, assets } = getStorageKeys(event.context.auth, key) + const prisma = event.context.prisma + await prisma.$transaction(async (t) => { + // TODO: Select only id + const app = await prisma.apps.findFirstOrThrow({ + include: { + Organization: true, + }, + where: { + name: appName, + Organization: { + name: orgName, + OrganizationsPeople: { + every: { + userId: event.context.auth.userId, + }, + }, + }, + }, + }) + const now = new Date() + await prisma.artifacts.create({ + data: { + createdAt: now, + updatedAt: now, + fileObjectKey: key, + versionCode: '1', + versionName: '1.0.0', + appsId: app.id, + }, + }) + }) + await event.context.s3.copyObject({ + CopySource: `app-deployin/${temp}`, + Bucket: 'app-deployin', + Key: assets, + }) + await event.context.s3.deleteObject({ + Bucket: 'app-deployin', + Key: temp, + }) +}) diff --git a/server/api/artifacts/upload-artifact.post.ts b/server/api/artifacts/upload-artifact.post.ts new file mode 100644 index 0000000..52006f5 --- /dev/null +++ b/server/api/artifacts/upload-artifact.post.ts @@ -0,0 +1,20 @@ +import { PutObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner" +import { v4 } from "uuid"; +import { getStorageKeys } from "~/server/utils/utils"; + +export default defineEventHandler(async (event) => { + const key = v4() + var expires = new Date(); expires.setMinutes(expires.getMinutes() + 30); + const { temp } = getStorageKeys(event.context.auth, key) + const signedUrl = await getSignedUrl(event.context.s3Client, new PutObjectCommand({ + Bucket: 'app-deployin', + Key: temp, + // Expires: expires, // TODO: epxiry here + ContentType: 'application/vnd.android.package-archive', + })) + return { + file: key, + url: signedUrl, + } +}) diff --git a/server/middleware/00.start.ts b/server/middleware/00.start.ts index 63f3f05..444cab2 100644 --- a/server/middleware/00.start.ts +++ b/server/middleware/00.start.ts @@ -1,12 +1,18 @@ import { PrismaClient } from '@prisma/client' import { services } from '../services'; +import { S3, S3Client } from '@aws-sdk/client-s3/'; declare module 'h3' { interface H3EventContext { - prisma: PrismaClient; + prisma: PrismaClient, + s3Client: S3Client, + s3: S3, } } export default defineEventHandler(async (event) => { - event.context.prisma = services.prisma + event.context = { + ...event.context, + ...services, + } }) diff --git a/server/middleware/auth-middleware.ts b/server/middleware/auth-middleware.ts index db5b289..9f6ed6f 100644 --- a/server/middleware/auth-middleware.ts +++ b/server/middleware/auth-middleware.ts @@ -1,7 +1,7 @@ import jsonwebtoken from 'jsonwebtoken' import { JWT_KEY } from '../utils/utils'; -type AuthData = { +export type AuthData = { userId: number, } diff --git a/server/plugins/startup.ts b/server/plugins/startup.ts index 8b0d95a..6085b59 100644 --- a/server/plugins/startup.ts +++ b/server/plugins/startup.ts @@ -1,8 +1,20 @@ import { PrismaClient } from "@prisma/client" import { services } from "../services" +import { S3, S3Client, type S3ClientConfig } from "@aws-sdk/client-s3" export default defineNitroPlugin(async (nuxtApp) => { services.prisma = new PrismaClient({ log: ['error', 'info', 'query', 'warn'] }) + const s3Config: S3ClientConfig = { + credentials: { + accessKeyId: 'niMVVLTJtujejdnkkceX', + secretAccessKey: 'n8FRdjEn7mAKKSq2hpnXAShu6GhqSj8PqQ0IGl9H' + }, + endpoint: 'http://127.0.0.1:9000', + forcePathStyle: true, + region: 'us-east-1', + } + services.s3Client = new S3Client(s3Config) + services.s3 = new S3(s3Config) }) diff --git a/server/services.ts b/server/services.ts index 03d2044..4e5d25e 100644 --- a/server/services.ts +++ b/server/services.ts @@ -1,7 +1,10 @@ +import type { S3, S3Client } from "@aws-sdk/client-s3"; import type { PrismaClient } from "@prisma/client"; class Services { prisma!: PrismaClient; + s3Client!: S3Client + s3!: S3 } export const services = new Services() diff --git a/server/utils/utils.ts b/server/utils/utils.ts index 10db7e6..bbef3aa 100644 --- a/server/utils/utils.ts +++ b/server/utils/utils.ts @@ -1,5 +1,14 @@ +import type { AuthData } from "../middleware/auth-middleware" + export const normalizeName = (value: string): string => { return value.replaceAll(' ', '-') } export const JWT_KEY = process.env.JWT_KEY! + +export const getStorageKeys = (auth: AuthData, key: String) => { + return { + temp: `temporary/userId-${auth.userId}/${key}`, + assets: `assets/userId-${auth.userId}/${key}`, + } +} diff --git a/utils/utils.ts b/utils/utils.ts new file mode 100644 index 0000000..337dcd3 --- /dev/null +++ b/utils/utils.ts @@ -0,0 +1,14 @@ +export const toOsType = (osType?: number): OsType => { + switch (osType) { + case 0: + return 'android' + default: + return 'ios' + } +} + +export const getMimeTypeFromosType = (osType?: OsType): string => { + return osType == 'android' ?'application/vnd.android.package-archive' : 'application/octet-stream' +} + +export type OsType = 'android' | 'ios' \ No newline at end of file