The app becomes a Rails 8 application; the Vue frontend moves to app/frontend and is served through vite_rails. - Without an account documents stay in the browser (IndexedDB). Sign-in is by magic link or the 6-digit code from the same mail; afterwards local documents can be uploaded into the account. - Server documents (/d/<id>) with versioned saves and a single-editor lock per browser tab; others see them read-only and follow by polling. - Sharing via link (works without an account) and via e-mail invitation, roles viewer/editor; the owner manages links and people. - Store gains a read-only mode that blocks all content changes. - Solid Queue, Cache and Cable share the one PostgreSQL database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
2.6 KiB
Ruby
72 lines
2.6 KiB
Ruby
class CreateCoreTables < ActiveRecord::Migration[8.1]
|
|
def change
|
|
enable_extension "citext"
|
|
enable_extension "pgcrypto"
|
|
|
|
create_table :users do |t|
|
|
t.citext :email, null: false, index: { unique: true }
|
|
t.string :display_name
|
|
t.string :color
|
|
t.datetime :last_seen_at
|
|
t.timestamps
|
|
end
|
|
|
|
create_table :sessions do |t|
|
|
t.references :user, null: false, foreign_key: { on_delete: :cascade }
|
|
t.string :user_agent
|
|
t.string :ip_address
|
|
t.datetime :last_active_at
|
|
t.timestamps
|
|
end
|
|
|
|
create_table :documents, id: :uuid do |t|
|
|
t.references :owner, null: false, foreign_key: { to_table: :users, on_delete: :cascade }
|
|
t.string :title, null: false
|
|
t.text :content, null: false # the DocumentState JSON, same as inside a .num file
|
|
t.integer :version, null: false, default: 1
|
|
t.references :updated_by, foreign_key: { to_table: :users, on_delete: :nullify }
|
|
t.datetime :content_updated_at, null: false
|
|
# simple edit lock until live collaboration exists (concept, phase 3)
|
|
t.string :lock_holder
|
|
t.string :lock_holder_name
|
|
t.datetime :lock_expires_at
|
|
t.timestamps
|
|
end
|
|
|
|
create_table :share_links do |t|
|
|
t.references :document, type: :uuid, null: false, foreign_key: { on_delete: :cascade }
|
|
t.integer :role, null: false
|
|
t.string :token, null: false, index: { unique: true }
|
|
t.references :created_by, null: false, foreign_key: { to_table: :users, on_delete: :cascade }
|
|
t.datetime :revoked_at
|
|
t.timestamps
|
|
end
|
|
|
|
create_table :document_memberships do |t|
|
|
t.references :document, type: :uuid, null: false, foreign_key: { on_delete: :cascade }
|
|
t.references :user, null: false, foreign_key: { on_delete: :cascade }
|
|
t.integer :role, null: false
|
|
t.integer :source, null: false
|
|
t.references :share_link, foreign_key: { on_delete: :cascade }
|
|
t.datetime :last_opened_at
|
|
t.timestamps
|
|
t.index [ :document_id, :user_id ], unique: true
|
|
end
|
|
|
|
create_table :magic_links do |t|
|
|
t.citext :email, null: false, index: true
|
|
t.string :token_digest, null: false, index: { unique: true }
|
|
t.string :code_digest, null: false
|
|
t.integer :purpose, null: false, default: 0
|
|
t.references :document, type: :uuid, foreign_key: { on_delete: :cascade }
|
|
t.integer :role
|
|
t.references :invited_by, foreign_key: { to_table: :users, on_delete: :cascade }
|
|
t.string :return_to
|
|
t.integer :failed_attempts, null: false, default: 0
|
|
t.datetime :expires_at, null: false
|
|
t.datetime :used_at
|
|
t.timestamps
|
|
end
|
|
end
|
|
end
|