feat(models): add User and confirmed?

This commit is contained in:
dualfroz committed 2026-09-04 17:45:42 +02:00
1 parent 3fa9e92acc
commit 5025bb8d9c
1 file changed
+69
+69
View File
@@ -0,0 +1,69 @@
class User < Prism::Orm::Model
has_secure_password
has_many :login_sessions, dependent: :destroy
# No dependent: - there is no user-deletion flow, so nothing ever destroys
# the owner side of this association; used read-only by AccountController#
# comments ("my comments" across every post, indexed on comments.user_id).
has_many :comments
# Same read-only shape as :comments above, used only by AccountController
# #moderation to find which posts belong to the current user before
# looking up pending comments on them (indexed on posts.user_id - see
# db/migrate/20260907000009_add_index_on_posts_user_id.rb).
has_many :posts
# email is PII, so it is encrypted at rest (deterministic: true so
# find_by(email:)/uniqueness can still match on plaintext - see
# Prism::Orm::Encryption). Validators read the attribute's own reader
# method (Validatable#public_send), so they see the decrypted plaintext
# here exactly like they would for a plain column.
encrypts :email, deterministic: true
validates :email, presence: true, uniqueness: true, format: { with: /\A[^@\s]+@[^@\s]+\z/ }
# Backs PasswordResetsController's forgot-password flow (session 99's
# Prism::Orm::TokenFor). The fingerprint block snapshots password_digest
# at token-mint time, so a token becomes worthless the moment ANY save
# changes the digest - the successful reset that consumes it, but also a
# second concurrent reset request or a manual password change elsewhere -
# without a separate "used" column or token table to track that by hand.
generates_token_for(:password_reset, expires_in: 900) { password_digest }
# Backs EmailConfirmationsController. Fingerprinted on email (not
# password_digest like the reset token above, and not confirmed_at
# either) - so a link minted for one address stops verifying the moment
# that address changes (see reset_confirmation_if_email_changed below),
# but stays valid across repeat visits: confirming twice with the same
# link is harmless (#show is a no-op once confirmed_at is already set),
# unlike a password reset token, which self-invalidates on first use
# because using it always changes the fingerprint it was minted under.
# No expires_in - an email that sits unread for a week should still
# confirm when it is finally opened, unlike a reset link nobody would
# still want to click that late.
generates_token_for(:email_confirmation) { email }
def confirmed?
!confirmed_at.nil?
end
# Devise-style "reconfirmable": changing to a new address un-confirms the
# account rather than silently keeping the old confirmation around for an
# email nobody has proven they control yet. Only fires on an update (see
# `before_update`, not `before_save`) - on #create confirmed_at is already
# nil from the migration's default, so there is nothing to reset.
before_update :reset_confirmation_if_email_changed
# Same after_commit(on: :create) shape as Post's new-post notification -
# queued only once the sign-up has actually survived to disk, so a
# rolled-back create (e.g. a validation failure surfacing after this
# hook in some future callback) never sends a welcome/confirmation email
# for an account that does not exist.
after_commit(on: :create) { NotificationMailer.welcome(id).deliver_later }
after_commit(on: :create) do
NotificationMailer.confirmation_instructions(id, generate_token_for(:email_confirmation)).deliver_later
end
private
def reset_confirmation_if_email_changed
self.confirmed_at = nil if attribute_changed?('email')
end
end