- Go 59.3%
- templ 27.8%
- CSS 6.8%
- JavaScript 5.3%
- Just 0.5%
- Other 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .vscode | ||
| .zed | ||
| controllers | ||
| frontend | ||
| helpers | ||
| i18n | ||
| internal/sessions | ||
| mails | ||
| middlewares | ||
| migrations | ||
| models | ||
| static/images | ||
| templates | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| build-frontend.js | ||
| bun.lock | ||
| compose.dev.yml | ||
| compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| justfile | ||
| LICENSE | ||
| main.go | ||
| package.json | ||
| README.md | ||
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.