summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL.md333
-rw-r--r--README.md30
-rw-r--r--README.rdoc4
-rw-r--r--doc/CUTOVER_2026.md (renamed from doc/INSTALL.md)9
-rw-r--r--lib/tasks/init.rake75
5 files changed, 446 insertions, 5 deletions
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644
index 00000000..cad9c9e1
--- /dev/null
+++ b/INSTALL.md
@@ -0,0 +1,333 @@
1# Installing CCCMS
2
3A Rails 8 application on PostgreSQL. ImageMagick 7 and Ghostscript are
4hard runtime dependencies: image variants, PDF thumbnails and social
5cards are shelled out to them. Production runs on FreeBSD behind nginx
6with Unicorn; development works anywhere the stack below does.
7
8For the historical record of the June 2026 migration from Rails 2, see
9`doc/CUTOVER_2026.md` — it is not an installation guide and is not
10maintained.
11
12## 1. Dependencies
13
14| What | Why | FreeBSD 14/15 | Debian/Ubuntu | macOS (brew) |
15|---|---|---|---|---|
16| PostgreSQL 16 | database | `postgresql16-server postgresql16-client` | `postgresql postgresql-client libpq-dev` | `postgresql@16` |
17| ImageMagick **7** | image variants, social cards | `ImageMagick7-nox11` | see trap below | `imagemagick` |
18| Ghostscript | PDF rasterisation | `ghostscript10` | `ghostscript` | `ghostscript` |
19| libyaml | psych | `libyaml` | `libyaml-dev` | `libyaml` |
20| libffi, readline, gdbm | Ruby build | `libffi readline gdbm` | `libffi-dev libreadline-dev libgdbm-dev` | (in base) |
21| libxml2, libxslt | libxml-ruby | `libxml2 libxslt` | `libxml2-dev libxslt1-dev` | `libxml2 libxslt` |
22| libical | recurrence expansion via the chaos_calendar gem | libical | libical-dev | libical |
23| GNU make | native gems | `gmake` | (default) | (default) |
24| Node | asset pipeline | `node` | `nodejs` | `node` |
25| git, curl, gnupg | fetching and verifying | `git curl gnupg` | `git curl gnupg` | (in base) |
26
27Debian trap: the `imagemagick` package is version 6 on Debian 12 and
28earlier, which has no `magick` binary, only the deprecated `convert`.
29The code calls `magick` at four sites in
30`app/models/concerns/file_attachment.rb`. Check with `magick -version`
31before going further; if it is absent, install from a backport or build
32ImageMagick 7.
33
34FreeBSD jail: PostgreSQL needs System V shared memory. On the host,
35in `/etc/jail.conf`:
36
37 allow.sysvipc = 1;
38
39Restart the jail. Without it PostgreSQL fails to start with a cryptic
40shared-memory error.
41
42On 14.x with libical 3.0.20+ the include path for libical is
43`<libical/ical.h>`, not `<ical.h>`, should the chaos_calendar Gem act
44up.
45
46## 2. Ruby and the gemset
47
48rvm is used for its gemsets, which work like Python venvs. Version
493.4.10.
50
51 curl -L https://github.com/rvm/rvm/releases/download/1.29.12/1.29.12.tar.gz \
52 -o /tmp/rvm.tar.gz
53 curl -L https://github.com/rvm/rvm/releases/download/1.29.12/1.29.12.tar.gz.asc \
54 -o /tmp/rvm.tar.gz.asc
55 gpg --keyserver hkps://keys.openpgp.org \
56 --recv-keys 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
57 gpg --verify /tmp/rvm.tar.gz.asc /tmp/rvm.tar.gz
58 tar -xzf /tmp/rvm.tar.gz -C /tmp
59 bash /tmp/rvm-1.29.12/install --auto-dotfiles
60 source /usr/local/rvm/scripts/rvm
61
62**rvm 1.29.12 is the current stable release and is years old. Its
63version list does not know about Ruby 3.4.** Replace it:
64
65 curl -L https://raw.githubusercontent.com/rvm/rvm/master/config/known \
66 -o /usr/local/rvm/config/known
67 rvm list known | sed -n '/# MRI/,/^$/p'
68 rvm install 3.4.10 --autolibs=read-only --with-opt-dir=/usr/local
69
70`--autolibs=read-only` stops rvm running the package manager on your
71behalf. `--with-opt-dir=/usr/local` is the libyaml fix: ports and brew
72install there, Ruby's configure does not look there, and without it
73psych fails to build **silently** and surfaces much later as YAML errors
74when Rails loads `database.yml`. Verify the build before continuing:
75
76 ruby -ryaml -ropenssl -rzlib -e 'puts "ok #{Psych::LIBYAML_VERSION}"'
77
78Then the gemset:
79
80 cd /path/to/cccms
81 rvm use 3.4.10@rails8-upgrade --create
82
83`.ruby-version` and `.ruby-gemset` in the project root make rvm switch
84automatically on entering the directory. `.ruby-version` must keep the
85`ruby-` prefix, `ruby-3.4.10`, not `3.4.10`, because the rc.d script
86concatenates it into a gemset path and a bare version yields a path that
87does not exist.
88
89## 3. Gems
90
91 gem install bundler
92 MAKE=gmake bundle install
93
94`MAKE=gmake` on FreeBSD only, and it is not optional: several native
95extensions fail against BSD make.
96
97## 4. Database
98
99 # FreeBSD
100 sysrc 'postgresql_enable="YES"'
101 service postgresql initdb
102 service postgresql start
103
104 psql -U postgres postgres
105
106```sql
107CREATE ROLE rails WITH LOGIN PASSWORD 'choose-one';
108ALTER ROLE rails CREATEDB;
109
110CREATE DATABASE cccms_dev OWNER rails ENCODING 'UTF8'
111 LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0;
112CREATE DATABASE psql_test OWNER rails ENCODING 'UTF8'
113 LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0;
114```
115
116`CREATEDB` is needed because the test suite creates and drops its own
117database. `TEMPLATE template0` is required whenever a non-default locale
118is given.
119
120Two config files are gitignored and must be created. `config/database.yml`:
121
122```yaml
123development:
124 adapter: postgresql
125 encoding: unicode
126 database: cccms_dev
127 pool: 5
128 username: rails
129 password: choose-one
130
131test:
132 adapter: postgresql
133 encoding: UTF8
134 database: psql_test
135 username: rails
136 password:
137
138production:
139 adapter: postgresql
140 encoding: unicode
141 database: cccms_production
142 pool: 5
143 username: rails
144 password: choose-one
145```
146
147`config/initializers/secret_token.rb`, one line:
148
149```ruby
150Cccms::Application.config.secret_key_base = "<64 hex chars, e.g. from `rails secret`>"
151```
152
153### 4a. Migrate. Never load the schema.
154
155 bundle exec rails db:migrate
156
157Do not run `db:setup` or `db:schema:load`.
158
159`db/schema.rb` is gitignored, and it could not be used even if it were
160present: the full-text `search_vector` column is maintained by a PostgreSQL
161trigger, and Ruby's schema format cannot express triggers. A schema-loaded
162database gets the column and its GIN index with nothing populating them,
163and site search then silently returns no results. Migrations are the only
164complete record of the structure.
165
166## 5. First start
167
168Compile the admin assets. The TinyMCE bundle lives in gitignored
169`public/assets/`:
170
171 bundle exec rails assets:precompile
172
173Bootstrap the content tree and one account:
174
175 ADMIN_PASS=choose-one bundle exec rake cccms:init
176
177`ADMIN_LOGIN` (default `admin`) and `ADMIN_EMAIL` are optional. A missing
178`ADMIN_PASS` aborts. The task creates root, the Trash, `home`,
179`/updates`, `/disclosure`, `/club/erfas`, `/club/chaostreffs` with
180placeholder titles, and is idempotent.
181
182Start the server:
183
184 bundle exec rails server -p 3000 -b 0.0.0.0
185
186`-b 0.0.0.0` is required inside a FreeBSD jail, where `localhost` does
187not resolve.
188
189`public/system/uploads/` starts empty. It is gitignored; on a fresh
190install there is nothing to copy.
191
192### The first admin needs two logins
193
194The bootstrap account is an administrator without a second factor, so
195it cannot yet create users, reset factors or deactivate accounts:
196administrative actions need a code entered within the last thirty
197minutes, and there is no password-only path. This is deliberate. To
198finish:
199
2001. sign in as the bootstrap account
2012. **Mein Konto** -> enable second factor, scan the QR code, confirm
2023. sign out, sign in again, entering the code
203
204Elevation is granted at that login and user management unlocks.
205
206## 6. Production on FreeBSD
207
208Unicorn, started by an rc.d script. Templates in `doc/`:
209
210 doc/unicorn.rb -> /usr/local/etc/unicorn.rb
211 doc/rc.d_cccms -> /usr/local/etc/rc.d/cccms
212
213The rc.d script reads `.ruby-version` and `.ruby-gemset` from the project
214directory to find the gemset — see the prefix note in §2.
215
216nginx proxies everything to Unicorn. Uploads need their own block:
217
218 location /system/uploads/ {
219 add_header Content-Security-Policy "sandbox" always;
220 add_header X-Content-Type-Options "nosniff" always;
221
222 proxy_pass http://127.0.0.1:9090;
223 proxy_set_header Host $host;
224 proxy_buffering off;
225 proxy_set_header X-Forwarded-Host $host;
226 proxy_set_header X-Forwarded-Proto $scheme;
227 }
228
229 location / {
230 proxy_pass http://127.0.0.1:9090/;
231 proxy_set_header Host $host;
232 proxy_buffering off;
233 proxy_set_header X-Forwarded-Host $host;
234 proxy_set_header X-Forwarded-Proto $scheme;
235 }
236
237- Note: No trailing slash on its `proxy_pass`. With one, nginx strips the
238 matched prefix and the backend 404s. The `location /` block gets away
239 with a trailing slash only because replacing `/` with `/` is a no-op.
240- The CSP is not optional. Uploaded files are served by Rails' static
241 file server, which bypasses the middleware that sets the application's
242 security headers. Without `sandbox`, an uploaded SVG opened directly is
243 a document that runs its own script, on the same origin as the site
244 and its admin sessions.
245- `add_header` in a location replaces inherited headers, so anything
246 set at server level must be repeated here.
247
248## 7. Maintenance
249
250### Deploy
251
252 service cccms stop && git pull && bundle exec rails db:migrate && service cccms start
253
254`bundle install` too when `Gemfile.lock` changed. Use `install over`
255`update`: the lockfile names exact versions and checksums, so the server
256gets what was tested. In development, `touch tmp/restart.txt` restarts a
257running server in place.
258
259Occurrences are regenerated yearly at service start. Recurring
260events are expanded into finite `occurrences` rows rather than computed
261per request. Range queries over 200+ recurring events would otherwise
262mean full RRULE expansion on every page load. The window is five years,
263which is chaos_calendar's expansion limit.
264
265The rc.d script's `start_postcmd` regenerates when
266`/var/db/cccms_occurrences_regenerated` is missing or older than 365
267days. Run at post-start, since it must not block the server coming up
268or run when startup failed.
269
270 service cccms regenerate_occurrences
271
272The yearly cadence is chosen to coincide with the reboot that follows an
273operating-system upgrade. Regeneration is expensive, and that is the
274natural point to pay for it.
275
276### Security updates
277
278 gem install bundler-audit # once, outside the Gemfile
279 bundle-audit check --update
280
281Worth running monthly. Vulnerabilities in the HTML sanitizer matter most
282here: every page body passes through it.
283
284Ruby upgrades: a new gemset rather than a replacement, so the old one
285remains as the way back. Install and populate the new gemset before
286pulling a commit that changes `.ruby-version`, or every `rake` and
287`runner` invocation breaks while the running server carries on under the
288old Ruby.
289
290### One-shot tasks
291
292- `users:clear_otp` is the lockout escape hatch: it clears one account's
293 second factor from the shell when every administrator is locked out.
294 Deliberately unwitnessed — there is no actor to attribute a shell
295 command to.
296
297Logs are in `log/`, gitignored. The action log inside the application at
298`/admin/log` records who changed what; `log/production.log` records
299everything else.
300
301## 8. Traps
302
303- ImageMagick's policy travels with the project.
304 `config/imagemagick/policy.xml` is loaded via `MAGICK_CONFIGURE_PATH`,
305 set per invocation. Nothing to install, and do not patch the system
306 `policy.xml` or a port upgrade would revert it and a fresh checkout
307 would not have it. ImageMagick prepends the project path, so the
308 system file is still read.
309- Two independent allowlists govern editor HTML. TinyMCE's
310 `extended_valid_elements` in `public/javascripts/admin_interface.js`
311 and the server's sanitizer in `ContentHelper#aggregate?`. An attribute
312 permitted by one and not the other is either offered and discarded, or
313 stripped from markup the application itself emits. They must be
314 changed together.
315- `otp_required` is `false` on every account. Second factors are
316 effectively opt-in until that is flipped, and flipping it locks out
317 anyone who has not enrolled.
318- Uploads are not in the repository. `public/system/` is gitignored
319 and is not covered by a database dump either. Back it up separately or
320 the site loses every image.
321- The test database is not sandboxed against `rails runner`. A `runner`
322 invocation that writes will leave rows behind. Wrap writes in a
323 transaction with `raise ActiveRecord::Rollback`, or run
324 `RAILS_ENV=test bundle exec rails db:test:prepare` afterwards.
325- Ruby 3.4 bundled gems are fatal under bundler. A `require` of a
326 gem that is bundled-but-not-default warns outside bundler and raises
327 `LoadError` under `bundle exec`. `csv` is already declared for this
328 reason; the same applies to `base64`, `bigdecimal` and friends if a
329 future `require` reaches for one.
330
331## Tests
332
333 bundle exec rake test
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..9dc81727
--- /dev/null
+++ b/README.md
@@ -0,0 +1,30 @@
1# CCCMS
2
3The content management system behind [www.ccc.de](https://www.ccc.de).
4
5A Rails application with a nested-tree content model, per-node revision
6history, translated content via Globalize, and a witnessed action log.
7Editing is deliberately open: any editor may draft anywhere, and only
8changes that reach the RSS feeds are gated on a role.
9
10## Stack
11
12Ruby 3.4, Rails 8.1, PostgreSQL 16, ImageMagick 7 with Ghostscript.
13Production runs on FreeBSD behind nginx with Unicorn; development runs
14anywhere the above are available.
15
16## Documentation
17
18- `INSTALL.md` — setting up from scratch, and maintaining an existing
19 installation
20- `CONTRIBUTING.md` — conventions this codebase follows, and why
21- `doc/CUTOVER_2026.md` — historical record of the June 2026 migration
22 from Rails 2 to Rails 8. Not maintained.
23
24## Repositories
25
26- https://codeberg.org/erdgeist/cccms
27- git://erdgeist.org/cccms
28
29Public content is CC-licensed per page; see the site itself. The code is
30beerware. Original code credits to https://github.com/hukl/cccms
diff --git a/README.rdoc b/README.rdoc
deleted file mode 100644
index 5adf6893..00000000
--- a/README.rdoc
+++ /dev/null
@@ -1,4 +0,0 @@
1=CCCMS
2
3This is the repository for the CCCMS. Its a simple content management system
4inspired all the good parts of different other simple content management systems.
diff --git a/doc/INSTALL.md b/doc/CUTOVER_2026.md
index 8056f7cf..2a6cb22a 100644
--- a/doc/INSTALL.md
+++ b/doc/CUTOVER_2026.md
@@ -1,4 +1,11 @@
1# CCCMS Installation Guide 1# Rails 2 to Rails 8 cutover, June 2026
2
3A historical record of one migration onto a fresh FreeBSD jail. NOT an
4installation guide and no longer maintained: branch names, Ruby and
5gemset versions, migration stamps and expected test counts are all
6stale. See INSTALL.md for setting the project up.
7
8## CCCMS Installation Guide
2 9
3This document covers the non-obvious steps required to install the CCCMS 10This document covers the non-obvious steps required to install the CCCMS
4stack on a fresh FreeBSD jail. It assumes a FreeBSD 14.x base jail with 11stack on a fresh FreeBSD jail. It assumes a FreeBSD 14.x base jail with
diff --git a/lib/tasks/init.rake b/lib/tasks/init.rake
new file mode 100644
index 00000000..7e3d8dcc
--- /dev/null
+++ b/lib/tasks/init.rake
@@ -0,0 +1,75 @@
1namespace :cccms do
2 desc "Bootstrap a fresh installation: the node skeleton and one admin " \
3 "account. Idempotent -- every step finds before it creates, so " \
4 "re-running after a new step is added is safe. " \
5 "Requires ADMIN_PASS. ADMIN_LOGIN and ADMIN_EMAIL are optional. " \
6 "The admin is created without the role and promoted with " \
7 "update_column, because admin_needs_second_factor refuses a NEW " \
8 "admin without an enrolled factor -- it exempts retention, not " \
9 "creation. The account therefore cannot do user management until " \
10 "it enrols a second factor and signs in again; see INSTALL.md."
11 task :init => :environment do
12 password = ENV["ADMIN_PASS"].to_s
13 abort "usage: ADMIN_PASS=secret bundle exec rake cccms:init" if password.empty?
14 abort "ADMIN_PASS must be at least 6 characters" if password.length < 6
15
16 login = ENV.fetch("ADMIN_LOGIN", "admin")
17 email = ENV.fetch("ADMIN_EMAIL", "admin@example.org")
18
19 # publish_draft! is called with no user, which guard_live_change! treats
20 # as a trusted system context -- the documented nil-user path, and the
21 # reason a rake task can publish into /updates and /disclosure at all.
22 ensure_node = lambda do |parent, slug, title, body|
23 existing = parent ? parent.children.find_by(:slug => slug) : Node.root
24 if existing
25 puts format(" %-14s exists (%d)", slug || "root", existing.id)
26 next existing
27 end
28
29 node = parent ? parent.children.create!(:slug => slug) : Node.create!
30 Globalize.with_locale(I18n.default_locale) do
31 node.draft.update!(:title => title, :body => body.to_s)
32 end
33 node.publish_draft!
34 puts format(" %-14s created (%d)", slug || "root", node.id)
35 node
36 end
37
38 puts "Node skeleton:"
39 root = ensure_node.(nil, nil, "CCC", "")
40
41 # Referencing it is enough: Node.trash self-creates on first call.
42 puts format(" %-14s ready (%d)", "trash", Node.trash.id)
43
44 ensure_node.(root, "home", "Startseite", "")
45
46 ensure_node.(root, "updates", "Updates",
47 '[aggregate tags="update" limit="30" order_by="published_at" order_direction="DESC"]')
48
49 ensure_node.(root, "disclosure", "Disclosure", "")
50
51 club = ensure_node.(root, "club", "Chaos Computer Club", "")
52 ensure_node.(club, "erfas", "Erfa-Kreise",
53 '[aggregate children="direct" order_by="slug" partial="chapter"]')
54 ensure_node.(club, "chaostreffs", "Chaostreffs",
55 '[aggregate children="direct" order_by="slug" partial="chapter"]')
56
57 puts
58 if User.any?
59 puts "Accounts exist already; skipping admin creation."
60 else
61 user = User.create!(:login => login, :email => email,
62 :password => password,
63 :password_confirmation => password)
64 user.update_column(:roles, %w[admin redaktion])
65 puts "Created #{user.login} <#{user.email}> as admin + redaktion."
66 puts
67 puts "This account has no second factor, so it cannot yet create"
68 puts "users, reset factors or deactivate accounts. To finish:"
69 puts " 1. sign in as #{user.login}"
70 puts " 2. Mein Konto -> enable second factor, scan the QR, confirm"
71 puts " 3. sign out and sign in again, entering the code"
72 puts "Elevation is granted at that login and user management unlocks."
73 end
74 end
75end