diff --git a/examples/blog/app/controllers/posts_controller.rb b/examples/blog/app/controllers/posts_controller.rb index 6a1dc02..2618e9c 100644 --- a/examples/blog/app/controllers/posts_controller.rb +++ b/examples/blog/app/controllers/posts_controller.rb @@ -287,4 +287,65 @@ class PostsController < ApplicationController flash[:notice] = t('posts.restored_notice', id: post.id) redirect url_for(:trash_posts) rescue Prism::Orm::RecordNotFound + respond_not_found(t('posts.not_found')) + end + + # `published!`/`draft!` come straight from Post's `enum :status` (see the + # model) - they set the column and save() in one call, so this action is + # nothing but "find, authorize like an edit, flip the status". + def publish + post = Post.find_by!(slug: params['slug']) + authorize(post, :update?) + post.published! + flash[:notice] = t('posts.published_notice', id: post.id) + redirect url_for(:post, slug: post.to_param) + rescue Prism::Orm::RecordNotFound + respond_not_found(t('posts.not_found')) + end + + def unpublish + post = Post.find_by!(slug: params['slug']) + authorize(post, :update?) + post.draft! + flash[:notice] = t('posts.unpublished_notice', id: post.id) + redirect url_for(:post, slug: post.to_param) + rescue Prism::Orm::RecordNotFound + respond_not_found(t('posts.not_found')) + end + + # Post#acts_as_list is scoped by `featured` (see the model) - moving a + # post that ISN'T currently featured is harmless (it just reorders it + # inside the unused non-featured group) but pointless, so both actions + # redirect back to the SAME featured=1 view the reorder links only ever + # appear on, regardless of which group the post actually belongs to. + def move_up + post = Post.find_by!(slug: params['slug']) + authorize(post, :update?) + post.move_higher + redirect "#{url_for(:posts)}?featured=1" + rescue Prism::Orm::RecordNotFound + respond_not_found(t('posts.not_found')) + end + + def move_down + post = Post.find_by!(slug: params['slug']) + authorize(post, :update?) + post.move_lower + redirect "#{url_for(:posts)}?featured=1" + rescue Prism::Orm::RecordNotFound + respond_not_found(t('posts.not_found')) + end + + private + + def store_cover(upload) + return nil unless upload.is_a?(Prism::UploadedFile) + return nil if upload.original_filename.to_s.empty? + + uploads_dir = File.join(APP_ROOT, 'public', 'uploads') + FileUtils.mkdir_p(uploads_dir) + safe_name = "#{Time.now.strftime('%Y%m%d%H%M%S')}-#{upload.original_filename.gsub(/[^a-zA-Z0-9_.-]/, '_')}" + File.binwrite(File.join(uploads_dir, safe_name), upload.read) + safe_name + end end diff --git a/examples/blog/app/controllers/session_controller.rb b/examples/blog/app/controllers/session_controller.rb new file mode 100644 index 0000000..d56be9e --- /dev/null +++ b/examples/blog/app/controllers/session_controller.rb @@ -0,0 +1,47 @@ +class SessionController < ApplicationController + def new + render(:new, locals: { errors: [] }) + end + + def create + credentials = params.require('user').permit(:email, :password, :remember_me).to_h + user = User.find_by(email: credentials['email']) + + if user && user.authenticate(credentials['password']) + sign_in(user, remember: credentials['remember_me'] == '1') + respond_to do |format| + format.html do + flash[:notice] = t('sessions.signed_in_notice') + redirect after_authentication_url + end + format.json { json(user.as_json(methods: [:email])) } + end + else + respond_to do |format| + format.html do + status(422) + render(:new, locals: { errors: [t('sessions.invalid_credentials')] }) + end + # 401, not 422 like the HTML branch above: there is no resource + # here whose validation failed (no User object at all when the + # email does not match anyone), just a rejected authentication + # attempt - the JSON:API status a client should actually branch on. + format.json do + json({ errors: [jsonapi_error(status: 401, pointer: '/data/attributes/password', + title: 'invalid credentials', detail: t('sessions.invalid_credentials'))] }, 401) + end + end + end + end + + def destroy + sign_out + respond_to do |format| + format.html do + flash[:notice] = t('sessions.signed_out_notice') + redirect url_for(:root) + end + format.json { json({ message: t('sessions.signed_out_notice') }) } + end + end +end diff --git a/examples/blog/app/controllers/users_controller.rb b/examples/blog/app/controllers/users_controller.rb new file mode 100644 index 0000000..22616eb --- /dev/null +++ b/examples/blog/app/controllers/users_controller.rb @@ -0,0 +1,47 @@ +class UsersController < ApplicationController + def new + render(:new, locals: { user: User.new, errors: [] }) + end + + def create + attrs = params.require('user').permit(:email, :password, :password_confirmation).to_h + user = User.new(attrs) + + if user.save + sign_in(user) + respond_to do |format| + format.html do + flash[:notice] = t('users.signed_up_notice') + redirect url_for(:root) + end + format.json { json(user.as_json(methods: [:email]), 201) } + end + else + respond_to do |format| + format.html do + status(422) + render(:new, locals: { user: user, errors: user.errors.full_messages }) + end + format.json { render_errors(user) } + end + end + # The unique index on users.email (see db/migrate) is a storage-level + # constraint, not just the app-level `validates :email, uniqueness: true` + # above - it still fires for anything that reaches Store#insert without + # going through Model#valid? first (a racing concurrent signup that read + # the table before the other one committed, a future admin script, a bug + # elsewhere that skips validation). #create can't tell those apart from a + # normal duplicate, so it reports the same friendly message either way. + rescue Prism::Orm::Store::UniqueConstraintViolation + respond_to do |format| + format.html do + status(422) + render(:new, locals: { user: user, errors: [t('users.email_taken_notice')] }) + end + format.json do + json({ errors: [jsonapi_error(status: 422, pointer: '/data/attributes/email', + title: 'is taken', detail: t('users.email_taken_notice'))] }, 422) + end + end + end +end