Skip to content
LapikudPublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

138 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Proliige

Proliige ("pro member") is Lapikud's membership app, for working your way to becoming a member. People do club tasks, submit proof, and earn points when a reviewer approves it. Everyone can follow the feed of completed tasks, the leaderboard, and each person's page.

Built with Next.js, PostgreSQL (Drizzle), pg-boss background jobs, FreeIPA sign-in, Garage photo storage, Tailwind and shadcn/ui.

SPEC.md describes what the app does and the rules it keeps; this README is about working on the code.

Getting started

You need Node 22.12+, pnpm and Docker.

pnpm install
cp .env.example .env
docker compose up -d postgres
pnpm garage:setup      # writes garage.toml, starts Garage, prints a key for .env; run again after filling it
pnpm db:migrate
pnpm dev               # http://localhost:3000

.env.example lists every setting; set SITE_URL to the public address so link previews and the sitemap point at it. The app refuses to start while one is missing or invalid, and says which. .env and garage.toml hold secrets, so both are ignored by git and never archived; their templates are .env.example and garage.example.toml.

Signing in locally

To try the app without FreeIPA, set AUTH_DRIVER=seed in .env and run pnpm db:seed. Every seed account signs in with the password seed. Seed sign-in is refused in production, and pnpm db:seed refuses non-local databases unless SEED_CONFIRM=1 is set.

Account Role
seed-admin-1, seed-admin-2 Admin
seed-pixel-1 to seed-pixel-8 Pixel
seed-member-1, seed-member-2 Member
seed-excluded Pixel left out of the leaderboard and feed
seed-no-group No group

Everyday commands

Command What it does
pnpm dev Run the app with hot reload
pnpm test Run all tests (uses its own lapikud_test database)
pnpm typecheck Check types
pnpm lint Check code rules
pnpm format Format everything with Prettier
pnpm build Production build
pnpm db:generate <name> Write a named migration after changing a model.ts
pnpm db:migrate Apply migrations; stops if a model.ts change has no migration
pnpm db:check Check that every model.ts change has a migration
pnpm db:seed Fill a local database with seed accounts, tasks and posts
pnpm garage:setup [app-origin] Prepare Garage: config, layout, bucket, key, CORS

Before opening a pull request, run pnpm format && pnpm lint && pnpm typecheck && pnpm test. CI runs the same checks, plus a build, on every pull request.

Glossary

The same words are used in the code, the database and the screens.

Word Meaning
User Anyone who signed in with a FreeIPA account
Role What a user may do, derived from their FreeIPA groups: member, pixel or admin
Member A user in the members group; likes, comments and suggests tasks
Pixel A user in the pixels group; does tasks, and can do everything a member can
Admin A user in an admin group (team_juhatus); reviews proofs and manages tasks, never submits
Task Something to do for the club, worth points, in a category
Suggestion A task a member or pixel proposes; it stays off the task list until an admin publishes it
Own task A task a pixel adds for work they already did, sent with its proof; a reviewer sets points
Policy How often a task can be done: once per user, repeatable, first one wins
Proof What a pixel submits to show they did a task, often with photos
Reviewer An admin asked to review a proof, like a reviewer on a pull request
Review Approving or rejecting a proof; any admin may review any proof
Points Awarded when a proof is approved or a contribution is recognized, in the points ledger
Recognition Points an admin gives a pixel for a contribution with no task or proof, shown in the feed; admins can edit it, remove it from the feed or take the points back
Badge A reusable label, with an optional image, that an admin can attach to a recognition
Feed Approved proofs and recognitions, newest first; proofs have likes and comments
Report A signed-in user flags a post on the feed with a reason; admins see it on the review page
Unapprove An admin takes back an approved proof: it leaves the feed and its points are reversed
Leaderboard Users ranked by points, all time or this month
Rule A yes/no check of what a user may do, such as userCanSubmitProof

Who can do what:

Who Can
Visitor Read the feed, the leaderboard and user pages
Signed in, no group Also like, comment, report posts, and get notified of replies
Member Also suggest tasks
Pixel Also browse tasks, submit proofs, request reviewers and add own tasks
Admin Review proofs, manage tasks and categories, recognize pixels, remove or unapprove posts; never submits proofs

A proof with requested reviewers notifies them; without any, nobody is notified and it simply waits in the review queue. Groups are set in src/config/access.ts. Every rule lives in src/domain/rules.ts.

How the code is organised

Code depends inwards only: app and components use infra, infra implements core, and core builds on domain.

