feat: extend renderable with csrf and csrf_token
This commit is contained in:
1 parent
b9fc8a09d2
commit
721d576956
3 files changed
+181
-4
No files matched your search
@@ -13,8 +13,23 @@ module Prism
|
||||
end
|
||||
|
||||
def cookies
|
||||
@cookie_jar ||= begin
|
||||
secret = @env['prism.secret']
|
||||
if secret
|
||||
CookieJar.new(@request, @response, secret: secret, previous_secrets: @env['prism.previous_secrets'] || [])
|
||||
else
|
||||
@request.cookies
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def csrf
|
||||
@csrf ||= Csrf.new(session)
|
||||
end
|
||||
|
||||
def csrf_token
|
||||
csrf.masked_token
|
||||
end
|
||||
|
||||
def status(code = nil)
|
||||
@response.status = code if code
|
||||
@@ -34,6 +49,21 @@ module Prism
|
||||
payload
|
||||
end
|
||||
|
||||
# JSON:API-lite error response for a failed-validation record: `{
|
||||
# "errors": [...] }` with each entry built by Orm::Errors#to_jsonapi.
|
||||
# Mirrors `render_errors(post)` -> `format.json` the same way `render`
|
||||
# covers the HTML branch of a respond_to block.
|
||||
def render_errors(record, status: 422)
|
||||
json({ errors: record.errors.to_jsonapi(status: status) }, status)
|
||||
end
|
||||
|
||||
# JSON:API-lite "not found" error, the 404 counterpart to render_errors.
|
||||
# No `source.pointer` (unlike a validation error, a missing resource
|
||||
# does not point at one attribute of it).
|
||||
def render_not_found(detail, status: 404)
|
||||
json({ errors: [{ status: status.to_s, title: 'Resource Not Found', detail: detail }] }, status)
|
||||
end
|
||||
|
||||
def html(markup, code = nil)
|
||||
@response.status = code if code
|
||||
@response['Content-Type'] = 'text/html;charset=utf-8'
|
||||
@@ -41,6 +71,15 @@ module Prism
|
||||
markup
|
||||
end
|
||||
|
||||
# ActionController::Live-lite: `stream { |out| out.write(row) }` sends
|
||||
# the response as HTTP/1.1 chunked Transfer-Encoding instead of
|
||||
# buffering the whole body first - see Prism::StreamingBody and
|
||||
# Server#write_response. Nothing inside the block runs here; it only
|
||||
# runs once the server starts sending the response (see Response#stream).
|
||||
def stream(&block)
|
||||
@response.stream(&block)
|
||||
end
|
||||
|
||||
def redirect(location, code = 302)
|
||||
@response.status = code
|
||||
@response['Location'] = location
|
||||
@@ -73,5 +112,36 @@ module Prism
|
||||
def finalize_flash
|
||||
@flash.sweep if @flash
|
||||
end
|
||||
|
||||
# Locale for this request: an explicit ?locale= param wins, then a
|
||||
# sticky value stashed in the session, then the client's
|
||||
# Accept-Language header, then Prism::I18n.default_locale.
|
||||
def locale
|
||||
@locale ||= detect_locale
|
||||
end
|
||||
|
||||
def translate(key, **options)
|
||||
Prism::I18n.t(key, locale: options.delete(:locale) || locale, **options)
|
||||
end
|
||||
alias_method :t, :translate
|
||||
|
||||
def localize(value, **options)
|
||||
Prism::I18n.l(value, locale: options.delete(:locale) || locale, **options)
|
||||
end
|
||||
alias_method :l, :localize
|
||||
|
||||
private
|
||||
|
||||
def detect_locale
|
||||
requested = params['locale'] || session[:locale]
|
||||
return requested.to_sym if requested && available_locale?(requested)
|
||||
|
||||
negotiated = Prism::I18n.best_locale_from_accept(@request.headers['Accept-Language'])
|
||||
negotiated || Prism::I18n.default_locale
|
||||
end
|
||||
|
||||
def available_locale?(candidate)
|
||||
Prism::I18n.available_locales.map(&:to_s).include?(candidate.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
+34
-4
@@ -6,8 +6,10 @@ module Prism
|
||||
@env = env
|
||||
end
|
||||
|
||||
OverridableMethods = %w[PUT PATCH DELETE].freeze
|
||||
|
||||
def request_method
|
||||
@env['REQUEST_METHOD']
|
||||
@request_method ||= compute_request_method
|
||||
end
|
||||
|
||||
def path
|
||||
@@ -22,10 +24,20 @@ module Prism
|
||||
@env['CONTENT_TYPE']
|
||||
end
|
||||
|
||||
def user_agent
|
||||
@env['HTTP_USER_AGENT']
|
||||
end
|
||||
|
||||
def remote_addr
|
||||
@env['REMOTE_ADDR']
|
||||
end
|
||||
|
||||
def body
|
||||
@body ||= begin
|
||||
input = @env['rack.input']
|
||||
input ? input.read : ''
|
||||
content = input ? input.read : ''
|
||||
input.rewind if input.respond_to?(:rewind)
|
||||
content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -67,11 +79,27 @@ module Prism
|
||||
end
|
||||
end
|
||||
|
||||
# Negotiated response format: an explicit `.json` path/query extension
|
||||
# wins, otherwise it falls back to the Accept header, defaulting to
|
||||
# :html when nothing else matches (mirrors Rails' request.format).
|
||||
def format
|
||||
explicit = path_params['format'] || query_params['format']
|
||||
return explicit.to_s.to_sym unless explicit.nil? || explicit.to_s.empty?
|
||||
Mime.best_from_accept(headers['Accept']) || :html
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_request_method
|
||||
actual = @env['REQUEST_METHOD']
|
||||
return actual unless actual == 'POST'
|
||||
override = (@env['HTTP_X_HTTP_METHOD_OVERRIDE'] || body_params['_method']).to_s.upcase
|
||||
OverridableMethods.include?(override) ? override : actual
|
||||
end
|
||||
|
||||
def parse_query(string)
|
||||
return Params.new if string.empty?
|
||||
Params.from(URI.decode_www_form(string).to_h)
|
||||
Params.nested_from(URI.decode_www_form(string))
|
||||
rescue ArgumentError
|
||||
Params.new
|
||||
end
|
||||
@@ -82,7 +110,9 @@ module Prism
|
||||
parsed = JSON.parse(body)
|
||||
parsed.is_a?(Hash) ? Params.from(parsed) : Params.from('_json' => parsed)
|
||||
elsif content_type&.include?('application/x-www-form-urlencoded')
|
||||
Params.from(URI.decode_www_form(body).to_h)
|
||||
Params.nested_from(URI.decode_www_form(body))
|
||||
elsif Multipart.multipart?(content_type)
|
||||
Multipart.parse(body, Multipart.boundary_from(content_type))
|
||||
else
|
||||
Params.new
|
||||
end
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
module Prism
|
||||
# DSL context yielded (via instance_eval) inside `resources ... do ... end`
|
||||
# and `resource ... do ... end` blocks. Provides member/collection routes
|
||||
# and nesting further resources under the current one.
|
||||
class ResourceScope
|
||||
def initialize(app, name, singular, param)
|
||||
@app = app
|
||||
@name = name
|
||||
@singular = singular
|
||||
@param = param
|
||||
end
|
||||
|
||||
# Extra actions scoped to one member: /name/:id/action -> name#action,
|
||||
# named "action_singular" (e.g. `publish_post`).
|
||||
def member(&block)
|
||||
require_member_param!
|
||||
@app.group("/#{@name}/:#{@param}") do
|
||||
MemberCollectionScope.new(@app, @singular, @name, :member).instance_eval(&block)
|
||||
end
|
||||
end
|
||||
|
||||
# Extra actions scoped to the whole collection: /name/action -> name#action,
|
||||
# named "action_name" (e.g. `search_posts`).
|
||||
def collection(&block)
|
||||
@app.group("/#{@name}") do
|
||||
MemberCollectionScope.new(@app, @singular, @name, :collection).instance_eval(&block)
|
||||
end
|
||||
end
|
||||
|
||||
# Nested resources, scoped under this resource: for a plural parent
|
||||
# that's /name/:singular_id/nested_name/... (Rails' classic nested
|
||||
# routing); for a singular parent (no id at all) it's just
|
||||
# /name/nested_name/....
|
||||
def resources(nested_name, **options, &block)
|
||||
@app.group(nested_prefix) { @app.resources(nested_name, **options, &block) }
|
||||
end
|
||||
|
||||
def resource(nested_name, **options, &block)
|
||||
@app.group(nested_prefix) { @app.resource(nested_name, **options, &block) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def nested_prefix
|
||||
@param ? "/#{@name}/:#{@singular}_id" : "/#{@name}"
|
||||
end
|
||||
|
||||
def require_member_param!
|
||||
return if @param
|
||||
raise ArgumentError, 'member/collection routes need an id segment; not available on a singular resource'
|
||||
end
|
||||
end
|
||||
|
||||
# DSL context for the block passed to ResourceScope#member/#collection.
|
||||
class MemberCollectionScope
|
||||
def initialize(app, singular, plural, kind)
|
||||
@app = app
|
||||
@singular = singular
|
||||
@plural = plural
|
||||
@kind = kind
|
||||
end
|
||||
|
||||
%w[get post put patch delete].each do |verb|
|
||||
define_method(verb) do |action, to: nil, as: nil|
|
||||
controller_action = to || "#{@plural}##{action}"
|
||||
name = as || default_name(action)
|
||||
@app.public_send(verb, "/#{action}", to: controller_action, as: name)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_name(action)
|
||||
@kind == :member ? "#{action}_#{@singular}" : "#{action}_#{@plural}"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user