No description
  • Go 59.3%
  • templ 27.8%
  • CSS 6.8%
  • JavaScript 5.3%
  • Just 0.5%
  • Other 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-07 09:15:57 +01:00
.vscode Editors: fix indentation for project 2026-10-31 14:57:48 +01:00
.zed Editors: fix indentation for project 2026-10-31 14:57:48 +01:00
controllers Add default security groups when enabling IT accounts 2026-11-07 00:07:11 +01:00
frontend Refine user avatar with deterministic name-based colors 2026-11-07 00:07:35 +01:00
helpers Unify application access with LLDAP member accounts and configurable MFA 2026-11-06 22:40:19 +01:00
i18n Add default security groups when enabling IT accounts 2026-11-07 00:07:11 +01:00
internal/sessions Improve Go code readability and documentation 2026-11-06 13:32:40 +01:00
mails Replace Authelia sync with LLDAP invitations 2026-11-05 19:02:56 +01:00
middlewares Unify application access with LLDAP member accounts and configurable MFA 2026-11-06 22:40:19 +01:00
migrations Add default security groups when enabling IT accounts 2026-11-07 00:07:11 +01:00
models Add default security groups when enabling IT accounts 2026-11-07 00:07:11 +01:00
static/images Replace favicon with comrades icon 2026-11-05 20:01:20 +01:00
templates Refine user avatar with deterministic name-based colors 2026-11-07 00:07:35 +01:00
.dockerignore Replace pnpm and Parcel with Bun 2026-11-05 09:40:30 +01:00
.env.example Unify application access with LLDAP member accounts and configurable MFA 2026-11-06 22:40:19 +01:00
.gitignore Track generated templates and align templ CLI with module version 2026-11-06 21:38:03 +01:00
AGENTS.md Improve Go code readability and documentation 2026-11-06 13:32:40 +01:00
build-frontend.js Replace pnpm and Parcel with Bun 2026-11-05 09:40:30 +01:00
bun.lock Trust Parcel watcher dependency 2026-11-07 09:01:55 +01:00
compose.dev.yml Improve development commands and service readiness 2026-11-07 09:15:57 +01:00
compose.yml Improve development commands and service readiness 2026-11-07 09:15:57 +01:00
Dockerfile Store revocable sessions in PostgreSQL 2026-11-05 19:47:32 +01:00
go.mod Store revocable sessions in PostgreSQL 2026-11-05 19:47:32 +01:00
go.sum Add LLDAP services and directory client 2026-11-05 19:02:33 +01:00
justfile Improve development commands and service readiness 2026-11-07 09:15:57 +01:00
LICENSE License project under GPLv3 2025-03-29 12:44:18 +01:00
main.go Unify application access with LLDAP member accounts and configurable MFA 2026-11-06 22:40:19 +01:00
package.json Trust Parcel watcher dependency 2026-11-07 09:01:55 +01:00
README.md Improve development commands and service readiness 2026-11-07 09:15:57 +01:00

Camarades (Comrades)

Membership and sympathizer management for PST-POP/PDA + JPOP/KJ.

Requirements

  • Go (+ GOPATH and $GOPATH/bin in PATH)
  • Just
  • Docker or Podman (for PostgreSQL and LLDAP, and to build production containers)
  • Bun (to install dependencies and build frontend assets)

How to work with dev environment

# Install deps (when cloning for the first time)
just install

# Update deps (go + Bun)
just update

# Run project
just run

# Watch project (restart when changes are detected; install or update Air as needed)
just watch

# Build to project
just build

just watch checks for the latest Air release each time it starts. It requires network access and installs updates with go install; ensure GOBIN (or GOPATH/bin when unset) is in PATH.

Framework

This project is written in Go and is based on Fiber v3.

Database

The only supported database is Postgres.
The project uses GORM to access data and Gormigrate to manage schema changes.

Migrations run at startup and are recorded in the migrations table. The initial migration captures the schema that existed before Gormigrate; on an existing database it applies the same GORM migration once before recording it. Its model definitions in migrations/initial are a historical snapshot and must not be changed when application models evolve.

For every subsequent model change, append a migration with a unique, ordered ID in migrations/migrations.go. Define any migration-specific model structures locally or use explicit schema operations; do not refer to the changing application models. Migrations run in a PostgreSQL transaction and errors prevent startup. The initial migration has no rollback, to avoid dropping existing data.