src/
  domain/       Plain types and pure functions: what a proof, task or rule is. No database, no React.
  core/
    ports/      Interfaces the services need, such as ProofRepository or PushPort.
    services/   One per resource. Each method is wrapped in guard(rule, …), so permissions are never skipped.
  infra/        Implementations of the ports: Drizzle repositories, Garage storage, FreeIPA, Web Push, the job queue.
    index.ts    Builds every adapter and service; the only thing app code imports from infra.
  app/          Next.js routes only. Pages are grouped by who they are for: (public), (member),
                (account), admin/. The root holds Next's special files (layout, errors, metadata).
  components/   UI grouped by feature (feed/, proof/, review/, leaderboard/, profile/…); ui/ holds the basics.
  actions/      Server actions: what forms submit, one folder per resource.
  lib/          Small shared helpers: server actions, forms, navigation, dates, logging.
  config/       Settings in code: site name and colours, access groups, photo limits, feed paging.
tests/
  domain/       Pure tests of domain logic and rules.
  integration/  Services against a real database.

Following one request

Submitting a proof, from click to database:

  1. components/proof/form.tsx validates the form with actions/proof/schema.ts and calls submitProofAction.
  2. actions/proof/index.ts wraps the call in action(), which validates again on the server and finds the signed-in user.
  3. core/services/proof.ts submitProof is guarded by userCanSubmitProof, checks the task's policy, and saves through ProofRepository.
  4. infra/db/proof/submit.ts writes the proof, its photos and requested reviewers in one transaction.
  5. The service notifies the requested reviewers through NotificationPort, which queues a Web Push for each of their browsers.

Adding a feature

The user page (/users/[userId]) is a small, complete example:

  1. Rule: userCanViewProfiles in domain/rules.ts.
  2. Port: the data it needs, here the feed's authorId filter in core/ports/feed.ts.
  3. Infra: the query in infra/db/feed/repo.ts.
  4. Service: core/services/user.ts, registered in infra/index.ts.
  5. Page and components: app/(public)/users/[userId]/page.tsx and components/profile/.
  6. Tests: rules in tests/domain/, queries in tests/integration/.

Conventions

Files

  • Name .ts and .tsx files in camelCase: photoCarousel.tsx.
  • Don't repeat the folder in the file name: feed/card.tsx, not feed/feedCard.tsx.
  • Next.js files and route folders keep Next's names: page.tsx, not-found.tsx, [userId]/.
  • Other files keep their usual names: lapikud-logo.svg, setup.sh.
  • Write code in TypeScript. The exceptions are public/serviceWorker.js and the shell scripts in scripts/.

Code

  • Check permissions with a rule from domain/rules.ts. Never check roles directly.
  • Comment only what someone could otherwise break by accident.
  • Let Prettier format the code. It also sorts Tailwind classes.
  • Don't edit components/ui/chart.tsx by hand; update it with npx shadcn add chart.

Configuration notes

Photos

Photos are uploaded and served through the app: the browser posts each file to POST /api/photos/upload, which validates it and stores it in Garage over the server's own connection. Garage stays private and never needs to be reachable from the browser, so no public endpoint or bucket CORS is required. Approved photos are served through the app to everyone; pending or rejected ones only to their owner and admins.

Notifications

Notifications are stored in PostgreSQL and delivered live over /api/notifications/stream. Allow long-lived connections and turn off proxy buffering for that path.

Web Push delivers them with the site closed. Generate keys with npx web-push generate-vapid-keys and set NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY and VAPID_SUBJECT, or leave all three empty to turn push off. People turn desktop notifications on from the notifications page; signing out turns them off for that browser.

Background jobs

Slow or unreliable work runs as jobs in pg-boss, a queue stored in the same PostgreSQL database under the pgboss schema, so there is nothing extra to run. Workers start with the server (src/instrumentation.ts calls startWorkers()). Failed jobs are retried five times with growing delays.

There are two queues: push-delivery (one job per browser per notification, five sent at a time) and photo-backup (the weekly backup below, scheduled with pg-boss). To add a job, follow src/infra/push/webPush.ts or src/infra/backup/schedule.ts.

Logging

Server errors are written as JSON to stdout and, in development, to logs/app.log. Set LOG_FILE for a durable path and LOG_LEVEL to debug, info, warn or error. Secrets, cookies and tokens are redacted.

Weekly photo backups

Photos can grow large, so every Sunday at 03:00 (Europe/Tallinn) a background job copies new proof photos from Garage to Nextcloud over WebDAV, under NEXTCLOUD_BACKUP_PATH with the same paths as in the bucket. Each photo is copied once; the photo_backups table records what is already there. Set NEXTCLOUD_WEBDAV_URL, NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD to turn it on.

Deploying

Every push to main is checked again and, if it passes, pushed to git.lapikud.ee, where a runner inside the club's LXC container deploys it. The container builds the new version next to the running one, applies migrations, switches over and restarts the app. If the new version doesn't come up, it goes back to the previous one.

A rollback only undoes code, and migrations run while the previous version is still live, so every migration must also work with the code before it. To remove or rename a column, stop using it in one pull request and drop it in the next.

docs/deploy.md explains how deploys work and how to set up the server, Gitea and GitHub.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages