Compare commits
10 Commits
1b28a22589
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e5af5cb2a3
|
|||
|
a1cecd6de4
|
|||
|
053aa97983
|
|||
|
7d86903565
|
|||
|
32b3102070
|
|||
|
1dc6e8b4f9
|
|||
|
1299fc4dd3
|
|||
|
309b0c618f
|
|||
|
58f5af27db
|
|||
|
89cb014163
|
@@ -22,6 +22,24 @@ steps:
|
||||
depends_on:
|
||||
- Install
|
||||
|
||||
- name: Send Prettier Status Notification (failure)
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_tests_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Prettier failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||
depends_on:
|
||||
- Prettier
|
||||
when:
|
||||
- status: [failure]
|
||||
|
||||
- name: Lint
|
||||
image: node:22-bookworm-slim
|
||||
commands:
|
||||
@@ -38,6 +56,24 @@ steps:
|
||||
depends_on:
|
||||
- Lint
|
||||
|
||||
- name: Send Lint Status Notification (failure)
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_tests_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Lint failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||
depends_on:
|
||||
- Lint
|
||||
when:
|
||||
- status: [failure]
|
||||
|
||||
- name: Build
|
||||
image: node:22-bookworm-slim
|
||||
commands:
|
||||
@@ -46,23 +82,41 @@ steps:
|
||||
depends_on:
|
||||
- Tests & Coverage
|
||||
|
||||
# build-full:
|
||||
# image: node:22-bookworm-slim
|
||||
# commands:
|
||||
# - apt-get update
|
||||
# - apt-get install -y --no-install-recommends ca-certificates libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 libdrm2 libgbm1 libgtk-3-0 libnss3 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2
|
||||
# - rm -rf /var/lib/apt/lists/*
|
||||
# - corepack enable && corepack prepare pnpm@latest --activate
|
||||
# - pnpm run critical-css:install
|
||||
# - pnpm run build:full
|
||||
# depends_on:
|
||||
# - build
|
||||
- name: Send Build Status Notification (failure)
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_tests_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||
depends_on:
|
||||
- Build
|
||||
when:
|
||||
- status: [failure]
|
||||
|
||||
# e2e:
|
||||
# image: node:22-bookworm-slim
|
||||
# commands:
|
||||
# - corepack enable && corepack prepare pnpm@latest --activate
|
||||
# - pnpm exec playwright install chromium --with-deps
|
||||
# - pnpm run test:e2e
|
||||
# depends_on:
|
||||
# - build
|
||||
- name: Send CI Pipeline Status Notification (success)
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_tests_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] CI pipeline success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||
depends_on:
|
||||
- Install
|
||||
- Lint
|
||||
- Prettier
|
||||
- Build
|
||||
when:
|
||||
- status: [success]
|
||||
|
||||
@@ -55,12 +55,16 @@ steps:
|
||||
- name: Send Build Status Notification (success)
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
DISCORD_WEBHOOK_URL:
|
||||
from_secret: discord_webhook_url
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_pushes_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Docker images build success 🎉"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker images build success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||
depends_on:
|
||||
- Docker image build (qr-api + qr-web)
|
||||
when:
|
||||
@@ -69,12 +73,16 @@ steps:
|
||||
- name: Send Build Status Notification (failure)
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
DISCORD_WEBHOOK_URL:
|
||||
from_secret: discord_webhook_url
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_pushes_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Docker images build failure 💩"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker images build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||
depends_on:
|
||||
- Docker image build (qr-api + qr-web)
|
||||
when:
|
||||
@@ -103,12 +111,16 @@ steps:
|
||||
- name: Send Deploy Status Notification (success)
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
DISCORD_WEBHOOK_URL:
|
||||
from_secret: discord_webhook_url
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_pushes_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Production Deploy success 🎉"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||
depends_on:
|
||||
- Trigger Portainer stack redeploy
|
||||
when:
|
||||
@@ -117,12 +129,16 @@ steps:
|
||||
- name: Send Deploy Status Notification (failure)
|
||||
image: curlimages/curl
|
||||
environment:
|
||||
DISCORD_WEBHOOK_URL:
|
||||
from_secret: discord_webhook_url
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_pushes_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Production Deploy failure 💩"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||
depends_on:
|
||||
- Trigger Portainer stack redeploy
|
||||
when:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Portainer stack: registry-based images (no build). Use with CI/CD webhook redeploy.
|
||||
# Set in Portainer stack env (or .env): REGISTRY, IMAGE_TAG (defaults below).
|
||||
# Images: ${REGISTRY}/mifi-holdings/shorty-qr-api:${IMAGE_TAG}, shorty-qr-web:${IMAGE_TAG}
|
||||
|
||||
services:
|
||||
kutt_db:
|
||||
image: postgres:16-alpine
|
||||
@@ -64,24 +63,27 @@ services:
|
||||
start_period: 40s
|
||||
environment:
|
||||
ADMIN_EMAILS: ${ADMIN_EMAILS:?Set ADMIN_EMAILS}
|
||||
DEFAULT_DOMAIN: mifi.me
|
||||
CUSTOM_DOMAIN_USE_HTTPS: 'true'
|
||||
DEFAULT_DOMAIN: ${DEFAULT_DOMAIN:?Set DEFAULT_DOMAIN}
|
||||
DB_CLIENT: pg
|
||||
DB_HOST: kutt_db
|
||||
DB_PORT: '5432'
|
||||
DB_USER: ${DB_USER:-kutt}
|
||||
DB_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD}
|
||||
DB_POOL_MIN: '2'
|
||||
DB_POOL_MAX: '10'
|
||||
DB_NAME: ${DB_NAME:-kutt}
|
||||
DISALLOW_ANONYMOUS_LINKS: 'true'
|
||||
DISALLOW_REGISTRATION: 'true'
|
||||
LINK_LENGTH: 6
|
||||
JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET}
|
||||
MAIL_ENABLED: 'true'
|
||||
MAIL_HOST: 'mail.mifi.holdings'
|
||||
MAIL_PORT: '465'
|
||||
MAIL_HOST: ${MAIL_HOST:?Set MAIL_HOST}
|
||||
MAIL_PORT: ${MAIL_PORT:?Set MAIL_PORT}
|
||||
MAIL_SECURE: 'true'
|
||||
MAIL_USER: 'mailbot@mifi.ventures'
|
||||
MAIL_FROM: 'mifi Holdings Shorty <noreply@mifi.holdings>'
|
||||
MAIL_PASSWORD: '${SMTP_PASSWORD:?Set SMTP_PASSWORD}'
|
||||
MAIL_USER: ${MAIL_USER:?Set MAIL_USER}
|
||||
MAIL_FROM: ${MAIL_FROM:?Set MAIL_FROM}
|
||||
MAIL_PASSWORD: '${MAIL_PASSWORD:?Set MAIL_PASSWORD}'
|
||||
NODE_ENV: production
|
||||
# OIDC_ENABLED: 'true'
|
||||
# OIDC_ISSUER: 'https://git.mifi.dev'
|
||||
@@ -90,18 +92,24 @@ services:
|
||||
REDIS_ENABLED: 'true'
|
||||
REDIS_HOST: kutt_redis
|
||||
REDIS_PORT: '6379'
|
||||
SITE_DOMAIN: link.mifi.me
|
||||
SITE_NAME: 'mifi Shorty'
|
||||
SERVER_CNAME_ADDRESS: ${SERVER_CNAME_ADDRESS:-${DEFAULT_DOMAIN}}
|
||||
SITE_NAME: ${SITE_NAME:-Kutt}
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'docker.network=marina-net'
|
||||
- 'traefik.http.routers.kutt-mifi.rule=Host(`mifi.me`)'
|
||||
- 'traefik.http.routers.kutt-mifi.rule=Host(`${DEFAULT_DOMAIN}`)'
|
||||
- 'traefik.http.routers.kutt-mifi.entrypoints=websecure'
|
||||
- 'traefik.http.routers.kutt-mifi.tls.certresolver=letsencrypt'
|
||||
- 'traefik.http.routers.kutt-mifi.service=kutt-short'
|
||||
- 'traefik.http.services.kutt-short.loadbalancer.server.port=3000'
|
||||
# Backend timeout: use transport from file provider (see traefik-kutt-timeout.example.yml).
|
||||
- 'traefik.http.services.kutt-short.loadbalancer.serversTransport=kutt-long-timeout@file'
|
||||
# UI
|
||||
- 'traefik.http.routers.kutt-mifi-ui.rule=Host(`${SERVER_CNAME_ADDRESS}`)'
|
||||
- 'traefik.http.routers.kutt-mifi-ui.entrypoints=websecure'
|
||||
- 'traefik.http.routers.kutt-mifi-ui.tls.certresolver=letsencrypt'
|
||||
- 'traefik.http.routers.kutt-mifi-ui.service=kutt-short-ui'
|
||||
- 'traefik.http.services.kutt-short-ui.loadbalancer.server.port=3000'
|
||||
- 'traefik.http.services.kutt-short-ui.loadbalancer.serversTransport=kutt-long-timeout@file'
|
||||
|
||||
qr_api:
|
||||
image: ${REGISTRY:-git.mifi.dev}/mifi-holdings/shorty-qr-api:${IMAGE_TAG:-latest}
|
||||
@@ -140,7 +148,7 @@ services:
|
||||
# Use service_started so the stack can deploy even if qr_api is still broken; switch to service_healthy once qr_api image is fixed.
|
||||
depends_on:
|
||||
qr_api:
|
||||
condition: service_started
|
||||
condition: service_healthy
|
||||
environment:
|
||||
QR_API_URL: http://qr_api:8080
|
||||
healthcheck:
|
||||
@@ -148,7 +156,7 @@ services:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- 'require("http").get("http://127.0.0.1:3000/", (r) => { r.resume(); process.exit(r.statusCode >= 200 && r.statusCode < 500 ? 0 : 1); }).on("error", () => process.exit(1))'
|
||||
- 'require("http").get("http://localhost:3000/", {timeout: 5000}, (r) => { r.resume(); process.exit(r.statusCode >= 200 && r.statusCode < 500 ? 0 : 1); }).on("error", () => process.exit(1))'
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -76,11 +76,7 @@ services:
|
||||
- 'traefik.http.routers.kutt-mifi.tls.certresolver=letsencrypt'
|
||||
- 'traefik.http.routers.kutt-mifi.service=kutt-short'
|
||||
- 'traefik.http.services.kutt-short.loadbalancer.server.port=3000'
|
||||
- 'traefik.http.routers.kutt-link.rule=Host(`link.mifi.me`)'
|
||||
- 'traefik.http.routers.kutt-link.entrypoints=websecure'
|
||||
- 'traefik.http.routers.kutt-link.tls.certresolver=letsencrypt'
|
||||
- 'traefik.http.routers.kutt-link.service=kutt'
|
||||
- 'traefik.http.services.kutt.loadbalancer.server.port=3000'
|
||||
- 'traefik.http.services.kutt-short.loadbalancer.serversTransport=kutt-long-timeout@file'
|
||||
|
||||
qr_api:
|
||||
build:
|
||||
@@ -128,10 +124,10 @@ services:
|
||||
QR_API_URL: http://qr_api:8080
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
'CMD-SHELL',
|
||||
'node -e "require(''http'').get(''http://127.0.0.1:3000/'', (r) => { r.resume(); process.exit(r.statusCode >= 200 && r.statusCode < 500 ? 0 : 1); }).on(''error'', () => process.exit(1))"',
|
||||
]
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- 'require("http").get("http://localhost:3000/", {timeout: 5000}, (r) => { r.resume(); process.exit(r.statusCode >= 200 && r.statusCode < 500 ? 0 : 1); }).on("error", () => process.exit(1))'
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Resetting the Kutt PostgreSQL password
|
||||
|
||||
If you lost the stack env vars (e.g. after deleting the Portainer stack) and need to use the existing database with a new password, you have two options.
|
||||
|
||||
**Default DB user/db name from compose:** `kutt` / `kutt` (or whatever you set in `DB_USER` / `DB_NAME`).
|
||||
**Data path on host:** `/mnt/config/docker/kutt/postgres` (from `docker-compose.portainer.yml`).
|
||||
|
||||
---
|
||||
|
||||
## Option A: Fresh start (delete all Kutt data)
|
||||
|
||||
Use this if you **don’t need** existing links/users.
|
||||
|
||||
1. On the host, remove the Postgres data directory:
|
||||
```bash
|
||||
sudo rm -rf /mnt/config/docker/kutt/postgres
|
||||
```
|
||||
2. In Portainer, (re-)create the stack and set env vars, including a new `DB_PASSWORD`.
|
||||
3. Deploy. Postgres will initialise a new database with the new password.
|
||||
|
||||
---
|
||||
|
||||
## Option B: Keep data, force-set a new password
|
||||
|
||||
Use this if you **want to keep** existing Kutt data but don’t know the current password.
|
||||
**Stop the stack first** (so nothing is using the Postgres data volume).
|
||||
|
||||
1. On the host, temporarily allow local connections without a password:
|
||||
```bash
|
||||
cd /mnt/config/docker/kutt/postgres
|
||||
cp pg_hba.conf pg_hba.conf.bak
|
||||
echo 'local all all trust' > pg_hba.conf
|
||||
echo 'host all all 127.0.0.1/32 trust' >> pg_hba.conf
|
||||
echo 'host all all ::1/128 trust' >> pg_hba.conf
|
||||
```
|
||||
|
||||
2. Start a temporary Postgres container (it will use the modified `pg_hba.conf`):
|
||||
```bash
|
||||
docker run -d --name pg-reset \
|
||||
-v /mnt/config/docker/kutt/postgres:/var/lib/postgresql/data \
|
||||
postgres:16-alpine
|
||||
sleep 5
|
||||
```
|
||||
|
||||
3. Set the new password (replace `kutt` if you use a different `DB_USER`, and set `YOUR_NEW_PASSWORD`):
|
||||
```bash
|
||||
docker exec pg-reset psql -U postgres -c "ALTER USER kutt PASSWORD 'YOUR_NEW_PASSWORD';"
|
||||
```
|
||||
|
||||
4. Stop the temporary container and restore `pg_hba.conf`:
|
||||
```bash
|
||||
docker stop pg-reset && docker rm pg-reset
|
||||
cd /mnt/config/docker/kutt/postgres
|
||||
mv pg_hba.conf.bak pg_hba.conf
|
||||
```
|
||||
|
||||
5. In Portainer, create/redeploy the stack and set `DB_PASSWORD` (and other env vars) to the same `YOUR_NEW_PASSWORD`.
|
||||
2
qr-api/.npmrc
Normal file
2
qr-api/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
# Force pnpm to run build scripts for better-sqlite3
|
||||
enable-pre-post-scripts=true
|
||||
@@ -1,25 +1,23 @@
|
||||
# Build stage: TypeScript only (no native deps in this stage).
|
||||
# Build stage: use npm (not pnpm) to avoid workspace issues with better-sqlite3.
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN pnpm install
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
# Verify better-sqlite3 was compiled; fail build if not.
|
||||
RUN test -f node_modules/better-sqlite3/build/Release/better_sqlite3.node || \
|
||||
(echo "ERROR: better-sqlite3.node not found after npm install" && exit 1)
|
||||
COPY . .
|
||||
RUN pnpm run build
|
||||
RUN npm run build
|
||||
RUN npm prune --production
|
||||
|
||||
# Runtime: install deps here so better-sqlite3 is compiled for this exact image/platform.
|
||||
# Runtime: copy pre-built node_modules from builder.
|
||||
FROM node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
# Install build deps needed to compile better-sqlite3; remove after install to keep image small.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN pnpm install --prod \
|
||||
&& apt-get purge -y python3 make g++ \
|
||||
&& apt-get autoremove -y --purge \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
EXPOSE 8080
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
||||
@@ -35,7 +35,10 @@ describe('shortenUrl', () => {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': 'test-key',
|
||||
},
|
||||
body: JSON.stringify({ target: 'https://example.com' }),
|
||||
body: JSON.stringify({
|
||||
target: 'https://example.com',
|
||||
domain: 'mifi.me',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -54,6 +57,7 @@ describe('shortenUrl', () => {
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
target: 'https://example.com',
|
||||
domain: 'mifi.me',
|
||||
customurl: 'myslug',
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -18,14 +18,23 @@ export async function shortenUrl(
|
||||
}
|
||||
|
||||
const base = env.KUTT_BASE_URL.replace(/\/$/, '');
|
||||
const apiKey = env.KUTT_API_KEY.trim();
|
||||
|
||||
// Extract domain from SHORT_DOMAIN (e.g., "https://mifi.me" -> "mifi.me")
|
||||
const domain = env.SHORT_DOMAIN.replace(/^https?:\/\//, '').replace(
|
||||
/\/$/,
|
||||
'',
|
||||
);
|
||||
|
||||
const res = await fetch(`${base}/api/v2/links`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': env.KUTT_API_KEY,
|
||||
'X-API-Key': apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
target: body.targetUrl,
|
||||
domain: domain,
|
||||
...(body.customSlug && { customurl: body.customSlug }),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/pnpm-lock.yaml* ./
|
||||
RUN pnpm install --prod
|
||||
|
||||
@@ -1020,19 +1020,7 @@ export function Editor({ id }: EditorProps) {
|
||||
updateProject({ recipeJson: JSON.stringify(r) });
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
label="Shape"
|
||||
data={[
|
||||
{ value: 'square', label: 'Square' },
|
||||
{ value: 'circle', label: 'Circle' },
|
||||
]}
|
||||
value={recipe.shape ?? 'square'}
|
||||
onChange={(v) => {
|
||||
const r = { ...recipe };
|
||||
r.shape = (v as 'square' | 'circle') ?? 'square';
|
||||
updateProject({ recipeJson: JSON.stringify(r) });
|
||||
}}
|
||||
/>
|
||||
{/* Shape select removed - circle shape has rendering issues in qr-code-styling library */}
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
label="Margin"
|
||||
|
||||
@@ -127,14 +127,13 @@ describe('buildQrStylingOptions', () => {
|
||||
).toEqual(g);
|
||||
});
|
||||
|
||||
it('uses imageOptions and shape from recipe', () => {
|
||||
it('uses imageOptions from recipe (shape always square)', () => {
|
||||
const opts = buildQrStylingOptions({
|
||||
imageOptions: {
|
||||
hideBackgroundDots: false,
|
||||
imageSize: 0.5,
|
||||
margin: 5,
|
||||
},
|
||||
shape: 'circle',
|
||||
});
|
||||
expect(
|
||||
(opts.imageOptions as { hideBackgroundDots: boolean })
|
||||
@@ -144,7 +143,8 @@ describe('buildQrStylingOptions', () => {
|
||||
0.5,
|
||||
);
|
||||
expect((opts.imageOptions as { margin: number }).margin).toBe(5);
|
||||
expect(opts.shape).toBe('circle');
|
||||
// shape is always 'square' (circle has rendering issues in qr-code-styling)
|
||||
expect(opts.shape).toBe('square');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export function buildQrStylingOptions(
|
||||
data: overrides.data ?? recipe.data ?? ' ',
|
||||
image: overrides.image,
|
||||
type: 'canvas',
|
||||
shape: recipe.shape ?? 'square',
|
||||
shape: 'square', // circle shape has rendering issues in qr-code-styling library
|
||||
margin: recipe.margin ?? 0,
|
||||
qrOptions: {
|
||||
type: 'canvas',
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface RecipeOptions {
|
||||
type?: string;
|
||||
gradient?: QrGradient;
|
||||
};
|
||||
shape?: 'square' | 'circle';
|
||||
// shape removed - circle shape has rendering issues in qr-code-styling library
|
||||
margin?: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user