Audit log

A GORM callback writes an audit log for every write to the tables listed in helpers/audit/audit.go.
Each write must carry its author: use db.WithContext(c) in a request handler, or db.WithContext(audit.System(ctx)) in code run without a user. A write without an author fails.
An update or a delete must target one loaded row, such as db.Delete(&person). A write by condition, such as db.Delete(&models.FieldValue{}, "person_id = ?", id), fails, because it gives the callback no row to diff.

Templates

This project uses Templ templates.
Templates are type-safe and are compiled into Go code before the project builds.

Commit both the .templ sources and the generated *_templ.go files. Run just templ after editing templates. This checks the CLI on PATH, installs the exact Templ module version from go.mod with go install when needed, and regenerates the Go files. Add GOBIN (or GOPATH/bin when unset) to PATH. just build also runs this step automatically. Use just check_generated in CI to check that generated templates are committed and up to date.

Translation

Translation is done in Weblate.
You can authenticate with a login.popvaud.ch or forge.popvaud.ch account.
If you do not have access to the Weblate project, ask maintainer.
Translation files are created by Weblate and pushed directly to the git repo.

To create a new string, add a new key in the i18n/sources/en.toml file.
Source language is English.

Config

Config is done with a .env file at the project root.
When cloning the project for the first time, copy .env.example into .env.

