Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
15dbbef
chore: remove obsolete deployment scripts
mohamedaleya Dec 31, 2025
fbe9c96
feat: implement redis caching, async analytics queue, and shared post…
mohamedaleya Dec 31, 2025
3b2f80b
perf: optimize CI/CD deployment time with parallelization and pre-bui…
mohamedaleya Dec 31, 2025
04d13f1
fix: correct shell parsing in db_provision command
mohamedaleya Dec 31, 2025
98e5abf
fix: use external script for db provisioning to avoid shell escaping …
mohamedaleya Dec 31, 2025
a5f9fec
fix: use wget for healthcheck (curl not in Alpine image)
mohamedaleya Dec 31, 2025
1ec976f
fix(deploy): use node.js fetch for healthcheck to fix unhealthy statu…
mohamedaleya Dec 31, 2025
fd56e3b
fix: handle encrypted links redirect in /s/[id] route
mohamedaleya Dec 31, 2025
6b8d1f8
fix: optimize docker image and fix link details page
mohamedaleya Dec 31, 2025
debaaac
chore: optimize Dockerfile and fix prisma migration path
mohamedaleya Dec 31, 2025
1b0491d
fix: copy @prisma/engines to runner stage
mohamedaleya Dec 31, 2025
7eccc2b
chore: revert prisma bundling, allow runtime download for resilience
mohamedaleya Dec 31, 2025
0ee339d
feat: implement email verification, update branding, and optimize build
mohamedaleya Dec 31, 2025
44ed2d9
feat: implement email verification and fix input styles
mohamedaleya Dec 31, 2025
144875c
fix: resolve build and lint errors
mohamedaleya Dec 31, 2025
4fc3f8b
feat: fix SMTP auth and add email resend functionality
mohamedaleya Dec 31, 2025
474512f
feat(auth): fix SMTP, implement email resend, and unify auth UI
mohamedaleya Dec 31, 2025
f098af8
feat(auth): fix verification redirects and enhance unverified user ex…
mohamedaleya Dec 31, 2025
c89371b
docs: update CHANGELOG and README for v0.1.0-alpha.2
mohamedaleya Dec 31, 2025
b9d18e1
feat: add dedicated email verification success page and redirect
mohamedaleya Dec 31, 2025
c6bff2d
feat: improve SEO metadata, refine UI consistency, and fix link editi…
mohamedaleya Dec 31, 2025
b62a059
chore: upgrade to Next.js 16.1.1 and React 19
mohamedaleya Dec 31, 2025
dfd6550
chore: migrate project from npm to Bun
mohamedaleya Dec 31, 2025
7f27ef8
fix: migrate to modern flat config and resolve react 19 lint errors
mohamedaleya Dec 31, 2025
87dabff
build: fix Docker build by pinning Bun to v1.3.2
mohamedaleya Jan 1, 2026
4fbf501
fix(auth): redirect verified users away from email verification pages
mohamedaleya Jan 1, 2026
4e80674
fix(lint): fix react-hooks/exhaustive-deps lint error and enhance pre…
mohamedaleya Jan 1, 2026
7d21217
perf: Add dynamic imports for recharts and react-markdown
mohamedaleya Jan 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
node_modules
npm-debug.log

# Package manager lockfiles (bun.lockb is intentionally NOT ignored)
package-lock.json
yarn.lock
pnpm-lock.yaml

# Next.js build output
.next
out
Expand Down Expand Up @@ -38,6 +43,10 @@ docker-deploy.sh
# Misc
README.md
*.md
.agent
.gemini
.husky
coverage
tests
__tests__
*.test.*
Expand Down
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ VPS_IP=your_vps_ip
# UploadThing (for avatar uploads) - SDK v7+
UPLOADTHING_TOKEN=your_uploadthing_token

# Authentication Providers
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CLIENT_SECRET=your_google_client_secret

# Email Configuration (Direct SMTP)
SMTP_HOST=pro1.mail.ovh.net
SMTP_PORT=587
SMTP_USER=contact@linkops.at
SMTP_PASS=your_email_password
SMTP_FROM="LinkOps <contact@linkops.at>"
101 changes: 66 additions & 35 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,36 @@ env:
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
# Job 1: Lint and format check (runs in parallel with build)
lint-and-check:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: "20"
cache: "npm"
bun-version: latest

