Give ownerless lists to the first user
Lists from before users existed go to the first user to sign in, and
user_id is required from here on, so lists are private without
exception. Deploy only after that first sign-in: the migration fails if
there are ownerless lists and no users.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG4iASZnHb1yEM1JAASiWn
diff --git a/AGENTS.md b/AGENTS.md
index 8de48bb..789910c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,7 +85,7 @@ Tests set `DATABASE_URL=:memory:` in `test/test_helper.rb`.
- Views are Phlex components under `lib/rank_king/views/`
- CSRF protection is on for all POST routes
- Every request needs a `Remote-User` header or it gets a 401. That is safe only because a proxy (Tailscale identity) writes the header. `StubLogin` fills it in for `demo.ru` (dev server, Fly apps) and nowhere else: never in `config.ru`, and never in front of real data.
-- Lists are private to their owner (404 for anyone else); lists from before users existed have no owner and are visible to everyone
+- Lists are private to their owner (404 for anyone else)
- Config is a frozen `Data.define` value object
- Types are inline RBS comments (`#:` before a method, `# @rbs` for the
rest), checked by `rake steep`. `sig/` holds only what that syntax
diff --git a/db/migrate/003_assign_list_owners.rb b/db/migrate/003_assign_list_owners.rb
new file mode 100644
index 0000000..75212f2
--- /dev/null
+++ b/db/migrate/003_assign_list_owners.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Lists from before users existed go to the first user, the one who signed in
+# when the user migration was first deployed, and every list has an owner
+# from here on. Run it only after that first sign-in: with no users yet, it
+# fails rather than leave lists without one.
+Sequel.migration do
+ up do
+ unowned = from(:lists).where(user_id: nil)
+ unless unowned.empty?
+ first_user = from(:users).min(:id)
+ raise "no users to own the #{unowned.count} lists without an owner" unless first_user
+
+ unowned.update(user_id: first_user)
+ end
+
+ alter_table(:lists) do
+ set_column_not_null :user_id
+ end
+ end
+
+ down do
+ alter_table(:lists) do
+ set_column_allow_null :user_id
+ end
+ end
+end
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index 48fda45..104d57d 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -24,7 +24,7 @@ module RankKing
user = User.for_login(ProxyAuth.login(r.env))
r.root do
- lists = List.where(user_id: [user.id, nil]).order(:id).all
+ lists = List.where(user_id: user.id).order(:id).all
Views::Home.new(lists: lists, csrf: method(:csrf_token)).call
end
@@ -55,9 +55,8 @@ module RankKing
end
r.on Integer do |list_id|
- # Lists are private to their owner; ones from before users
- # existed have none and stay visible to everyone.
- @list = List.where(id: list_id, user_id: [user.id, nil]).first
+ # Lists are private to their owner.
+ @list = List.where(id: list_id, user_id: user.id).first
r.halt 404 unless @list
r.is do
diff --git a/sig/rank_king/models.rbs b/sig/rank_king/models.rbs
index 7497348..cf17012 100644
--- a/sig/rank_king/models.rbs
+++ b/sig/rank_king/models.rbs
@@ -9,8 +9,8 @@ module RankKing
end
class List < Sequel::Model
- def user_id: () -> Integer?
- def user: () -> User?
+ def user_id: () -> Integer
+ def user: () -> User
def name: () -> String
def items: () -> Array[Item]
def axes: () -> Array[Axis]