Key Description
DEBUG Show debug logs and enable hidden endpoints
MFA_MODE TOTP policy: disabled, optional, or required (default)
APP_BASE_URL Public base URL of the application, used in account invitations
APP_LISTEN_ADDRESS Address on which fiber will listen
APP_LISTEN_PORT Port on which fiber will listen
APP_BEHIND_PROXY Is fiber behind a reverse proxy
POSTGRES_HOST Postgres host (localhost for dev, database for prod)
POSTGRES_PORT Postgres port (usually 5432)
POSTGRES_USER Postgres username
POSTGRES_PASSWORD Postgres password
POSTGRES_DB Postgres database name
SESSION_JWT_SECRET Persistent, random session signing secret (at least 32 bytes)
LLDAP_URL LLDAP HTTP URL (http://localhost:17170 for dev; Docker production uses http://lldap:17170)
LLDAP_LDAP_URL LDAP URL (ldap://localhost:3890 for dev; Docker production uses ldap://lldap:3890)
LLDAP_LDAP_USER_DN LLDAP administrator username (defaults to lldap-admin)
LLDAP_LDAP_USER_PASS LLDAP administrator password
LLDAP_LDAP_BASE_DN LLDAP base DN
LLDAP_JWT_SECRET, LLDAP_KEY_SEED Persistent LLDAP secrets
MAIL_DISABLED Set to true to disable email delivery (default: false)
MAIL_HOST Address of the mail server
MAIL_PORT Port of the mail server
MAIL_USERNAME Username of the account to send emails
MAIL_PASSWORD Password of the account to send emails
MAIL_FROM_NAME Name of the email sender
MAIL_FROM_ADDRESS Email address of the account to send emails

Email delivery

Invitations are queued in PostgreSQL and sent in the background. Failed deliveries are retried after 30 seconds, for up to five attempts in total; sent and permanently failed rows remain in mail_queue without the message content. With MAIL_DISABLED=true, the worker pauses, new invitations cannot be requested, and automatic invitations wait until email is enabled again. Queued messages resume when the application restarts with email enabled. Delivery is at least once: a crash after SMTP accepts a message but before the database commits can result in a duplicate.

Sessions

Sessions are stored in PostgreSQL. Set SESSION_JWT_SECRET to a unique, random secret of at least 32 bytes and keep it stable across restarts and application instances. Changing it invalidates all sessions. Migrating from the disk-based session store signs everyone out, including remembered sessions. Administrators and users can revoke active sessions from their respective session menus.

LLDAP

Both Compose configurations start LLDAP with persistent storage in the lldap_data volume. LDAP is bound to localhost on port 3890; the web port 17170 is exposed only by the development configuration. Set LLDAP_LDAP_BASE_DN, LLDAP_LDAP_USER_PASS, LLDAP_JWT_SECRET and LLDAP_KEY_SEED in .env before the first start; use independent random secrets. Keep the key seed and volume backed up: changing the seed invalidates passwords. The initial directory administrator password is only applied when LLDAP is first initialized. Use LLDAP_LDAP_USER_DN=lldap-admin for that technical account so the login admin remains available to the Camarades member administrator.

Camarades reconciles accounts, security groups and section groups at startup and once per minute. The account-id user attribute contains the Camarades account UUID. Only marked directory entries are managed; name collisions with unmarked entries stop the sync and are logged. The application and LLDAP must share a private network, or use HTTPS and LDAPS. LLDAP allows users to edit their own built-in profile fields through its HTTP API. Restrict HTTP access to trusted administrators and the application; network isolation is not a per-user ACL. Except for the initial Camarades administrator, initial passwords are generated randomly, set via LDAP and discarded. The application emails a single-use invitation link valid for 30 days. Its random token is stored as a hash in the invitation record; the queued message temporarily contains the activation link until it is sent or exhausts its retries. Administrators can send a new invitation from the member's account page; this revokes the old link. Configure APP_BASE_URL to the externally reachable HTTPS URL in production and ensure reverse proxies do not log /invitations/ paths. The optional post-setup redirect is configured in the application settings.

LLDAP has no native account disable operation. Disabling or deleting an account therefore deletes its managed LLDAP user to revoke access. Re-enabling it creates a new directory user with a new random password and sends a new reset invitation. Changing a username also recreates the directory entry, invalidates its password and sends a new invitation. The form warns the administrator before applying the change. Other applications must not rely on LLDAP's own generated UUID remaining the same; use account-id instead. Existing Authelia file passwords cannot be migrated to LLDAP, so users must reset them. Back up the Camarades database and LLDAP data together.

First login

An initial member administrator is created when no IT account has application access. Sign in with admin@camarades.local (username admin) and password camarades after the directory synchronization completes. This account receives no welcome email. Change its password from account settings after signing in.

Deploy

TODO: In the process of building docker images for easy deploy.

  • Reverse proxy
  • App server (stage 1 -> build go, stage 2 -> run binary)
  • Database (postgres)
  • LDAP server (lldap)
  • Authentik

Security groups and migration

Security groups are managed in the administration area and are available to members of their section and its subsections. Groups created before sections were introduced initially have no section; an administrator must assign one. When a member is saved, memberships in groups without a section or outside the member's new section are removed. Changing a group's section also removes memberships that are no longer compatible.

Active group names and technical names must be unique, regardless of case. Resolve any existing duplicates before upgrading; otherwise, creating the unique indexes prevents the application from starting. Technical names may contain ASCII letters and digits, periods, hyphens and underscores, and must start with a letter or digit.

Authentication and access

Members authenticate with their IT account username or email and their LLDAP password. Only enabled accounts belonging to a security group linked to an application role can sign in. Each regular role links a security group to a section; its permissions include descendant sections. The protected administrator role applies globally and uses the protected camarades_admin group. Both display names default to Administrators and can be renamed by administrators; their identity, scope, security settings, and protection against deletion remain fixed. Only administrators can assign membership in that group. The last active administrator account cannot be disabled, removed, archived, or stripped of its administrator group.

MFA_MODE accepts disabled, optional, or required (the default). In optional mode, users can enroll TOTP from their account settings; enrolled accounts must verify TOTP at login. In disabled mode, existing secrets are retained but TOTP is bypassed.

An empty installation creates a section named Root and the member Admin Admin with IT username admin, email admin@camarades.local, and initial LLDAP password camarades. Provisioning runs through the normal directory synchronization worker and sends no welcome email for this account. Change this initial password in account settings after signing in.

The access migration matches legacy application users to active member IT accounts by email. Only administrator permissions are carried over; regular roles must be configured again. Ambiguous email matches are never granted administrator access. If no administrator can be mapped, startup creates the initial member administrator described above. Existing sessions are revoked. The users and user_roles database views expose account information to existing read-only screens; there is no separate application user table or local password store.

The bootstrap username migration changes the previous initial account aadmin to admin. Directory synchronization recreates that bootstrap account with the initial password camarades and does not send a welcome email. Other member accounts are unaffected.