From 27cb044247b2de3bf05445e26fd0f1faf3a99cbb Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Sat, 30 May 2026 23:25:50 +0200 Subject: [PATCH] feat: use portainer hook instead of pushing image to gitea package repository --- .gitea/workflows/ci.yml | 34 ----- .gitea/workflows/deploy.yml | 44 +++++-- app/components/FilesTable.vue | 69 ++++++++++ app/components/RenameFileModal.vue | 59 +++++++++ app/pages/dashboard.vue | 162 ++++++++++++++++++------ app/utils/api.ts | 1 + docker-compose.yml | 2 +- nuxt.config.ts | 4 +- package.json | 1 + pnpm-lock.yaml | 13 ++ public/files/pubkey.asc | 13 ++ server/api/files/[name]/index.delete.ts | 24 ++++ server/api/files/[name]/index.patch.ts | 31 +++++ server/api/files/index.get.ts | 20 +++ server/api/files/index.post.ts | 22 ++++ 15 files changed, 411 insertions(+), 88 deletions(-) delete mode 100644 .gitea/workflows/ci.yml create mode 100644 app/components/FilesTable.vue create mode 100644 app/components/RenameFileModal.vue create mode 100644 public/files/pubkey.asc create mode 100644 server/api/files/[name]/index.delete.ts create mode 100644 server/api/files/[name]/index.patch.ts create mode 100644 server/api/files/index.get.ts create mode 100644 server/api/files/index.post.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index 58d9ce9..0000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: ci - -on: push - -jobs: - ci: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [22] - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Install pnpm - uses: pnpm/action-setup@v4 - - - name: Install node - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node }} - cache: pnpm - - - name: Install dependencies - run: pnpm install - - - name: Lint - run: pnpm run lint - - - name: Typecheck - run: pnpm run typecheck diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b7783a2..d9e57fa 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Build and Push Docker Image +name: Deploy on: push: @@ -11,24 +11,40 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install + + - name: Lint + run: pnpm run lint + + - name: Typecheck + run: pnpm run typecheck + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to Gitea Container Registry - uses: docker/login-action@v3 - with: - registry: git.pihkaal.me - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_PASSWORD }} - - - name: Build and push Docker image + - name: Build Docker image uses: docker/build-push-action@v6 with: context: . - push: true - tags: git.pihkaal.me/pihkaal/pihka-al:latest - cache-from: type=registry,ref=git.pihkaal.me/pihkaal/pihka-al:cache - cache-to: type=registry,ref=git.pihkaal.me/pihkaal/pihka-al:cache,mode=max + push: false + + deploy: + runs-on: ubuntu-latest + needs: build + + steps: + - name: Trigger Portainer webhook + run: curl -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}" diff --git a/app/components/FilesTable.vue b/app/components/FilesTable.vue new file mode 100644 index 0000000..e6606a9 --- /dev/null +++ b/app/components/FilesTable.vue @@ -0,0 +1,69 @@ + + + diff --git a/app/components/RenameFileModal.vue b/app/components/RenameFileModal.vue new file mode 100644 index 0000000..dbfadd1 --- /dev/null +++ b/app/components/RenameFileModal.vue @@ -0,0 +1,59 @@ + + + diff --git a/app/pages/dashboard.vue b/app/pages/dashboard.vue index 0411595..53eaf33 100644 --- a/app/pages/dashboard.vue +++ b/app/pages/dashboard.vue @@ -1,10 +1,11 @@ - +
+ + + + + +
+ - + diff --git a/app/utils/api.ts b/app/utils/api.ts index f259774..6ed3b67 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import type { InternalApi } from "nitropack/types"; export type Link = InternalApi["/api/links"]["get"][number]; +export type FileEntry = { name: string; size: number; modifiedAt: string }; const apiErrorSchema = z.object({ data: z.object({ diff --git a/docker-compose.yml b/docker-compose.yml index f528a69..675c7d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: app: container_name: pihka-al - image: git.pihkaal.me/pihkaal/pihka-al:latest + build: . restart: unless-stopped environment: - DATABASE_URL=/data/db.sqlite diff --git a/nuxt.config.ts b/nuxt.config.ts index b92fb19..898042c 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,6 +1,6 @@ // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ - modules: ['@nuxt/eslint', '@nuxt/ui', 'nuxt-auth-utils'], + modules: ['@nuxt/eslint', '@nuxt/ui', 'nuxt-auth-utils', 'nuxt-file-storage'], vite: { optimizeDeps: { @@ -30,4 +30,4 @@ export default defineNuxtConfig({ }, compatibilityDate: '2025-01-15', -}) +}) \ No newline at end of file diff --git a/package.json b/package.json index dad876c..97cf1b6 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "drizzle-orm": "^0.45.1", "nuxt": "^4.4.2", "nuxt-auth-utils": "0.5.29", + "nuxt-file-storage": "0.3.2", "tailwindcss": "^4.2.1", "zod": "^4.3.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c14d5b..7ccf026 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: nuxt-auth-utils: specifier: 0.5.29 version: 0.5.29(magicast@0.5.2) + nuxt-file-storage: + specifier: 0.3.2 + version: 0.3.2(magicast@0.5.2) tailwindcss: specifier: ^4.2.1 version: 4.2.1 @@ -4252,6 +4255,9 @@ packages: '@simplewebauthn/server': optional: true + nuxt-file-storage@0.3.2: + resolution: {integrity: sha512-ET2bg2dSiSqiuos017h/6e+zE7BNvdugsppOBCJMfwz6Jvqg+So0QUELmNGloJC2y+CEt2fe09tvtHeqfFwP/w==} + nuxt@4.4.2: resolution: {integrity: sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9778,6 +9784,13 @@ snapshots: - bcrypt - magicast + nuxt-file-storage@0.3.2(magicast@0.5.2): + dependencies: + '@nuxt/kit': 4.4.2(magicast@0.5.2) + defu: 6.1.4 + transitivePeerDependencies: + - magicast + nuxt@4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.5.0)(@vue/compiler-sfc@3.5.30)(better-sqlite3@12.8.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.8.0)(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0)))(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0))(eslint@10.0.3(jiti@2.6.1))(ioredis@5.10.0)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@6.0.11(rollup@4.59.0))(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3))(yaml@2.8.2): dependencies: '@dxup/nuxt': 0.4.0(magicast@0.5.2)(typescript@5.9.3) diff --git a/public/files/pubkey.asc b/public/files/pubkey.asc new file mode 100644 index 0000000..fb84ea0 --- /dev/null +++ b/public/files/pubkey.asc @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaUnZbxYJKwYBBAHaRw8BAQdABdo9ty0Y6CM0YuDnWBwSYhtoa11yEYcgSWRj +CJjvk8S0GlBpaGthYWwgPGhlbGxvQHBpaGthYWwubWU+iJYEExYKAD4WIQR3xxLs +eQDwQbaRfTdcynYY9m/fvQUCaUnZbwIbAwUJAeEzgAULCQgHAgYVCgkICwIEFgID +AQIeAQIXgAAKCRBcynYY9m/fva/9AQCsdzzCeJHfrX8Y3vLlNOf1urpJ22J2acIa +7mYc04NX8QEAtpMvXSqOWnfLszSXmU/jQSW2i6e2bb4ifmXjxVRpJQG4OARpSdlv +EgorBgEEAZdVAQUBAQdA7+ROVEdDM6OewnIwjbAvqErWqn0wXj9/JqfoV8dD5XsD +AQgHiH4EGBYKACYWIQR3xxLseQDwQbaRfTdcynYY9m/fvQUCaUnZbwIbDAUJAeEz +gAAKCRBcynYY9m/fvVQcAQCXK5a0t5nFzKn6FOa2W3232XNyOHkohvCKJHiojbKp +RgD/bn5ChqQXkskUJ//VdVNbSFpV22Z3jobNcTkPSwrIFwk= +=8Y+6 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/server/api/files/[name]/index.delete.ts b/server/api/files/[name]/index.delete.ts new file mode 100644 index 0000000..e7cb725 --- /dev/null +++ b/server/api/files/[name]/index.delete.ts @@ -0,0 +1,24 @@ +import { unlink } from "node:fs/promises"; +import { resolve } from "node:path"; +import { z } from "zod"; + +const paramsSchema = z.object({ + name: z.string().min(1).regex(/^[^/\\]+$/, "Invalid filename"), +}); + +export default defineEventHandler(async (event) => { + const params = await getValidatedRouterParams(event, paramsSchema.parse); + const dir = resolve(process.cwd(), "public/files"); + const filePath = resolve(dir, params.name); + + if (!filePath.startsWith(dir + "/")) { + throw createError({ statusCode: 400, message: "Invalid filename" }); + } + + try { + await unlink(filePath); + return { success: true }; + } catch { + throw createError({ statusCode: 404, message: "File not found" }); + } +}); diff --git a/server/api/files/[name]/index.patch.ts b/server/api/files/[name]/index.patch.ts new file mode 100644 index 0000000..26f996a --- /dev/null +++ b/server/api/files/[name]/index.patch.ts @@ -0,0 +1,31 @@ +import { rename } from "node:fs/promises"; +import { resolve } from "node:path"; +import { z } from "zod"; + +const paramsSchema = z.object({ + name: z.string().min(1).regex(/^[^/\\]+$/, "Invalid filename"), +}); + +const bodySchema = z.object({ + name: z.string().min(1).regex(/^[^/\\]+$/, "Invalid filename"), +}); + +export default defineEventHandler(async (event) => { + const params = await getValidatedRouterParams(event, paramsSchema.parse); + const body = await readValidatedBody(event, bodySchema.parse); + + const dir = resolve(process.cwd(), "public/files"); + const oldPath = resolve(dir, params.name); + const newPath = resolve(dir, body.name); + + if (!oldPath.startsWith(dir + "/") || !newPath.startsWith(dir + "/")) { + throw createError({ statusCode: 400, message: "Invalid filename" }); + } + + try { + await rename(oldPath, newPath); + return { name: body.name }; + } catch { + throw createError({ statusCode: 404, message: "File not found" }); + } +}); diff --git a/server/api/files/index.get.ts b/server/api/files/index.get.ts new file mode 100644 index 0000000..84098ca --- /dev/null +++ b/server/api/files/index.get.ts @@ -0,0 +1,20 @@ +import { readdir, stat } from "node:fs/promises"; +import { resolve } from "node:path"; + +const filesDir = () => resolve(process.cwd(), "public/files"); + +export default defineEventHandler(async () => { + const dir = filesDir(); + try { + const names = await readdir(dir); + const files = await Promise.all( + names.map(async (name) => { + const s = await stat(resolve(dir, name)); + return { name, size: s.size, modifiedAt: s.mtime.toISOString() }; + }), + ); + return files; + } catch { + return []; + } +}); diff --git a/server/api/files/index.post.ts b/server/api/files/index.post.ts new file mode 100644 index 0000000..bc07e2c --- /dev/null +++ b/server/api/files/index.post.ts @@ -0,0 +1,22 @@ +import { writeFile, mkdir } from "node:fs/promises"; +import { resolve } from "node:path"; + +export default defineEventHandler(async (event) => { + const parts = await readMultipartFormData(event); + if (!parts || parts.length === 0) { + throw createError({ statusCode: 400, message: "No file provided" }); + } + + const filePart = parts.find((p) => p.name === "file"); + if (!filePart || !filePart.filename) { + throw createError({ statusCode: 400, message: "No file provided" }); + } + + const filename = filePart.filename.replace(/[^a-zA-Z0-9._-]/g, "_"); + const dir = resolve(process.cwd(), "public/files"); + + await mkdir(dir, { recursive: true }); + await writeFile(resolve(dir, filename), filePart.data); + + return { name: filename, size: filePart.data.length }; +});