- name: Install dependencies
run: npm ci
run: bun install --frozen-lockfile

- name: Check formatting
run: npx prettier --check "src/**/*.{ts,tsx,js,jsx,css}"
run: bunx prettier --check "src/**/*.{ts,tsx,js,jsx,css}"

- name: Run lint
run: npm run lint
run: bun run lint

# Job 2: Build and push Docker images (runs in parallel with lint)
build-images:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand All @@ -60,9 +67,11 @@ jobs:
echo "EOF"
} >> $GITHUB_OUTPUT
echo "NEXT_PUBLIC_URL=${{ secrets.NEXT_PUBLIC_URL }}" >> $GITHUB_OUTPUT
echo "CRON_TAG=latest" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
echo "TAGS=type=raw,value=staging" >> $GITHUB_OUTPUT
echo "NEXT_PUBLIC_URL=https://staging.linkops.at" >> $GITHUB_OUTPUT
echo "CRON_TAG=staging" >> $GITHUB_OUTPUT
fi

- name: Extract metadata for Docker
Expand All @@ -72,7 +81,8 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ steps.meta-config.outputs.TAGS }}

- name: Build and push Docker image
# Build main app image
- name: Build and push App image
uses: docker/build-push-action@v5
with:
context: .
Expand All @@ -85,8 +95,20 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

# Build cron image (avoids building on VPS)
- name: Build and push Cron image
uses: docker/build-push-action@v5
with:
context: ./cron
push: true
platforms: linux/amd64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cron:${{ steps.meta-config.outputs.CRON_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max

# Job 3: Deploy to VPS (waits for both lint and build)
deploy-to-vps:
needs: build-and-push
needs: [lint-and-check, build-images]
runs-on: ubuntu-latest
env:
FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
Expand All @@ -99,46 +121,37 @@ jobs:
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "COMPOSE_FILE=docker-compose.prod.yml" >> $GITHUB_OUTPUT
echo "REMOTE_COMPOSE_FILE=docker-compose.prod.yml" >> $GITHUB_OUTPUT
echo "ENV_FILE=.env.prod" >> $GITHUB_OUTPUT
echo "TAG=latest" >> $GITHUB_OUTPUT
echo "WEB_CONTAINER=linkops-web" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
echo "COMPOSE_FILE=docker-compose.staging.yml" >> $GITHUB_OUTPUT
echo "REMOTE_COMPOSE_FILE=docker-compose.staging.yml" >> $GITHUB_OUTPUT
echo "ENV_FILE=.env.staging" >> $GITHUB_OUTPUT
echo "TAG=staging" >> $GITHUB_OUTPUT
echo "WEB_CONTAINER=linkops-staging-web" >> $GITHUB_OUTPUT
fi

- name: Create Env File
env:
PROD_ENV: ${{ secrets.PRODUCTION_ENV }}
STAGE_ENV: ${{ secrets.STAGING_ENV }}
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "${{ secrets.PRODUCTION_ENV }}" > ${{ steps.deploy-config.outputs.ENV_FILE }}
echo "$PROD_ENV" > ${{ steps.deploy-config.outputs.ENV_FILE }}
elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
echo "${{ secrets.STAGING_ENV }}" > ${{ steps.deploy-config.outputs.ENV_FILE }}
echo "$STAGE_ENV" > ${{ steps.deploy-config.outputs.ENV_FILE }}
fi

- name: Copy Config and Env to VPS
- name: Copy Config, Env, and Scripts to VPS
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
source: "${{ steps.deploy-config.outputs.COMPOSE_FILE }},${{ steps.deploy-config.outputs.ENV_FILE }}"
source: "${{ steps.deploy-config.outputs.COMPOSE_FILE }},${{ steps.deploy-config.outputs.ENV_FILE }},scripts/"
target: "~/linkops/"
strip_components: 0

- name: Rename/Prepare docker-compose on VPS
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
# Ensure correct naming on remote if different
if [ "${{ steps.deploy-config.outputs.COMPOSE_FILE }}" != "${{ steps.deploy-config.outputs.REMOTE_COMPOSE_FILE }}" ]; then
mv ~/linkops/${{ steps.deploy-config.outputs.COMPOSE_FILE }} ~/linkops/${{ steps.deploy-config.outputs.REMOTE_COMPOSE_FILE }} 2>/dev/null || true
fi

- name: Deploy to VPS
uses: appleboy/ssh-action@v1.0.3
with:
Expand All @@ -151,14 +164,32 @@ jobs:

cd ~/linkops

# Pull the new image
# Make init script executable
chmod +x scripts/init-db.sh

# Pull the new images
docker pull ghcr.io/${{ github.repository }}:${{ steps.deploy-config.outputs.TAG }}
docker pull ghcr.io/${{ github.repository }}-cron:${{ steps.deploy-config.outputs.TAG }}

# Run database provisioning (idempotent - safe to run every time)
docker compose -f ${{ steps.deploy-config.outputs.COMPOSE_FILE }} --env-file ${{ steps.deploy-config.outputs.ENV_FILE }} up db_provision

# Start all containers (no --build needed, images are pre-built)
docker compose -f ${{ steps.deploy-config.outputs.COMPOSE_FILE }} --env-file ${{ steps.deploy-config.outputs.ENV_FILE }} up -d --force-recreate web redis cron

# Restart containers
docker compose -f ${{ steps.deploy-config.outputs.REMOTE_COMPOSE_FILE }} --env-file ${{ steps.deploy-config.outputs.ENV_FILE }} up -d --force-recreate
# Wait for web container to be ready (healthcheck)
echo "Waiting for web container to be healthy..."
for i in {1..15}; do
if docker exec ${{ steps.deploy-config.outputs.WEB_CONTAINER }} curl -sf http://localhost:3000/api/health > /dev/null 2>&1; then
echo "Web container is healthy!"
break
fi
echo "Waiting... ($i/15)"
sleep 1
done

# Run database migrations
docker compose -f ${{ steps.deploy-config.outputs.REMOTE_COMPOSE_FILE }} --env-file ${{ steps.deploy-config.outputs.ENV_FILE }} exec -T web npx prisma@6.19.0 migrate deploy
docker exec -t ${{ steps.deploy-config.outputs.WEB_CONTAINER }} bunx --bun prisma@6.19.0 migrate deploy

# Clean up old images
docker image prune -f
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

All notable changes to this project will be documented in this file.

## [0.1.0-alpha.2] - 2025-12-31 - Email Verification & Polish

- **Email Verification System**: Complete email verification flow for new user registrations.
- Verification emails sent via SMTP (Nodemailer integration).
- Dedicated verification pages with token handling.
- Verification banner prompting unverified users to confirm their email.
- **Password Reset Flow**: Full forgot/reset password functionality.
- Request password reset via email.
- Secure token-based password reset page.
- Beautifully designed email templates.
- **Alpha Banner**: Eye-catching marquee banner indicating the app is in alpha stage.
- **Improved Logo**: New reusable SVG logo component with theme support.
- **Cookie Consent**: GDPR-compliant cookie consent banner.

### Infrastructure & DevOps

- **Docker Optimizations**: Reduced Docker image size and improved build times.
- **CI/CD Improvements**:
- Explicit project naming for staging/production isolation.
- Fixed database provisioning scripts.
- Improved migration reliability with health checks.
- **Environment Isolation**: Staging and production now run on completely separate Docker networks and databases.

### Bug Fixes & Polish

- **Focus Ring Fix**: Eliminated white flash on input focus across all components.
- **Session Stability**: Prevented unnecessary session refreshes on window focus changes.
- **Improved Skeleton Loading**: Better loading states for session-dependent UI elements.

## [0.1.0-alpha.1] - 2025-12-30 - Security & Feature Update

- **Unified Dashboard**: Renamed `/links` to `/dashboard` and integrated real-time analytics directly above your link list for at-a-glance insights.
Expand Down
53 changes: 30 additions & 23 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,54 +1,61 @@
FROM node:20-alpine AS base
# syntax=docker/dockerfile:1.4
FROM oven/bun:1-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
COPY package.json bun.lockb* ./
COPY prisma ./prisma/

# Use BuildKit cache mount for faster dependency installs
RUN --mount=type=cache,target=/root/.bun/install/cache \
bun install --frozen-lockfile

RUN npm ci

FROM base AS builder
FROM oven/bun:1.3.2-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN npx prisma generate
RUN bunx --bun prisma generate

# Build argument for NEXT_PUBLIC_URL (baked into client bundle at build time)
ARG NEXT_PUBLIC_URL
ENV NEXT_PUBLIC_URL=$NEXT_PUBLIC_URL
ENV NEXT_TELEMETRY_DISABLED=1

# Use BuildKit cache mount for Next.js build cache
RUN --mount=type=cache,target=/app/.next/cache \
bun run build


RUN npm run build

FROM base AS runner
FROM oven/bun:1.3.2-alpine AS runner
WORKDIR /app
RUN apk add --no-cache curl libc6-compat

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# In bun images, the user is 'bun' (uid 1000)
# We'll stick with the standard bun user for simplicity
RUN mkdir .next && chown bun:bun .next

COPY --from=builder /app/package.json ./package.json

RUN mkdir .next
RUN chown nextjs:nodejs .next

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/.next/server ./.next/server

COPY --from=builder --chown=bun:bun /app/.next/standalone ./
COPY --from=builder --chown=bun:bun /app/.next/static ./.next/static
COPY --from=builder --chown=bun:bun /app/public ./public

COPY --chown=nextjs:nodejs prisma ./prisma/
COPY --chown=bun:bun prisma ./prisma/
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/@prisma/client ./node_modules/@prisma/client

USER nextjs
USER bun

EXPOSE 3066
ENV PORT 3000
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

ENV HOSTNAME "0.0.0.0"
CMD ["bun", "server.js"]

CMD ["node", "server.js"]
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ LinkOps is a professional, high-performance URL management platform designed for
- **Safety Warning**: Interstitial warning for potentially unsafe links.
- **Expiration Dates**: Set links to automatically deactivate after a certain period.
- **Enable/Disable**: Instantly toggle link availability without deleting.
- **βœ‰οΈ Email Verification**:
- **Account Verification**: Email-based verification for new user registrations.
- **Password Reset**: Secure password reset flow with token-based email verification.
- **SMTP Integration**: Reliable email delivery via Nodemailer.
- **πŸ“„ Informational Pages**:
- **Features**: Detailed overview of platform capabilities.
- **Contact**: Direct line of communication for users.
Expand All @@ -32,6 +36,7 @@ LinkOps is a professional, high-performance URL management platform designed for
- **Better-Auth Integration**: Secure authentication via credentials or OAuth (Google, GitHub).
- **Advanced Profile Management**: Custom usernames, display names, and profile picture management with integrated cropping.
- **Responsive & Dynamic UI**: Built with Shadcn UI, Framer Motion, and support for Dark Mode.
- **Cookie Consent**: GDPR-compliant cookie banner with customizable preferences.

## πŸ—οΈ Architecture & Tech Stack

Expand Down Expand Up @@ -83,7 +88,7 @@ LinkOps is built with a focus on scalability, performance, and developer experie
2. **Environment Configuration**:
Create a `.env` file based on `.env.example`. Ensure you provide valid credentials for PostgreSQL, Redis, and Better-Auth.

> **Note**: For staging environment, use `.env.staging` and the `staging` branch.
> **Note**: For staging environment, use `.env.staging` and the `dev` branch.

3. **Database Initialization**:

Expand All @@ -109,7 +114,7 @@ This builds the production image for `linux/amd64`, pushes it to GitHub Containe

### Staging Environment

The project maintains a `dev` branch for staging. Pushes to this branch trigger a deployment to the staging environment, accessible via port `3071`.
The project maintains a `dev` branch for staging. Pushes to this branch trigger a deployment to the staging environment.

## 🀝 Contributing

Expand Down
Binary file added bun.lockb
Binary file not shown.
11 changes: 11 additions & 0 deletions cron/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM alpine:3.19

RUN apk add --no-cache curl gettext

COPY analytics-crontab /etc/cron.d/analytics-crontab
RUN chmod 0644 /etc/cron.d/analytics-crontab

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

CMD ["/entrypoint.sh"]
2 changes: 2 additions & 0 deletions cron/analytics-crontab
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Run analytics worker every minute
* * * * * curl -s -X POST -H "Authorization: Bearer ${ANALYTICS_WORKER_SECRET}" http://${WEB_HOST:-web}:3000/api/process-analytics >> /var/log/cron.log 2>&1
Loading