From fe62127bc21ad141d4a1ab79463b9b5ac8b1a8b7 Mon Sep 17 00:00:00 2001 From: reesericci Date: Thu, 18 Jan 2024 05:28:40 +0000 Subject: [PATCH 01/20] doorkeeper initial --- Gemfile | 2 + Gemfile.lock | 3 + app/models/user/user.rb | 10 + config/initializers/doorkeeper.rb | 515 ++++++++++++++++++ config/locales/doorkeeper.en.yml | 151 +++++ config/routes.rb | 1 + ...20240118051026_create_doorkeeper_tables.rb | 98 ++++ 7 files changed, 780 insertions(+) create mode 100644 config/initializers/doorkeeper.rb create mode 100644 config/locales/doorkeeper.en.yml create mode 100644 db/migrate/20240118051026_create_doorkeeper_tables.rb diff --git a/Gemfile b/Gemfile index 8c531b8..2b8a56f 100644 --- a/Gemfile +++ b/Gemfile @@ -116,3 +116,5 @@ gem "activejob-status", "~> 1.0" gem "standard", "~> 1.33" gem "standard-rails", "~> 1.0" + +gem "doorkeeper", "~> 5.6" diff --git a/Gemfile.lock b/Gemfile.lock index 0404621..c424c2e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,6 +126,8 @@ GEM diff-lcs (1.5.0) dnsimple (8.7.1) httparty + doorkeeper (5.6.8) + railties (>= 5) dotenv (2.8.1) dotenv-rails (2.8.1) dotenv (= 2.8.1) @@ -444,6 +446,7 @@ DEPENDENCIES dalli (~> 3.2) debug dnsimple (~> 8.1) + doorkeeper (~> 5.6) dotenv-rails erb-formatter importmap-rails (~> 1.2) diff --git a/app/models/user/user.rb b/app/models/user/user.rb index f378d21..76deace 100644 --- a/app/models/user/user.rb +++ b/app/models/user/user.rb @@ -2,6 +2,16 @@ class User::User < ApplicationRecord validates :email, uniqueness: true # standard:disable all has_many :user_credentials # standard:disable all + has_many :access_grants, + class_name: 'Doorkeeper::AccessGrant', + foreign_key: :resource_owner_id, + dependent: :delete_all + + has_many :access_tokens, + class_name: 'Doorkeeper::AccessToken', + foreign_key: :resource_owner_id, + dependent: :delete_all + after_initialize do @hotp = ROTP::HOTP.new(hotp_token) end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 0000000..d1d2aea --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,515 @@ +# frozen_string_literal: true + +Doorkeeper.configure do + # Change the ORM that doorkeeper will use (requires ORM extensions installed). + # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" + # Put your resource owner authentication logic here. + # Example implementation: + # User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url) + end + + # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb + # file then you need to declare this block in order to restrict access to the web interface for + # adding oauth authorized applications. In other case it will return 403 Forbidden response + # every time somebody will try to access the admin web interface. + # + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # + # if current_user + # head :forbidden unless current_user.admin? + # else + # redirect_to sign_in_url + # end + # end + + # You can use your own model classes if you need to extend (or even override) default + # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. + # + # Be default Doorkeeper ActiveRecord ORM uses it's own classes: + # + # access_token_class "Doorkeeper::AccessToken" + # access_grant_class "Doorkeeper::AccessGrant" + # application_class "Doorkeeper::Application" + # + # Don't forget to include Doorkeeper ORM mixins into your custom models: + # + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) + # + # For example: + # + # access_token_class "MyAccessToken" + # + # class MyAccessToken < ApplicationRecord + # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken + # + # self.table_name = "hey_i_wanna_my_name" + # + # def destroy_me! + # destroy + # end + # end + + # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. + # By default this option is disabled. + # + # Make sure you properly setup you database and have all the required columns (run + # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails + # migrations). + # + # If this option enabled, Doorkeeper will store not only Resource Owner primary key + # value, but also it's type (class name). See "Polymorphic Associations" section of + # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations + # + # [NOTE] If you apply this option on already existing project don't forget to manually + # update `resource_owner_type` column in the database and fix migration template as it will + # set NOT NULL constraint for Access Grants table. + # + # use_polymorphic_resource_owner + + # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might + # want to use API mode that will skip all the views management and change the way how + # Doorkeeper responds to a requests. + # + # api_only + + # Enforce token request content type to application/x-www-form-urlencoded. + # It is not enabled by default to not break prior versions of the gem. + # + # enforce_content_type + + # Authorization Code expiration time (default: 10 minutes). + # + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default: 2 hours). + # If you want to disable expiration, set this to `nil`. + # + # access_token_expires_in 2.hours + + # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in + # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to + # +access_token_expires_in+ configuration option value. If you really need to issue a + # non-expiring access token (which is not recommended) then you need to return + # Float::INFINITY from this block. + # + # `context` has the following properties available: + # + # * `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # * `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # * `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # * `resource_owner` - authorized resource owner instance (if present) + # + # custom_access_token_expires_in do |context| + # context.client.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator + # + # access_token_generator '::Doorkeeper::JWT' + + # The controller +Doorkeeper::ApplicationController+ inherits from. + # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to + # +ActionController::API+. The return value of this option must be a stringified class name. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers + # + # base_controller 'ApplicationController' + + # Reuse access token for the same resource owner within an application (disabled by default). + # + # This option protects your application from creating new tokens before old **valid** one becomes + # expired so your database doesn't bloat. Keep in mind that when this option is enabled Doorkeeper + # doesn't update existing token expiration time, it will create a new token instead if no active matching + # token found for the application, resources owner and/or set of scopes. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # + # You can not enable this option together with +hash_token_secrets+. + # + # reuse_access_token + + # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching + # token using `matching_token_for` Access Token API that searches for valid records + # in batches in order not to pollute the memory with all the database records. By default + # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value + # depending on your needs and server capabilities. + # + # token_lookup_batch_size 10_000 + + # Set a limit for token_reuse if using reuse_access_token option + # + # This option limits token_reusability to some extent. + # If not set then access_token will be reused unless it expires. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 + # + # This option should be a percentage(i.e. (0,100]) + # + # token_reuse_limit 100 + + # Only allow one valid access token obtained via client credentials + # per client. If a new access token is obtained before the old one + # expired, the old one gets revoked (disabled by default) + # + # When enabling this option, make sure that you do not expect multiple processes + # using the same credentials at the same time (e.g. web servers spanning + # multiple machines and/or processes). + # + # revoke_previous_client_credentials_token + + # Hash access and refresh tokens before persisting them. + # This will disable the possibility to use +reuse_access_token+ + # since plain values can no longer be retrieved. + # + # Note: If you are already a user of doorkeeper and have existing tokens + # in your installation, they will be invalid without adding 'fallback: :plain'. + # + # hash_token_secrets + # By default, token secrets will be hashed using the + # +Doorkeeper::Hashing::SHA256+ strategy. + # + # If you wish to use another hashing implementation, you can override + # this strategy as follows: + # + # hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl' + # + # Keep in mind that changing the hashing function will invalidate all existing + # secrets, if there are any. + + # Hash application secrets before persisting them. + # + # hash_application_secrets + # + # By default, applications will be hashed + # with the +Doorkeeper::SecretStoring::SHA256+ strategy. + # + # If you wish to use bcrypt for application secret hashing, uncomment + # this line instead: + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' + + # When the above option is enabled, and a hashed token or secret is not found, + # you can allow to fall back to another strategy. For users upgrading + # doorkeeper and wishing to enable hashing, you will probably want to enable + # the fallback to plain tokens. + # + # This will ensure that old access tokens and secrets + # will remain valid even if the hashing above is enabled. + # + # This can be done by adding 'fallback: plain', e.g. : + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt', fallback: :plain + + # Issue access tokens with refresh token (disabled by default), you may also + # pass a block which accepts `context` to customize when to give a refresh + # token or not. Similar to +custom_access_token_expires_in+, `context` has + # the following properties: + # + # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # + # use_refresh_token + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter confirmation: true (default: false) if you want to enforce ownership of + # a registered application + # NOTE: you must also run the rails g doorkeeper:application_owner generator + # to provide the necessary support + # + # enable_application_owner confirmation: false + + # Define access token scopes for your provider + # For more information go to + # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes + # + # default_scopes :public + # optional_scopes :write, :update + + # Allows to restrict only certain scopes for grant_type. + # By default, all the scopes will be available for all the grant types. + # + # Keys to this hash should be the name of grant_type and + # values should be the array of scopes for that grant type. + # Note: scopes should be from configured_scopes (i.e. default or optional) + # + # scopes_by_grant_type password: [:write], client_credentials: [:update] + + # Forbids creating/updating applications with arbitrary scopes that are + # not in configuration, i.e. +default_scopes+ or +optional_scopes+. + # (disabled by default) + # + # enforce_configured_scopes + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # Callable objects such as proc, lambda, block or any object that responds to + # #call can be used in order to allow conditional checks (to allow non-SSL + # redirects to localhost for example). + # + # force_ssl_in_redirect_uri !Rails.env.development? + # + # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } + + # Specify what redirect URI's you want to block during Application creation. + # Any redirect URI is allowed by default. + # + # You can use this option in order to forbid URI's with 'javascript' scheme + # for example. + # + # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } + + # Allows to set blank redirect URIs for Applications in case Doorkeeper configured + # to use URI-less OAuth grant flows like Client Credentials or Resource Owner + # Password Credentials. The option is on by default and checks configured grant + # types, but you **need** to manually drop `NOT NULL` constraint from `redirect_uri` + # column for `oauth_applications` database table. + # + # You can completely disable this feature with: + # + # allow_blank_redirect_uri false + # + # Or you can define your custom check: + # + # allow_blank_redirect_uri do |grant_flows, client| + # client.superapp? + # end + + # Specify how authorization errors should be handled. + # By default, doorkeeper renders json errors when access token + # is invalid, expired, revoked or has invalid scopes. + # + # If you want to render error response yourself (i.e. rescue exceptions), + # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken + # or following specific errors: + # + # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, + # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown + # + # handle_auth_errors :raise + # + # If you want to redirect back to the client application in accordance with + # https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1, you can set + # +handle_auth_errors+ to :redirect + # + # handle_auth_errors :redirect + + # Customize token introspection response. + # Allows to add your own fields to default one that are required by the OAuth spec + # for the introspection response. It could be `sub`, `aud` and so on. + # This configuration option can be a proc, lambda or any Ruby object responds + # to `.call` method and result of it's invocation must be a Hash. + # + # custom_introspection_response do |token, context| + # { + # "sub": "Z5O3upPC88QrAjx00dis", + # "aud": "https://protected.example.net/resource", + # "username": User.find(token.resource_owner_id).username + # } + # end + # + # or + # + # custom_introspection_response CustomIntrospectionResponder + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables authorization_code and + # client_credentials. + # + # implicit and password grant flows have risks that you should understand + # before enabling: + # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.2 + # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.3 + # + # grant_flows %w[authorization_code client_credentials] + + # Allows to customize OAuth grant flows that +each+ application support. + # You can configure a custom block (or use a class respond to `#call`) that must + # return `true` in case Application instance supports requested OAuth grant flow + # during the authorization request to the server. This configuration +doesn't+ + # set flows per application, it only allows to check if application supports + # specific grant flow. + # + # For example you can add an additional database column to `oauth_applications` table, + # say `t.array :grant_flows, default: []`, and store allowed grant flows that can + # be used with this application there. Then when authorization requested Doorkeeper + # will call this block to check if specific Application (passed with client_id and/or + # client_secret) is allowed to perform the request for the specific grant type + # (authorization, password, client_credentials, etc). + # + # Example of the block: + # + # ->(flow, client) { client.grant_flows.include?(flow) } + # + # In case this option invocation result is `false`, Doorkeeper server returns + # :unauthorized_client error and stops the request. + # + # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call + # @return [Boolean] `true` if allow or `false` if forbid the request + # + # allow_grant_flow_for_client do |grant_flow, client| + # # `grant_flows` is an Array column with grant + # # flows that application supports + # + # client.grant_flows.include?(grant_flow) + # end + + # If you need arbitrary Resource Owner-Client authorization you can enable this option + # and implement the check your need. Config option must respond to #call and return + # true in case resource owner authorized for the specific application or false in other + # cases. + # + # By default all Resource Owners are authorized to any Client (application). + # + # authorize_resource_owner_for_client do |client, resource_owner| + # resource_owner.admin? || client.owners_allowlist.include?(resource_owner) + # end + + # Allows additional data fields to be sent while granting access to an application, + # and for this additional data to be included in subsequently generated access tokens. + # The 'authorizations/new' page will need to be overridden to include this additional data + # in the request params when granting access. The access grant and access token models + # will both need to respond to these additional data fields, and have a database column + # to store them in. + # + # Example: + # You have a multi-tenanted platform and want to be able to grant access to a specific + # tenant, rather than all the tenants a user has access to. You can use this config + # option to specify that a ':tenant_id' will be passed when authorizing. This tenant_id + # will be included in the access tokens. When a request is made with one of these access + # tokens, you can check that the requested data belongs to the specified tenant. + # + # Default value is an empty Array: [] + # custom_access_token_attributes [:tenant_id] + + # Hook into the strategies' request & response life-cycle in case your + # application needs advanced customization or logging: + # + # before_successful_strategy_response do |request| + # puts "BEFORE HOOK FIRED! #{request}" + # end + # + # after_successful_strategy_response do |request, response| + # puts "AFTER HOOK FIRED! #{request}, #{response}" + # end + + # Hook into Authorization flow in order to implement Single Sign Out + # or add any other functionality. Inside the block you have an access + # to `controller` (authorizations controller instance) and `context` + # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth + # or auth objects with issued token based on hook type (before or after). + # + # before_successful_authorization do |controller, context| + # Rails.logger.info(controller.request.params.inspect) + # + # Rails.logger.info(context.pre_auth.inspect) + # end + # + # after_successful_authorization do |controller, context| + # controller.session[:logout_urls] << + # Doorkeeper::Application + # .find_by(controller.request.params.slice(:redirect_uri)) + # .logout_uri + # + # Rails.logger.info(context.auth.inspect) + # Rails.logger.info(context.issued_token) + # end + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # Configure custom constraints for the Token Introspection request. + # By default this configuration option allows to introspect a token by another + # token of the same application, OR to introspect the token that belongs to + # authorized client (from authenticated client) OR when token doesn't + # belong to any client (public token). Otherwise requester has no access to the + # introspection and it will return response as stated in the RFC. + # + # Block arguments: + # + # @param token [Doorkeeper::AccessToken] + # token to be introspected + # + # @param authorized_client [Doorkeeper::Application] + # authorized client (if request is authorized using Basic auth with + # Client Credentials for example) + # + # @param authorized_token [Doorkeeper::AccessToken] + # Bearer token used to authorize the request + # + # In case the block returns `nil` or `false` introspection responses with 401 status code + # when using authorized token to introspect, or you'll get 200 with { "active": false } body + # when using authorized client to introspect as stated in the + # RFC 7662 section 2.2. Introspection Response. + # + # Using with caution: + # Keep in mind that these three parameters pass to block can be nil as following case: + # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. + # `token` will be nil if and only if `authorized_token` is present. + # So remember to use `&` or check if it is present before calling method on + # them to make sure you doesn't get NoMethodError exception. + # + # You can define your custom check: + # + # allow_token_introspection do |token, authorized_client, authorized_token| + # if authorized_token + # # customize: require `introspection` scope + # authorized_token.application == token&.application || + # authorized_token.scopes.include?("introspection") + # elsif token.application + # # `protected_resource` is a new database boolean column, for example + # authorized_client == token.application || authorized_client.protected_resource? + # else + # # public token (when token.application is nil, token doesn't belong to any application) + # true + # end + # end + # + # Or you can completely disable any token introspection: + # + # allow_token_introspection false + # + # If you need to block the request at all, then configure your routes.rb or web-server + # like nginx to forbid the request. + + # WWW-Authenticate Realm (default: "Doorkeeper"). + # + # realm "Doorkeeper" +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 0000000..99fa3d4 --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,151 @@ +en: + activerecord: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + unspecified_scheme: 'must specify a scheme.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + forbidden_uri: 'is forbidden by the server.' + scopes: + not_match_configured: "doesn't match configured on the server." + + doorkeeper: + applications: + confirmations: + destroy: 'Are you sure?' + buttons: + edit: 'Edit' + destroy: 'Destroy' + submit: 'Submit' + cancel: 'Cancel' + authorize: 'Authorize' + form: + error: 'Whoops! Check your form for possible errors' + help: + confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' + redirect_uri: 'Use one line per URI' + blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI." + scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' + edit: + title: 'Edit application' + index: + title: 'Your applications' + new: 'New Application' + name: 'Name' + callback_url: 'Callback URL' + confidential: 'Confidential?' + actions: 'Actions' + confidentiality: + 'yes': 'Yes' + 'no': 'No' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'UID' + secret: 'Secret' + secret_hashed: 'Secret hashed' + scopes: 'Scopes' + confidential: 'Confidential' + callback_urls: 'Callback urls' + actions: 'Actions' + not_defined: 'Not defined' + + authorizations: + buttons: + authorize: 'Authorize' + deny: 'Deny' + error: + title: 'An error has occurred' + new: + title: 'Authorization required' + prompt: 'Authorize %{client_name} to use your account?' + able_to: 'This application will be able to' + show: + title: 'Authorization code' + form_post: + title: 'Submit this form' + + authorized_applications: + confirmations: + revoke: 'Are you sure?' + buttons: + revoke: 'Revoke' + index: + title: 'Your authorized applications' + application: 'Application' + created_at: 'Created At' + date_format: '%Y-%m-%d %H:%M:%S' + + pre_authorization: + status: 'Pre-authorization' + + errors: + messages: + # Common error messages + invalid_request: + unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + missing_param: 'Missing required parameter: %{value}.' + request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.' + invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI." + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + invalid_code_challenge_method: 'The code challenge method must be plain or S256.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + # Configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.' + admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + unsupported_response_mode: 'The authorization server does not support this response mode.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + revoke: + unauthorized: "You are not authorized to revoke this token" + + forbidden_token: + missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".' + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + title: 'Doorkeeper' + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + home: 'Home' + application: + title: 'OAuth authorization required' diff --git a/config/routes.rb b/config/routes.rb index ee056ab..990c632 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ # typed: true Rails.application.routes.draw do + use_doorkeeper # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html get "up" => "rails/health#show", :as => :rails_health_check diff --git a/db/migrate/20240118051026_create_doorkeeper_tables.rb b/db/migrate/20240118051026_create_doorkeeper_tables.rb new file mode 100644 index 0000000..269ca9b --- /dev/null +++ b/db/migrate/20240118051026_create_doorkeeper_tables.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +class CreateDoorkeeperTables < ActiveRecord::Migration[7.1] + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + + # Remove `null: false` if you are planning to use grant flows + # that doesn't require redirect URI to be used during authorization + # like Client Credentials flow or Resource Owner Password. + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.boolean :confidential, null: false, default: true + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_grants do |t| + t.references :resource_owner, null: false + t.references :application, null: false + t.string :token, null: false + t.integer :expires_in, null: false + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.datetime :created_at, null: false + t.datetime :revoked_at + end + + add_index :oauth_access_grants, :token, unique: true + add_foreign_key( + :oauth_access_grants, + :oauth_applications, + column: :application_id + ) + + create_table :oauth_access_tokens do |t| + t.references :resource_owner, index: true + + # Remove `null: false` if you are planning to use Password + # Credentials Grant flow that doesn't require an application. + t.references :application, null: false + + # If you use a custom token generator you may need to change this column + # from string to text, so that it accepts tokens larger than 255 + # characters. More info on custom token generators in: + # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator + # + # t.text :token, null: false + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.string :scopes + t.datetime :created_at, null: false + t.datetime :revoked_at + + # The authorization server MAY issue a new refresh token, in which case + # *the client MUST discard the old refresh token* and replace it with the + # new refresh token. The authorization server MAY revoke the old + # refresh token after issuing a new refresh token to the client. + # @see https://datatracker.ietf.org/doc/html/rfc6749#section-6 + # + # Doorkeeper implementation: if there is a `previous_refresh_token` column, + # refresh tokens will be revoked after a related access token is used. + # If there is no `previous_refresh_token` column, previous tokens are + # revoked as soon as a new access token is created. + # + # Comment out this line if you want refresh tokens to be instantly + # revoked after use. + t.string :previous_refresh_token, null: false, default: "" + end + + add_index :oauth_access_tokens, :token, unique: true + + # See https://github.com/doorkeeper-gem/doorkeeper/issues/1592 + if ActiveRecord::Base.connection.adapter_name == "SQLServer" + execute <<~SQL.squish + CREATE UNIQUE NONCLUSTERED INDEX index_oauth_access_tokens_on_refresh_token ON oauth_access_tokens(refresh_token) + WHERE refresh_token IS NOT NULL + SQL + else + add_index :oauth_access_tokens, :refresh_token, unique: true + end + + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id + ) + + # Uncomment below to ensure a valid reference to the resource owner's table + add_foreign_key :oauth_access_grants, :user_users, column: :resource_owner_id + add_foreign_key :oauth_access_tokens, :user_users, column: :resource_owner_id + end +end From 02ff09271dc4942f67639c96b4111c4b0397c275 Mon Sep 17 00:00:00 2001 From: Reese Armstrong Date: Thu, 18 Jan 2024 07:24:37 -0600 Subject: [PATCH 02/20] Sync main -> api (#47) --- Gemfile | 1 + Gemfile.lock | 2 ++ app/controllers/users_controller.rb | 4 ++-- app/models/record.rb | 11 ++++++----- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 2b8a56f..f6ed58f 100644 --- a/Gemfile +++ b/Gemfile @@ -117,4 +117,5 @@ gem "standard", "~> 1.33" gem "standard-rails", "~> 1.0" +gem "syntax_suggest", "~> 2.0" gem "doorkeeper", "~> 5.6" diff --git a/Gemfile.lock b/Gemfile.lock index c424c2e..8f6edc3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -374,6 +374,7 @@ GEM stimulus-rails (1.3.0) railties (>= 6.0.0) stringio (3.1.0) + syntax_suggest (2.0.0) syntax_tree (6.2.0) prettier_print (>= 1.2.0) tailwindcss-rails (2.1.0-x86_64-darwin) @@ -473,6 +474,7 @@ DEPENDENCIES standard (~> 1.33) standard-rails (~> 1.0) stimulus-rails + syntax_suggest (~> 2.0) tailwindcss-rails (~> 2.1) tapioca turbo-rails diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 18dfedd..7802a6f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -7,8 +7,8 @@ def register end def create - if !User::User.find_by(email: params[:email]) - if User::User.find_by(email: params[:email]).verified == false + if User::User.find_by(email: params[:email]) + if !User::User.find_by(email: params[:email]).verified user = User::User.find_by(email: params[:email]) session[:current_user_id] = user.id session[:new_user] = true diff --git a/app/models/record.rb b/app/models/record.rb index 7cf9c3e..23f093e 100644 --- a/app/models/record.rb +++ b/app/models/record.rb @@ -45,7 +45,7 @@ def self.filter_dnsimple_host(host, domains) domain = Domain.find_by(host: host) Rails.cache.fetch([domain, "records"], expires_in: 1.week) do records = [] - domains.each |r| + domains.each do |r| if r.domain_id == domain.id records.push(r) end @@ -66,7 +66,6 @@ def self.dnsimple_to_record(obj) domain = cap[3] domain_obj = Domain.find_by(host: domain) - puts domain_obj Record.new( _id: obj.id, _persisted: true, @@ -92,7 +91,7 @@ def save broadcast_replace_to("records:main", partial: "records/record") Rails.cache.delete("records") - domain.update(updated_at: Time.now) + domain.update!(updated_at: Time.now) # standard:disable all end def persisted? @@ -109,7 +108,7 @@ def destroy! end def self.destroy_all_host!(host) - for r in where_host(host) + where_host(host).each do |r| r.destroy! end end @@ -130,9 +129,10 @@ def self.all records.push(record) end end - end + end records + end end @@ -259,4 +259,5 @@ def destroy_record @_persisted = false true end + end From 494e74df081144f4db99b222ce68a855f56a0fad Mon Sep 17 00:00:00 2001 From: reesericci Date: Fri, 19 Jan 2024 06:05:29 +0000 Subject: [PATCH 03/20] API routes & OIDC --- Gemfile | 2 + Gemfile.lock | 4 ++ app/controllers/api/v1/api_controller.rb | 11 +++ .../api/v1/domains/records_controller.rb | 47 ++++++++++++ app/controllers/api/v1/domains_controller.rb | 43 +++++++++++ app/controllers/api/v1/user_controller.rb | 49 +++++++++++++ app/controllers/auth_controller.rb | 4 +- .../controllers/webauthn_controller.js | 12 ++-- app/models/domain.rb | 5 ++ app/models/record.rb | 4 +- .../api/v1/domains/_domain.json.jbuilder | 10 +++ app/views/api/v1/domains/index.json.jbuilder | 1 + .../v1/domains/records/_record.json.jbuilder | 6 ++ .../v1/domains/records/index.json.jbuilder | 1 + .../api/v1/domains/records/show.json.jbuilder | 1 + app/views/api/v1/domains/show.json.jbuilder | 1 + app/views/api/v1/user/show.json.jbuilder | 17 +++++ app/views/auth/unsupported.html.erb | 4 +- config/application.rb | 4 +- config/initializers/doorkeeper.rb | 32 +++++---- .../initializers/doorkeeper_openid_connect.rb | 71 +++++++++++++++++++ .../locales/doorkeeper_openid_connect.en.yml | 23 ++++++ config/locales/en.yml | 13 ++++ config/puma.rb | 2 +- config/routes.rb | 13 ++++ ...create_doorkeeper_openid_connect_tables.rb | 15 ++++ db/schema.rb | 55 +++++++++++++- 27 files changed, 426 insertions(+), 24 deletions(-) create mode 100644 app/controllers/api/v1/api_controller.rb create mode 100644 app/controllers/api/v1/domains/records_controller.rb create mode 100644 app/controllers/api/v1/domains_controller.rb create mode 100644 app/controllers/api/v1/user_controller.rb create mode 100644 app/views/api/v1/domains/_domain.json.jbuilder create mode 100644 app/views/api/v1/domains/index.json.jbuilder create mode 100644 app/views/api/v1/domains/records/_record.json.jbuilder create mode 100644 app/views/api/v1/domains/records/index.json.jbuilder create mode 100644 app/views/api/v1/domains/records/show.json.jbuilder create mode 100644 app/views/api/v1/domains/show.json.jbuilder create mode 100644 app/views/api/v1/user/show.json.jbuilder create mode 100644 config/initializers/doorkeeper_openid_connect.rb create mode 100644 config/locales/doorkeeper_openid_connect.en.yml create mode 100644 db/migrate/20240118133216_create_doorkeeper_openid_connect_tables.rb diff --git a/Gemfile b/Gemfile index f6ed58f..e5b52f4 100644 --- a/Gemfile +++ b/Gemfile @@ -119,3 +119,5 @@ gem "standard-rails", "~> 1.0" gem "syntax_suggest", "~> 2.0" gem "doorkeeper", "~> 5.6" + +gem "doorkeeper-openid_connect", "~> 1.8" diff --git a/Gemfile.lock b/Gemfile.lock index 8f6edc3..340d1aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -128,6 +128,9 @@ GEM httparty doorkeeper (5.6.8) railties (>= 5) + doorkeeper-openid_connect (1.8.7) + doorkeeper (>= 5.5, < 5.7) + jwt (>= 2.5) dotenv (2.8.1) dotenv-rails (2.8.1) dotenv (= 2.8.1) @@ -448,6 +451,7 @@ DEPENDENCIES debug dnsimple (~> 8.1) doorkeeper (~> 5.6) + doorkeeper-openid_connect (~> 1.8) dotenv-rails erb-formatter importmap-rails (~> 1.2) diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb new file mode 100644 index 0000000..63aa91c --- /dev/null +++ b/app/controllers/api/v1/api_controller.rb @@ -0,0 +1,11 @@ +class Api::V1::ApiController < ActionController::Base + + skip_before_action :verify_authenticity_token + + private + + # Find the user that owns the access token + def current_user + User::User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token + end +end diff --git a/app/controllers/api/v1/domains/records_controller.rb b/app/controllers/api/v1/domains/records_controller.rb new file mode 100644 index 0000000..c345001 --- /dev/null +++ b/app/controllers/api/v1/domains/records_controller.rb @@ -0,0 +1,47 @@ +class Api::V1::Domains::RecordsController < Api::V1::ApiController + include DomainAuthorization + before_action do + doorkeeper_authorize! :domains + doorkeeper_authorize! :domains_records + end + + before_action only: [:create, :update, :destroy] do + doorkeeper_authorize! :domains_records_write + end + + def index + @records = current_domain.records + end + + def create + @record = Record.create(domain_id: current_domain.id, name: params["name"], type: params["type"], content: params["content"], ttl: params["ttl"], priority: params["priority"]) # standard:disable all + render "show" + end + + def show + @record = Record.find(params[:id]) + end + + def update + @record = Record.find(params[:id]) + + (@record.type = params[:type]) if params[:type] + (@record.name = params[:name]) if params[:name] + (@record.content = params[:content]) if params[:content] + (@record.ttl = params[:ttl]) if params[:ttl] + (@record.priority = params[:priority]) if params[:priority] + + @record.save # standard:disable all + + render "show" + end + + def destroy + @record = Record.find(params[:id]) + + @record.destroy! #standard:disable all + + index + render "index" + end +end diff --git a/app/controllers/api/v1/domains_controller.rb b/app/controllers/api/v1/domains_controller.rb new file mode 100644 index 0000000..ad52928 --- /dev/null +++ b/app/controllers/api/v1/domains_controller.rb @@ -0,0 +1,43 @@ +class Api::V1::DomainsController < Api::V1::ApiController + include DomainAuthorization + before_action do + doorkeeper_authorize! :domains + end + + before_action only: [:create, :destroy] do + doorkeeper_authorize! :domains_write + end + + skip_before_action :authorize_domain, only: [:index, :create] + + def index + @domains = Domain.where(user_users_id: current_user.id) + if params[:records] + doorkeeper_authorize!(:domains_records) + end + end + + def show + @domain = Domain.find_by(host: params[:host]) + if params[:records] + doorkeeper_authorize!(:domains_records) + end + end + + def create + @domain = Domain.new(host: params[:host], plan: params[:plan], provisional: true, user_users_id: current_user.id) + if @domain.save + render "show" + else + render json: @domain.errors, status: 418 + end + end + + def destroy + @domain = Domain.find_by(host: params[:host]) + @domain.destroy! + + index + render "index" + end +end diff --git a/app/controllers/api/v1/user_controller.rb b/app/controllers/api/v1/user_controller.rb new file mode 100644 index 0000000..e568849 --- /dev/null +++ b/app/controllers/api/v1/user_controller.rb @@ -0,0 +1,49 @@ +class Api::V1::UserController < Api::V1::ApiController + before_action do + doorkeeper_authorize! :user + end + + def show + if doorkeeper_token.scopes.exists?(:name) + @name = current_user.name + end + + if doorkeeper_token.scopes.exists?(:email) + @email = current_user.email + end + + @id = current_user.id + @created_at = current_user.created_at + @updated_at = current_user.updated_at + @verified = current_user.verified + + if doorkeeper_token.scopes.exists?(:admin) + @admin = current_user.admin + end + end + + def update + redirected = false + + if params[:name] + if doorkeeper_authorize! :name_write + redirected = true + else + current_user.update!(name: params[:name]) + end + end + + if params[:email] + if doorkeeper_authorize! :email_write + redirected = true + else + current_user.update!(email: params[:email]) + end + end + + if !redirected + show + render "show" + end + end +end diff --git a/app/controllers/auth_controller.rb b/app/controllers/auth_controller.rb index 2419934..c59c7d1 100644 --- a/app/controllers/auth_controller.rb +++ b/app/controllers/auth_controller.rb @@ -40,7 +40,7 @@ def verify_code session[:authenticated] = true session[:current_user_id] = u.id - redirect_to(root_path, notice: (User::Credential.where(user_users_id: u.id).length == 0) ? "Passkeys are more secure & convienient way to login. Head to Account Settings to add one." : "To disable insecure email code authentication, head to Account Settings.") + redirect_to(session[:return_path] || root_path, notice: (User::Credential.where(user_users_id: u.id).length == 0) ? "Passkeys are more secure & convienient way to login. Head to Account Settings to add one." : "To disable insecure email code authentication, head to Account Settings.") else render inline: "<%= turbo_stream.replace \"error\" do %>

Invalid OTP

<% end %>", status: :unprocessable_entity, format: :turbo_stream end @@ -57,7 +57,7 @@ def create_key user.verified = true user.save! session[:authenticated] = true - redirect_to(root_path, notice: "To add a passkey in the future, head to Account Settings") + redirect_to(session[:return_path] || root_path, notice: "To add a passkey in the future, head to Account Settings") end @options = WebAuthn::Credential.options_for_create( diff --git a/app/javascript/controllers/webauthn_controller.js b/app/javascript/controllers/webauthn_controller.js index 0112334..639ee36 100644 --- a/app/javascript/controllers/webauthn_controller.js +++ b/app/javascript/controllers/webauthn_controller.js @@ -14,11 +14,15 @@ export default class extends Controller { connect() { console.log("hai") - if (typeof(PublicKeyCredential) == "undefined") { - window.location.pathname = "/auth/unsupported" - } + if (typeof(PublicKeyCredential) == "undefined" && window.location.search != "?force=true") { + const url = new URL(window.location); + url.searchParams.append("returnpath", window.location.pathname) + url.pathname = "/auth/unsupported" - window.Auth = Auth + window.location.href = url.href + } + + window.Auth = Auth } async createKey() { diff --git a/app/models/domain.rb b/app/models/domain.rb index 2adc781..90922ee 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -1,6 +1,7 @@ class Domain < ApplicationRecord include DnsimpleHelper validates :host, uniqueness: true # standard:disable all + validates :host, presence: {message: "Host is not present"} validates :user_users_id, presence: {message: "User ID is not present"} after_create ->(d) { Domain::InitializeJob.perform_later(d.id) }, unless: proc { |d| d.provisional } @@ -38,6 +39,10 @@ def top_records records end + def records + Record.where_host(host) + end + def user User::User.find_by(id: user_users_id) end diff --git a/app/models/record.rb b/app/models/record.rb index 23f093e..9291a03 100644 --- a/app/models/record.rb +++ b/app/models/record.rb @@ -1,5 +1,6 @@ +include DnsimpleHelper #standard:disable all + class Record - include DnsimpleHelper include ActiveModel::Model include ActiveModel::Dirty include ActiveModel::API @@ -137,6 +138,7 @@ def self.all end def self.find(id) + id = id.to_i found = nil for r in all if r.id == id diff --git a/app/views/api/v1/domains/_domain.json.jbuilder b/app/views/api/v1/domains/_domain.json.jbuilder new file mode 100644 index 0000000..3b2d590 --- /dev/null +++ b/app/views/api/v1/domains/_domain.json.jbuilder @@ -0,0 +1,10 @@ +json.id d.id +json.host d.host +json.created_at d.created_at +json.updated_at d.updated_at +json.user_id d.user_users_id +json.provisional d.provisional +json.plan d.plan +if records + json.records d.records, partial: "records/record", as: :r +end diff --git a/app/views/api/v1/domains/index.json.jbuilder b/app/views/api/v1/domains/index.json.jbuilder new file mode 100644 index 0000000..cc128b8 --- /dev/null +++ b/app/views/api/v1/domains/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @domains, partial: "domain", as: :d, locals: {records: params[:records]} diff --git a/app/views/api/v1/domains/records/_record.json.jbuilder b/app/views/api/v1/domains/records/_record.json.jbuilder new file mode 100644 index 0000000..bc7794a --- /dev/null +++ b/app/views/api/v1/domains/records/_record.json.jbuilder @@ -0,0 +1,6 @@ +json.id r.id +json.name r.name +json.content r.content +json.type r.type +json.ttl r.ttl +json.domain_id r.domain_id diff --git a/app/views/api/v1/domains/records/index.json.jbuilder b/app/views/api/v1/domains/records/index.json.jbuilder new file mode 100644 index 0000000..a7f20fa --- /dev/null +++ b/app/views/api/v1/domains/records/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @records, partial: "record", as: :r diff --git a/app/views/api/v1/domains/records/show.json.jbuilder b/app/views/api/v1/domains/records/show.json.jbuilder new file mode 100644 index 0000000..d87f50d --- /dev/null +++ b/app/views/api/v1/domains/records/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "record", r: @record diff --git a/app/views/api/v1/domains/show.json.jbuilder b/app/views/api/v1/domains/show.json.jbuilder new file mode 100644 index 0000000..dd2d1d1 --- /dev/null +++ b/app/views/api/v1/domains/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "domain", d: @domain, records: params[:records] diff --git a/app/views/api/v1/user/show.json.jbuilder b/app/views/api/v1/user/show.json.jbuilder new file mode 100644 index 0000000..4af2b7c --- /dev/null +++ b/app/views/api/v1/user/show.json.jbuilder @@ -0,0 +1,17 @@ +json.id @id + +if @name + json.name @name +end + +if @email + json.email @email +end + +if @admin + json.admin @admin +end + +json.verified @verified +json.created_at @created_at +json.updated_at @updated_at diff --git a/app/views/auth/unsupported.html.erb b/app/views/auth/unsupported.html.erb index 988a149..df2e98f 100644 --- a/app/views/auth/unsupported.html.erb +++ b/app/views/auth/unsupported.html.erb @@ -1,3 +1,5 @@

Unsupported Browser

-

Your browser doesn't support passkeys, how you access your Obl.ong account. Please upgrade to a supported browser [list browsers]

\ No newline at end of file +

Your browser doesn't support passkeys, how you access your Obl.ong account. Please upgrade to a supported browser [list browsers]

+ +Continue anyway \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 022b97c..3cde42f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -21,7 +21,7 @@ class Application < Rails::Application config.action_mailer.delivery_method = :postmark - config.sentry = true + config.sentry = false config.action_mailer.postmark_settings = { api_token: Rails.application.credentials.postmark_api_token @@ -36,6 +36,6 @@ class Application < Rails::Application config.assets.paths << Rails.root.join("app/javascript") - config.slack_notify = true + config.slack_notify = false end end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index d1d2aea..be6b31c 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -7,10 +7,12 @@ # This block will be called to check whether the resource owner is authenticated or not. resource_owner_authenticator do - raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" # Put your resource owner authentication logic here. # Example implementation: # User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url) + session[:return_path] = request.fullpath + User::User.find_by(id: session[:current_user_id]) || redirect_to("/auth/login") + end # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb @@ -18,16 +20,18 @@ # adding oauth authorized applications. In other case it will return 403 Forbidden response # every time somebody will try to access the admin web interface. # - # admin_authenticator do - # # Put your admin authentication logic here. - # # Example implementation: - # - # if current_user - # head :forbidden unless current_user.admin? - # else - # redirect_to sign_in_url - # end - # end + admin_authenticator do + # Put your admin authentication logic here. + # Example implementation: + current_user = User::User.find_by(id: session[:current_user_id]) + + if current_user + head :forbidden unless current_user.admin? + else + session[:return_path] = request.fullpath + redirect_to("/auth/login") + end + end # You can use your own model classes if you need to extend (or even override) default # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. @@ -233,6 +237,10 @@ # default_scopes :public # optional_scopes :write, :update + default_scopes :user + + optional_scopes :openid, :domains, :domains_write, :domains_records, :domains_records_write, :email, :email_write, :name, :name_write, :admin + # Allows to restrict only certain scopes for grant_type. # By default, all the scopes will be available for all the grant types. # @@ -246,7 +254,7 @@ # not in configuration, i.e. +default_scopes+ or +optional_scopes+. # (disabled by default) # - # enforce_configured_scopes + enforce_configured_scopes # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb new file mode 100644 index 0000000..a3d05a2 --- /dev/null +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +Doorkeeper::OpenidConnect.configure do + issuer do |resource_owner, application| + "https://admin.obl.ong" + end + + signing_key Rails.application.credentials.oidc_key + + subject_types_supported [:pairwise] + + subject do |resource_owner, application| + Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{Rails.application.credentials.oidc_salt}") + end + + resource_owner_from_access_token do |access_token| + User::User.find_by(id: access_token.resource_owner_id) + end + + auth_time_from_resource_owner do |resource_owner| + # Example implementation: + # resource_owner.current_sign_in_at + end + + reauthenticate_resource_owner do |resource_owner, return_to| + # Example implementation: + # store_location_for resource_owner, return_to + # sign_out resource_owner + # redirect_to new_user_session_url + end + + # Depending on your configuration, a DoubleRenderError could be raised + # if render/redirect_to is called at some point before this callback is executed. + # To avoid the DoubleRenderError, you could add these two lines at the beginning + # of this callback: (Reference: https://github.com/rails/rails/issues/25106) + # self.response_body = nil + # @_response_body = nil + select_account_for_resource_owner do |resource_owner, return_to| + # Example implementation: + # store_location_for resource_owner, return_to + # redirect_to account_select_url + end + + subject do |resource_owner, application| + # Example implementation: + # resource_owner.id + + # or if you need pairwise subject identifier, implement like below: + # Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{'your_secret_salt'}") + end + + # Protocol to use when generating URIs for the discovery endpoint, + # for example if you also use HTTPS in development + # protocol do + # :https + # end + + # Expiration time on or after which the ID Token MUST NOT be accepted for processing. (default 120 seconds). + # expiration 600 + + # Example claims: + # claims do + # normal_claim :_foo_ do |resource_owner| + # resource_owner.foo + # end + + # normal_claim :_bar_ do |resource_owner| + # resource_owner.bar + # end + # end +end diff --git a/config/locales/doorkeeper_openid_connect.en.yml b/config/locales/doorkeeper_openid_connect.en.yml new file mode 100644 index 0000000..1bed506 --- /dev/null +++ b/config/locales/doorkeeper_openid_connect.en.yml @@ -0,0 +1,23 @@ +en: + doorkeeper: + scopes: + openid: 'Authenticate your account' + profile: 'View your profile information' + email: 'View your email address' + address: 'View your physical address' + phone: 'View your phone number' + errors: + messages: + login_required: 'The authorization server requires end-user authentication' + consent_required: 'The authorization server requires end-user consent' + interaction_required: 'The authorization server requires end-user interaction' + account_selection_required: 'The authorization server requires end-user account selection' + openid_connect: + errors: + messages: + # Configuration error messages + resource_owner_from_access_token_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.resource_owner_from_access_token missing configuration.' + auth_time_from_resource_owner_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.auth_time_from_resource_owner missing configuration.' + reauthenticate_resource_owner_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.reauthenticate_resource_owner missing configuration.' + select_account_for_resource_owner_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.select_account_for_resource_owner missing configuration.' + subject_not_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.subject missing configuration.' diff --git a/config/locales/en.yml b/config/locales/en.yml index 8ca56fc..b7b788d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -31,3 +31,16 @@ en: hello: "Hello world" + doorkeeper: + scopes: + openid: 'Read identity data for authentication to a 3rd party service' + domains: 'Read your domains' + domains_write: 'Request and destroy your domains' + domains_records: 'Read your domains’ records' + domains_records_write: 'Create, change, and destroy your domains’ records' + email: 'Read your email address' + email_write: 'Change your email address' + name: 'Read your full name' + name_write: 'Change your full name' + admin: 'Read your administrator status' + user: 'Read your user ID, verification status, and when your account was created & last updated' \ No newline at end of file diff --git a/config/puma.rb b/config/puma.rb index 0cee716..86d6720 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -44,4 +44,4 @@ # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart -plugin :solid_queue +plugin :solid_queue if ENV.fetch("RAILS_ENV", "production") == "production" diff --git a/config/routes.rb b/config/routes.rb index 990c632..2eb7e63 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ # typed: true Rails.application.routes.draw do + use_doorkeeper_openid_connect use_doorkeeper # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html @@ -54,4 +55,16 @@ # Defines the root path route ("/") root "domains#index" + + namespace :api do + namespace :v1 do + resources :domains, param: :host do + member do + resources :records, module: "domains" + end + end + get "user", to: "user#show" + patch "user", to: "user#update" + end + end end diff --git a/db/migrate/20240118133216_create_doorkeeper_openid_connect_tables.rb b/db/migrate/20240118133216_create_doorkeeper_openid_connect_tables.rb new file mode 100644 index 0000000..93cfd60 --- /dev/null +++ b/db/migrate/20240118133216_create_doorkeeper_openid_connect_tables.rb @@ -0,0 +1,15 @@ +class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration[7.1] + def change + create_table :oauth_openid_requests do |t| + t.references :access_grant, null: false, index: true + t.string :nonce, null: false + end + + add_foreign_key( + :oauth_openid_requests, + :oauth_access_grants, + column: :access_grant_id, + on_delete: :cascade + ) + end +end diff --git a/db/schema.rb b/db/schema.rb index ece9924..9a71f15 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_01_14_085755) do +ActiveRecord::Schema[7.1].define(version: 2024_01_18_133216) do create_table "domains", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -21,6 +21,54 @@ t.index ["user_users_id"], name: "index_domains_on_user_users_id" end + create_table "oauth_access_grants", force: :cascade do |t| + t.integer "resource_owner_id", null: false + t.integer "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.index ["application_id"], name: "index_oauth_access_grants_on_application_id" + t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true + end + + create_table "oauth_access_tokens", force: :cascade do |t| + t.integer "resource_owner_id" + t.integer "application_id", null: false + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.string "scopes" + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.string "previous_refresh_token", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true + end + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true + end + + create_table "oauth_openid_requests", force: :cascade do |t| + t.integer "access_grant_id", null: false + t.string "nonce", null: false + t.index ["access_grant_id"], name: "index_oauth_openid_requests_on_access_grant_id" + end + create_table "solid_cache_entries", force: :cascade do |t| t.binary "key", limit: 1024, null: false t.binary "value", limit: 536870912, null: false @@ -147,6 +195,11 @@ end add_foreign_key "domains", "user_users", column: "user_users_id" + add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_grants", "user_users", column: "resource_owner_id" + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_tokens", "user_users", column: "resource_owner_id" + add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", on_delete: :cascade add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade From 5ac80c1ab71d5d6495f21757883c615446ea07f8 Mon Sep 17 00:00:00 2001 From: Reese Armstrong Date: Fri, 19 Jan 2024 00:07:27 -0600 Subject: [PATCH 04/20] sync main -> api (#49) From a5a0792240bf56cce29d0501fa099f69f235e851 Mon Sep 17 00:00:00 2001 From: reesericci Date: Sun, 21 Jan 2024 00:56:57 +0000 Subject: [PATCH 05/20] UI+ --- Gemfile | 2 +- Gemfile.lock | 12 +- .../stylesheets/application.tailwind.css | 22 ++- app/controllers/application_controller.rb | 4 +- .../developers/applications_controller.rb | 16 +++ app/helpers/developer/applications_helper.rb | 2 + app/helpers/developers/applications_helper.rb | 2 + app/models/user/user.rb | 22 +-- .../api/v1/domains/_domain.json.jbuilder | 2 +- .../developers/applications/index.html.erb | 45 ++++++ .../developers/applications/show.html.erb | 109 +++++++++++++++ .../applications/_application.html.erb | 16 +++ app/views/domains/_domain.html.erb | 2 +- app/views/domains/index.html.erb | 15 +- .../applications/_delete_form.html.erb | 6 + .../doorkeeper/applications/_form.html.erb | 59 ++++++++ .../doorkeeper/applications/edit.html.erb | 5 + .../doorkeeper/applications/index.html.erb | 38 +++++ .../doorkeeper/applications/new.html.erb | 5 + .../doorkeeper/applications/show.html.erb | 63 +++++++++ .../doorkeeper/authorizations/error.html.erb | 9 ++ .../authorizations/form_post.html.erb | 15 ++ .../doorkeeper/authorizations/new.html.erb | 131 ++++++++++++++++++ .../doorkeeper/authorizations/show.html.erb | 7 + .../_delete_form.html.erb | 4 + .../authorized_applications/index.html.erb | 24 ++++ app/views/layouts/admin.html.erb | 8 ++ app/views/layouts/doorkeeper/admin.html.erb | 39 ++++++ .../layouts/doorkeeper/application.html.erb | 23 +++ config/initializers/doorkeeper.rb | 2 +- .../initializers/doorkeeper_openid_connect.rb | 36 ++++- config/locales/doorkeeper.en.yml | 4 +- config/locales/en.yml | 22 +-- config/routes.rb | 5 + ...20240119180309_add_owner_to_application.rb | 9 ++ db/schema.rb | 5 +- .../developer/applications_controller_test.rb | 7 + .../applications_controller_test.rb | 7 + 38 files changed, 760 insertions(+), 44 deletions(-) create mode 100644 app/controllers/developers/applications_controller.rb create mode 100644 app/helpers/developer/applications_helper.rb create mode 100644 app/helpers/developers/applications_helper.rb create mode 100644 app/views/developers/applications/index.html.erb create mode 100644 app/views/developers/applications/show.html.erb create mode 100644 app/views/developers/doorkeeper/applications/_application.html.erb create mode 100644 app/views/doorkeeper/applications/_delete_form.html.erb create mode 100644 app/views/doorkeeper/applications/_form.html.erb create mode 100644 app/views/doorkeeper/applications/edit.html.erb create mode 100644 app/views/doorkeeper/applications/index.html.erb create mode 100644 app/views/doorkeeper/applications/new.html.erb create mode 100644 app/views/doorkeeper/applications/show.html.erb create mode 100644 app/views/doorkeeper/authorizations/error.html.erb create mode 100644 app/views/doorkeeper/authorizations/form_post.html.erb create mode 100644 app/views/doorkeeper/authorizations/new.html.erb create mode 100644 app/views/doorkeeper/authorizations/show.html.erb create mode 100644 app/views/doorkeeper/authorized_applications/_delete_form.html.erb create mode 100644 app/views/doorkeeper/authorized_applications/index.html.erb create mode 100644 app/views/layouts/doorkeeper/admin.html.erb create mode 100644 app/views/layouts/doorkeeper/application.html.erb create mode 100644 db/migrate/20240119180309_add_owner_to_application.rb create mode 100644 test/controllers/developer/applications_controller_test.rb create mode 100644 test/controllers/developers/applications_controller_test.rb diff --git a/Gemfile b/Gemfile index e5b52f4..b10b781 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby "3.1.2" +ruby "3.3.0" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 7.1.2" diff --git a/Gemfile.lock b/Gemfile.lock index 340d1aa..d9c7948 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,6 +183,7 @@ GEM marcel (1.0.2) matrix (0.4.2) mini_mime (1.1.5) + mini_portile2 (2.8.5) minitest (5.20.0) msgpack (1.7.2) multi_xml (0.6.0) @@ -198,9 +199,8 @@ GEM net-protocol netrc (0.11.0) nio4r (2.7.0) - nokogiri (1.15.5-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.15.5-x86_64-linux) + nokogiri (1.15.5) + mini_portile2 (~> 2.8.2) racc (~> 1.4) openssl (3.2.0) openssl-signature_algorithm (1.3.0) @@ -357,8 +357,8 @@ GEM sorbet-static-and-runtime (>= 0.5.10187) syntax_tree (>= 6.1.1) thor (>= 0.19.2) - sqlite3 (1.6.9-x86_64-darwin) - sqlite3 (1.6.9-x86_64-linux) + sqlite3 (1.6.9) + mini_portile2 (~> 2.8.0) standard (1.33.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) @@ -488,7 +488,7 @@ DEPENDENCIES webdrivers RUBY VERSION - ruby 3.1.2p20 + ruby 3.3.0p0 BUNDLED WITH 2.5.2 diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 345ed96..46599cf 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -194,6 +194,10 @@ .side-menu > * > span { @apply font-heading text-3; } + + .side-menu > a { + @apply font-heading text-3; + } .side-menu > * > svg { @apply fill-pink w-10 h-10 xl:w-12 xl:h-12; } @@ -208,7 +212,7 @@ @apply text-yellow font-heading text-1 md:text-2 xl:text-3 text-left; } .table > tbody { - @apply bg-[#444343]; + @apply bg-[#262626]; } .table td { @apply border-collapse border-y text-1 md:text-2 px-6 py-3 md:py-4; @@ -269,14 +273,18 @@ button, input[type=submit] { min-width: min-content; } -input[type=text], input[type=email], input[type=number] { - background-color: rgba(245, 245, 245, 0.1) !important; +input[type=text], input[type=email], input[type=number], .input2 { + background-color: #262626 !important; border: 1.5px solid var(--cultured)!important; border-radius: 5px !important; min-width: 350px; color: white; } +.admin > input[type=text], input[type=email], input[type=number], .input2 { + background-color: rgba(245, 245, 245, 0.1) !important; +} + input[type=number] { min-width: 100px !important; width: 100px !important; @@ -326,3 +334,11 @@ input[type=number] { .outline-pink-border:hover { background-color: var(--winter-sky) !important; } + +.card { + transition: ease-in-out 0.25s; +} + +.card:hover { + transform: translateY(-1rem); +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index afd76de..32eebd0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,7 +10,7 @@ def current_user def check_auth if session[:authenticated] != true - redirect_to controller: "auth", action: "login" + redirect_to controller: "/auth", action: "login" end end @@ -18,7 +18,7 @@ def check_verified if !session[:authenticated] check_auth elsif !current_user.verified? - redirect_to controller: "users", action: "email_verification", params: {skip_passkey: true} + redirect_to controller: "/users", action: "email_verification", params: {skip_passkey: true} end end end diff --git a/app/controllers/developers/applications_controller.rb b/app/controllers/developers/applications_controller.rb new file mode 100644 index 0000000..fca1da5 --- /dev/null +++ b/app/controllers/developers/applications_controller.rb @@ -0,0 +1,16 @@ +class Developers::ApplicationsController < ApplicationController + nested_layouts "layouts/admin", "layouts/application" + + before_action do + @developers = true + end + + def index + @user = current_user + @applications = current_user.oauth_applications + end + + def show + @application = Doorkeeper::Application.find_by(id: params[:id]) + end +end diff --git a/app/helpers/developer/applications_helper.rb b/app/helpers/developer/applications_helper.rb new file mode 100644 index 0000000..6f76d71 --- /dev/null +++ b/app/helpers/developer/applications_helper.rb @@ -0,0 +1,2 @@ +module Developer::ApplicationsHelper +end diff --git a/app/helpers/developers/applications_helper.rb b/app/helpers/developers/applications_helper.rb new file mode 100644 index 0000000..cf60cee --- /dev/null +++ b/app/helpers/developers/applications_helper.rb @@ -0,0 +1,2 @@ +module Developers::ApplicationsHelper +end diff --git a/app/models/user/user.rb b/app/models/user/user.rb index 76deace..6872911 100644 --- a/app/models/user/user.rb +++ b/app/models/user/user.rb @@ -2,16 +2,18 @@ class User::User < ApplicationRecord validates :email, uniqueness: true # standard:disable all has_many :user_credentials # standard:disable all - has_many :access_grants, - class_name: 'Doorkeeper::AccessGrant', - foreign_key: :resource_owner_id, - dependent: :delete_all - - has_many :access_tokens, - class_name: 'Doorkeeper::AccessToken', - foreign_key: :resource_owner_id, - dependent: :delete_all - + has_many :access_grants, #standard:disable all + class_name: "Doorkeeper::AccessGrant", + foreign_key: :resource_owner_id, + dependent: :delete_all + + has_many :access_tokens, #standard:disable all + class_name: "Doorkeeper::AccessToken", + foreign_key: :resource_owner_id, + dependent: :delete_all + + has_many :oauth_applications, class_name: "Doorkeeper::Application", as: :owner #standard:disable all + after_initialize do @hotp = ROTP::HOTP.new(hotp_token) end diff --git a/app/views/api/v1/domains/_domain.json.jbuilder b/app/views/api/v1/domains/_domain.json.jbuilder index 3b2d590..a4c0938 100644 --- a/app/views/api/v1/domains/_domain.json.jbuilder +++ b/app/views/api/v1/domains/_domain.json.jbuilder @@ -6,5 +6,5 @@ json.user_id d.user_users_id json.provisional d.provisional json.plan d.plan if records - json.records d.records, partial: "records/record", as: :r + json.records d.records, partial: "/api/v1/domains/records/record", as: :r end diff --git a/app/views/developers/applications/index.html.erb b/app/views/developers/applications/index.html.erb new file mode 100644 index 0000000..10fc993 --- /dev/null +++ b/app/views/developers/applications/index.html.erb @@ -0,0 +1,45 @@ +<% content_for :head do %> + +<% end %> + +
+ +

← Back to Obl.ong

+

Manage your applications

+ +
+ <% @applications.each do |application| %> + <% cache application do %> + <%= render application %> + <% end %> + <% end %> + + +
+ +
+

+ Request Application +

+
+ + <% if @user.admin? %> + <%= form_with url: developers_applications_path, method: :post do |form| %> +
+ <%= form.label :host, "Host:" %> + <%= form.text_field :host %> + <%= form.submit "Create domain" %> +
+ <% end %> + <% end %> +
+ +
+ + \ No newline at end of file diff --git a/app/views/developers/applications/show.html.erb b/app/views/developers/applications/show.html.erb new file mode 100644 index 0000000..7c1729e --- /dev/null +++ b/app/views/developers/applications/show.html.erb @@ -0,0 +1,109 @@ +<%# https://icons.hackclub.com/ %> +
+
+ <%= link_to "Credentials", anchor: "credentials" %> + <%= link_to "Scopes", anchor: "scopes" %> + <%= link_to "Redirect URLs", anchor: "redirect_urls" %> + <%= link_to "App Info", anchor: "info" %> + <%= link_to "Danger Zone", anchor: "danger_zone" %> +
+
+

+ <%= @application.name %> +

+
+

Credentials

+
+ + + <%= @application.uid %> + + +
+ +
+ + + <%= @application.secret %> + + +
+ + +
+ +
+

Scopes

+
+ +
+

Redirect URLs

+
+ +
+

App Info

+
+ +
+

Danger Zone

+
+
+
+ + \ No newline at end of file diff --git a/app/views/developers/doorkeeper/applications/_application.html.erb b/app/views/developers/doorkeeper/applications/_application.html.erb new file mode 100644 index 0000000..025fe43 --- /dev/null +++ b/app/views/developers/doorkeeper/applications/_application.html.erb @@ -0,0 +1,16 @@ + +
+
+ +
+

+ <%= application.name %> +

+
+
+
+ Live + Manage → +
+
+
\ No newline at end of file diff --git a/app/views/domains/_domain.html.erb b/app/views/domains/_domain.html.erb index b43b0ab..c1e8305 100644 --- a/app/views/domains/_domain.html.erb +++ b/app/views/domains/_domain.html.erb @@ -1,4 +1,4 @@ - href="<%= domain_path(domain) %>" <% end %> class="flex flex-col p-4 items-start gap-[1.375rem] bg-text text-black shadow-md rounded-2xl h-full text-ellipsis overflow-clip"> + href="<%= domain_path(domain) %>" <% end %> class="flex flex-col p-4 items-start gap-[1.375rem] bg-text text-black shadow-md rounded-2xl h-full text-ellipsis overflow-clip card">

<%= domain.host %><%= "." + Rails.application.config.domain %>

diff --git a/app/views/domains/index.html.erb b/app/views/domains/index.html.erb index d6f6e9d..ef6aead 100644 --- a/app/views/domains/index.html.erb +++ b/app/views/domains/index.html.erb @@ -13,7 +13,7 @@ <% end %> <% end %> -
+
@@ -24,7 +24,7 @@ <% if @user.admin? %> <%= form_with url: domains_path, method: :post do |form| %> -
<%= form.label :host, "Host:" %> <%= form.text_field :host %> <%= form.submit "Create domain" %> @@ -33,6 +33,8 @@ <% end %>
+
Developers → + diff --git a/app/views/doorkeeper/applications/_delete_form.html.erb b/app/views/doorkeeper/applications/_delete_form.html.erb new file mode 100644 index 0000000..654fb2a --- /dev/null +++ b/app/views/doorkeeper/applications/_delete_form.html.erb @@ -0,0 +1,6 @@ +<%- submit_btn_css ||= 'btn btn-link' %> +<%= form_tag oauth_application_path(application), method: :delete do %> + <%= submit_tag t('doorkeeper.applications.buttons.destroy'), + onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')", + class: submit_btn_css %> +<% end %> diff --git a/app/views/doorkeeper/applications/_form.html.erb b/app/views/doorkeeper/applications/_form.html.erb new file mode 100644 index 0000000..de86503 --- /dev/null +++ b/app/views/doorkeeper/applications/_form.html.erb @@ -0,0 +1,59 @@ +<%= form_for application, url: doorkeeper_submit_path(application), as: :doorkeeper_application, html: { role: 'form' } do |f| %> + <% if application.errors.any? %> +

<%= t('doorkeeper.applications.form.error') %>

+ <% end %> + +
+ <%= f.label :name, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_field :name, class: "form-control #{ 'is-invalid' if application.errors[:name].present? }", required: true %> + <%= doorkeeper_errors_for application, :name %> +
+
+ +
+ <%= f.label :redirect_uri, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_area :redirect_uri, class: "form-control #{ 'is-invalid' if application.errors[:redirect_uri].present? }" %> + <%= doorkeeper_errors_for application, :redirect_uri %> + + <%= t('doorkeeper.applications.help.redirect_uri') %> + + + <% if Doorkeeper.configuration.allow_blank_redirect_uri?(application) %> + + <%= t('doorkeeper.applications.help.blank_redirect_uri') %> + + <% end %> +
+
+ +
+ <%= f.label :confidential, class: 'col-sm-2 form-check-label font-weight-bold' %> +
+ <%= f.check_box :confidential, class: "checkbox #{ 'is-invalid' if application.errors[:confidential].present? }" %> + <%= doorkeeper_errors_for application, :confidential %> + + <%= t('doorkeeper.applications.help.confidential') %> + +
+
+ +
+ <%= f.label :scopes, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_field :scopes, class: "form-control #{ 'has-error' if application.errors[:scopes].present? }" %> + <%= doorkeeper_errors_for application, :scopes %> + + <%= t('doorkeeper.applications.help.scopes') %> + +
+
+ +
+
+ <%= f.submit t('doorkeeper.applications.buttons.submit'), class: 'btn btn-primary' %> + <%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, class: 'btn btn-secondary' %> +
+
+<% end %> diff --git a/app/views/doorkeeper/applications/edit.html.erb b/app/views/doorkeeper/applications/edit.html.erb new file mode 100644 index 0000000..737186b --- /dev/null +++ b/app/views/doorkeeper/applications/edit.html.erb @@ -0,0 +1,5 @@ +
+

<%= t('.title') %>

+
+ +<%= render 'form', application: @application %> diff --git a/app/views/doorkeeper/applications/index.html.erb b/app/views/doorkeeper/applications/index.html.erb new file mode 100644 index 0000000..3ba2650 --- /dev/null +++ b/app/views/doorkeeper/applications/index.html.erb @@ -0,0 +1,38 @@ +
+

<%= t('.title') %>

+
+ +

<%= link_to t('.new'), new_oauth_application_path, class: 'btn btn-success' %>

+ + + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + + + <% end %> + +
<%= t('.name') %><%= t('.callback_url') %><%= t('.confidential') %><%= t('.actions') %>
+ <%= link_to application.name, oauth_application_path(application) %> + + <%= simple_format(application.redirect_uri) %> + + <%= application.confidential? ? t('doorkeeper.applications.index.confidentiality.yes') : t('doorkeeper.applications.index.confidentiality.no') %> + + <%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %> + + <%= render 'delete_form', application: application %> +
diff --git a/app/views/doorkeeper/applications/new.html.erb b/app/views/doorkeeper/applications/new.html.erb new file mode 100644 index 0000000..737186b --- /dev/null +++ b/app/views/doorkeeper/applications/new.html.erb @@ -0,0 +1,5 @@ +
+

<%= t('.title') %>

+
+ +<%= render 'form', application: @application %> diff --git a/app/views/doorkeeper/applications/show.html.erb b/app/views/doorkeeper/applications/show.html.erb new file mode 100644 index 0000000..540ba48 --- /dev/null +++ b/app/views/doorkeeper/applications/show.html.erb @@ -0,0 +1,63 @@ +
+

<%= t('.title', name: @application.name) %>

+
+ +
+
+

<%= t('.application_id') %>:

+

<%= @application.uid %>

+ +

<%= t('.secret') %>:

+

+ + <% secret = flash[:application_secret].presence || @application.plaintext_secret %> + <% if secret.blank? && Doorkeeper.config.application_secret_hashed? %> + <%= t('.secret_hashed') %> + <% else %> + <%= secret %> + <% end %> + +

+ +

<%= t('.scopes') %>:

+

+ + <% if @application.scopes.present? %> + <%= @application.scopes %> + <% else %> + <%= t('.not_defined') %> + <% end %> + +

+ +

<%= t('.confidential') %>:

+

<%= @application.confidential? %>

+ +

<%= t('.callback_urls') %>:

+ + <% if @application.redirect_uri.present? %> + + <% @application.redirect_uri.split.each do |uri| %> + + + + + <% end %> +
+ <%= uri %> + + <%= link_to t('doorkeeper.applications.buttons.authorize'), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'btn btn-success', target: '_blank' %> +
+ <% else %> + <%= t('.not_defined') %> + <% end %> +
+ +
+

<%= t('.actions') %>

+ +

<%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(@application), class: 'btn btn-primary' %>

+ +

<%= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger' %>

+
+
diff --git a/app/views/doorkeeper/authorizations/error.html.erb b/app/views/doorkeeper/authorizations/error.html.erb new file mode 100644 index 0000000..7198ec3 --- /dev/null +++ b/app/views/doorkeeper/authorizations/error.html.erb @@ -0,0 +1,9 @@ +
+

<%= t('doorkeeper.authorizations.error.title') %>

+
+ +
+
+    <%= (respond_to?(:error_response) ? error_response : @pre_auth.error_response).body[:error_description] %>
+  
+
diff --git a/app/views/doorkeeper/authorizations/form_post.html.erb b/app/views/doorkeeper/authorizations/form_post.html.erb new file mode 100644 index 0000000..5e142a8 --- /dev/null +++ b/app/views/doorkeeper/authorizations/form_post.html.erb @@ -0,0 +1,15 @@ + + +<%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false do %> + <% @authorize_response.body.compact.each do |key, value| %> + <%= hidden_field_tag key, value %> + <% end %> +<% end %> + + diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb new file mode 100644 index 0000000..ddd09c6 --- /dev/null +++ b/app/views/doorkeeper/authorizations/new.html.erb @@ -0,0 +1,131 @@ +
+
+ <%= image_tag "oval.svg", size: "200" %> +

<%= raw t('.title', client_name: content_tag(:strong, class: 'text-info') { @pre_auth.client.name }) %>

+

+ <%= raw t('.prompt', client_name: content_tag(:strong, class: 'text-info') { @pre_auth.client.name }) %> +

+ + <% if @pre_auth.scopes.count > 0 %> +
+

<%= t('.able_to') %>:

+ +
    + <% @pre_auth.scopes.each do |scope| %> +
  • ✓ <%= raw t scope, scope: [:doorkeeper, :scopes] %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form_tag oauth_authorization_path, method: :post do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> + <%= hidden_field_tag :state, @pre_auth.state, id: nil %> + <%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %> + <%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %> + <%= hidden_field_tag :scope, @pre_auth.scope, id: nil %> + <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> + <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> + <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %> + <% end %> + <%= form_tag oauth_authorization_path, method: :delete do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> + <%= hidden_field_tag :state, @pre_auth.state, id: nil %> + <%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %> + <%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %> + <%= hidden_field_tag :scope, @pre_auth.scope, id: nil %> + <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> + <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> + <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %> + <% end %> +
+
+
+ + \ No newline at end of file diff --git a/app/views/doorkeeper/authorizations/show.html.erb b/app/views/doorkeeper/authorizations/show.html.erb new file mode 100644 index 0000000..f4d6610 --- /dev/null +++ b/app/views/doorkeeper/authorizations/show.html.erb @@ -0,0 +1,7 @@ + + +
+ <%= params[:code] %> +
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.erb b/app/views/doorkeeper/authorized_applications/_delete_form.html.erb new file mode 100644 index 0000000..512e8ec --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.erb @@ -0,0 +1,4 @@ +<%- submit_btn_css ||= 'btn btn-link' %> +<%= form_tag oauth_authorized_application_path(application), method: :delete do %> + <%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %> +<% end %> diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb new file mode 100644 index 0000000..a3f5aaa --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -0,0 +1,24 @@ + + +
+ + + + + + + + + + <% @applications.each do |application| %> + + + + + + <% end %> + +
<%= t('doorkeeper.authorized_applications.index.application') %><%= t('doorkeeper.authorized_applications.index.created_at') %>
<%= application.name %><%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %><%= render 'delete_form', application: application %>
+
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 5db2c7a..4eeedf0 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -44,3 +44,11 @@ } + +<% if @developers %> + +<% end %> diff --git a/app/views/layouts/doorkeeper/admin.html.erb b/app/views/layouts/doorkeeper/admin.html.erb new file mode 100644 index 0000000..3d1aead --- /dev/null +++ b/app/views/layouts/doorkeeper/admin.html.erb @@ -0,0 +1,39 @@ + + + + + + + <%= t('doorkeeper.layouts.admin.title') %> + <%= stylesheet_link_tag "doorkeeper/admin/application" %> + <%= csrf_meta_tags %> + + + + +
+ <%- if flash[:notice].present? %> +
+ <%= flash[:notice] %> +
+ <% end -%> + + <%= yield %> +
+ + diff --git a/app/views/layouts/doorkeeper/application.html.erb b/app/views/layouts/doorkeeper/application.html.erb new file mode 100644 index 0000000..df75a6b --- /dev/null +++ b/app/views/layouts/doorkeeper/application.html.erb @@ -0,0 +1,23 @@ + + + + <%= t('doorkeeper.layouts.application.title') %> + + + + + <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> + <%= csrf_meta_tags %> + + +
+ <%- if flash[:notice].present? %> +
+ <%= flash[:notice] %> +
+ <% end -%> + + <%= yield %> +
+ + diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index be6b31c..9092763 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -228,7 +228,7 @@ # NOTE: you must also run the rails g doorkeeper:application_owner generator # to provide the necessary support # - # enable_application_owner confirmation: false + enable_application_owner confirmation: true # Define access token scopes for your provider # For more information go to diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb index a3d05a2..96d845f 100644 --- a/config/initializers/doorkeeper_openid_connect.rb +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -26,7 +26,11 @@ # Example implementation: # store_location_for resource_owner, return_to # sign_out resource_owner - # redirect_to new_user_session_url + session[:authenticated] = false + session[:current_user_id] = nil + @_current_user = nil + session[:return_path] = return_to + redirect_to "/auth/login" end # Depending on your configuration, a DoubleRenderError could be raised @@ -43,7 +47,7 @@ subject do |resource_owner, application| # Example implementation: - # resource_owner.id + resource_owner.id # or if you need pairwise subject identifier, implement like below: # Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{'your_secret_salt'}") @@ -67,5 +71,31 @@ # normal_claim :_bar_ do |resource_owner| # resource_owner.bar # end - # end + # + + claims do + claim :name, scope: :name do |resource_owner| + resource_owner.name + end + + claim :email, scope: :email do |resource_owner| + resource_owner.email + end + + claim :email_verified, scope: :email do |resource_owner| + resource_owner.verified + end + + claim :verified, scope: :user do |resource_owner| + resource_owner.verified + end + + claim :created_at, scope: :user do |resource_owner| + resource_owner.created_at + end + + claim :updated_at, scope: :user do |resource_owner| + resource_owner.updated_at + end + end end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 99fa3d4..e9ad053 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -67,8 +67,8 @@ en: error: title: 'An error has occurred' new: - title: 'Authorization required' - prompt: 'Authorize %{client_name} to use your account?' + title: 'Authorize %{client_name}' + prompt: '%{client_name} would like access to your Obl.ong account.' able_to: 'This application will be able to' show: title: 'Authorization code' diff --git a/config/locales/en.yml b/config/locales/en.yml index b7b788d..f526315 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,14 +33,14 @@ en: hello: "Hello world" doorkeeper: scopes: - openid: 'Read identity data for authentication to a 3rd party service' - domains: 'Read your domains' - domains_write: 'Request and destroy your domains' - domains_records: 'Read your domains’ records' - domains_records_write: 'Create, change, and destroy your domains’ records' - email: 'Read your email address' - email_write: 'Change your email address' - name: 'Read your full name' - name_write: 'Change your full name' - admin: 'Read your administrator status' - user: 'Read your user ID, verification status, and when your account was created & last updated' \ No newline at end of file + openid: 'Read your user ID' + domains: 'Read your domains' + domains_write: 'Request and destroy your domains' + domains_records: 'Read your domains’ records' + domains_records_write: 'Create, change, and destroy your domains’ records' + email: 'Read your email address and verification status' + email_write: 'Change your email address' + name: 'Read your full name' + name_write: 'Change your full name' + admin: 'Read your administrator status' + user: 'Read your user ID, verification status, and when your account was created & last updated' \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 2eb7e63..abfca58 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,4 +67,9 @@ patch "user", to: "user#update" end end + + namespace :developers do + get "/", to: redirect("developers/applications") + resources :applications + end end diff --git a/db/migrate/20240119180309_add_owner_to_application.rb b/db/migrate/20240119180309_add_owner_to_application.rb new file mode 100644 index 0000000..729a944 --- /dev/null +++ b/db/migrate/20240119180309_add_owner_to_application.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddOwnerToApplication < ActiveRecord::Migration[7.1] + def change + add_column :oauth_applications, :owner_id, :bigint, null: true + add_column :oauth_applications, :owner_type, :string, null: true + add_index :oauth_applications, [:owner_id, :owner_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index 9a71f15..c8dcbdc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_01_18_133216) do +ActiveRecord::Schema[7.1].define(version: 2024_01_19_180309) do create_table "domains", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -60,6 +60,9 @@ t.boolean "confidential", default: true, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "owner_id" + t.string "owner_type" + t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end diff --git a/test/controllers/developer/applications_controller_test.rb b/test/controllers/developer/applications_controller_test.rb new file mode 100644 index 0000000..5f15b97 --- /dev/null +++ b/test/controllers/developer/applications_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class Developer::ApplicationsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/developers/applications_controller_test.rb b/test/controllers/developers/applications_controller_test.rb new file mode 100644 index 0000000..2304832 --- /dev/null +++ b/test/controllers/developers/applications_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class Developers::ApplicationsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end From 2cd04f63974c950f79954aa8852d1ba22562d6b2 Mon Sep 17 00:00:00 2001 From: reesericci Date: Mon, 22 Jan 2024 21:08:38 +0000 Subject: [PATCH 06/20] latest updates --- .prettierrc | 4 + .../stylesheets/application.tailwind.css | 48 +++++- .../developers/applications_controller.rb | 5 + app/javascript/application.js | 3 +- .../controllers/clipboard_controller.js | 14 ++ .../developers/applications/show.html.erb | 155 ++++++++++++++++-- app/views/layouts/application.html.erb | 2 +- config/.application.rb.swp | Bin 0 -> 12288 bytes config/environments/development.rb | 8 +- config/importmap.rb | 3 + config/routes.rb | 6 +- 11 files changed, 226 insertions(+), 22 deletions(-) create mode 100644 .prettierrc create mode 100644 app/javascript/controllers/clipboard_controller.js create mode 100644 config/.application.rb.swp diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 46599cf..5bf606a 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -273,7 +273,7 @@ button, input[type=submit] { min-width: min-content; } -input[type=text], input[type=email], input[type=number], .input2 { +input[type=text], input[type=email], input[type=number], .input2, x-selectmenu::part(button) { background-color: #262626 !important; border: 1.5px solid var(--cultured)!important; border-radius: 5px !important; @@ -281,7 +281,7 @@ input[type=text], input[type=email], input[type=number], .input2 { color: white; } -.admin > input[type=text], input[type=email], input[type=number], .input2 { +.admin > input[type=text], input[type=email], input[type=number], .input2, x-selectmenu::part(button) { background-color: rgba(245, 245, 245, 0.1) !important; } @@ -342,3 +342,47 @@ input[type=number] { .card:hover { transform: translateY(-1rem); } + + +.clipbutton { + border-radius: 4px; + font-size: 1rem; + padding: 0.5rem; + background-color: var(--lemon-glacier); + color: #262626; + transition: ease-in-out 0.25s; +} + +.clipbutton:hover { + transform: translateX(-4px); +} + +.clipbutton.shaking { + animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; + animation-fill-mode: forwards; +} + +@keyframes shake { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } +} + +[behavior="selected-value"] > .description { + display: none; +} \ No newline at end of file diff --git a/app/controllers/developers/applications_controller.rb b/app/controllers/developers/applications_controller.rb index fca1da5..84cf886 100644 --- a/app/controllers/developers/applications_controller.rb +++ b/app/controllers/developers/applications_controller.rb @@ -13,4 +13,9 @@ def index def show @application = Doorkeeper::Application.find_by(id: params[:id]) end + + def add_scope + @application = Doorkeeper::Application.find_by(id: params[:id]) + @application.update!(scopes: Doorkeeper::OAuth::Scopes.from_array([@application.scopes, params[:scope]])) + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 5221498..f3bd889 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,2 +1,3 @@ import "controllers" -import "@hotwired/turbo-rails" \ No newline at end of file +import "@hotwired/turbo-rails" + diff --git a/app/javascript/controllers/clipboard_controller.js b/app/javascript/controllers/clipboard_controller.js new file mode 100644 index 0000000..0d647d8 --- /dev/null +++ b/app/javascript/controllers/clipboard_controller.js @@ -0,0 +1,14 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = [ "source", "button" ] + + copy() { + navigator.clipboard.writeText(this.sourceTarget.getAttribute("value")) + this.buttonTarget.classList.add("shaking"); + + setTimeout(() => { + this.buttonTarget.classList.remove("shaking"); + }, 820); + } +} \ No newline at end of file diff --git a/app/views/developers/applications/show.html.erb b/app/views/developers/applications/show.html.erb index 7c1729e..b6b28b1 100644 --- a/app/views/developers/applications/show.html.erb +++ b/app/views/developers/applications/show.html.erb @@ -13,27 +13,74 @@

Credentials

-
+
- + <%= @application.uid %> - +
-
+
- + <%= @application.secret %> - +
- -

Scopes

+ +
+ <%= form_with method: :patch, url: add_scope_developers_application_path(@application.id), id: "add_scope" do |form| %> + + + <% end %> +
+ + + + + + + + + <% @application.scopes.each do |s| %> + + + + + <% end %> + +
ScopeDescription +
<%= s %><%= raw t("doorkeeper.scopes.#{s}") %>
@@ -50,6 +97,8 @@ + + \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1770177..d68d23b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,7 +5,7 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> - <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "application", "inter-font", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> <%= javascript_include_tag "application", "data-turbo-track": "reload" %> <%= yield :head %> diff --git a/config/.application.rb.swp b/config/.application.rb.swp new file mode 100644 index 0000000000000000000000000000000000000000..69c0387ee9d01382068cb0e911e65e954ef16c3c GIT binary patch literal 12288 zcmeI2&x;&I6vr##N%IT6=s|eSi0;ayC((mAL~tFA1e8Ebyh&Tr{ieIJ-BoQ>^{g8W z68#?(Z(h9%o;(J;3Hk@czpkV^!fjfGeZ0h{Q~_AeGPpG?Ln_Y=b%TS?@kNx74#+a4s;3n z`;-trK;J{Rp-a$P&{^mV^w&ut{)Fy8UqH8@PoX{NB=j3%`4##G`W(6qy$Q{sG4u=+ z;(ZE=S0oS#L;{gOBoGNi0+B!@@V^jXoeQG=m8hRoO&r{kT$ttl-?O()^&PAuB6MXYWxWQZf~s4O)Zs%kvTiy?NAzNSS)HbQg!!BEcyZv6n zLD#m%s-h36^uBSk>GVGqZ3eg%RcV|L5nbtLg9)A*Yw|V=v%Y3YTGEeVb2mG&|8y4@HgAt(f-0ygN$zN|<(kqt$sRHOn8h z%JR;Oeajctw2j*p Date: Wed, 24 Jan 2024 15:12:32 +0000 Subject: [PATCH 07/20] Provisional Apps --- app/assets/images/oblong-dev.png | Bin 0 -> 27371 bytes app/controllers/admin_controller.rb | 17 ++- app/controllers/api/v1/api_controller.rb | 9 +- .../developers/applications_controller.rb | 107 +++++++++++++- app/javascript/application.js | 2 +- .../controllers/formselect_controller.js | 14 ++ app/javascript/tinder.js | 14 +- app/mailers/developers/application_mailer.rb | 6 + app/views/admin/developers_review.html.erb | 138 ++++++++++++++++++ app/views/admin/index.html.erb | 1 + app/views/admin/review.html.erb | 6 +- .../app_created_email.text.erb | 7 + .../developers/applications/index.html.erb | 12 +- .../developers/applications/request.html.erb | 54 +++++++ .../developers/applications/show.html.erb | 92 ++++++++++-- .../applications/_application.html.erb | 9 +- app/views/layouts/admin.html.erb | 14 +- .../layouts/doorkeeper/application.html.erb | 2 +- config/.application.rb.swp | Bin 12288 -> 0 bytes config/importmap.rb | 4 +- config/initializers/doorkeeper.rb | 13 +- config/routes.rb | 36 ++++- ...245_add_provisonal_plan_to_applications.rb | 6 + db/schema.rb | 4 +- 24 files changed, 506 insertions(+), 61 deletions(-) create mode 100644 app/assets/images/oblong-dev.png create mode 100644 app/javascript/controllers/formselect_controller.js create mode 100644 app/mailers/developers/application_mailer.rb create mode 100644 app/views/admin/developers_review.html.erb create mode 100644 app/views/developers/application_mailer/app_created_email.text.erb create mode 100644 app/views/developers/applications/request.html.erb delete mode 100644 config/.application.rb.swp create mode 100644 db/migrate/20240123203245_add_provisonal_plan_to_applications.rb diff --git a/app/assets/images/oblong-dev.png b/app/assets/images/oblong-dev.png new file mode 100644 index 0000000000000000000000000000000000000000..456d07a92d4b6281575c70c94dba9e052a9047c2 GIT binary patch literal 27371 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 +

Review Apps

+ + + + <%= form_with method: :post, class: "image", "ng-controller": "CardCtrl", id: "form-{{card.resource_id}}" do |form| %> +

{{card.name}}

+

PLAN

+

{{card.plan}}

+

REDIRECT

+

{{card.uri}}

+
+
+ + + <% end %> +
+
+

No more domains to review

+
+
+
REJECT
+
APPROVE
+
+ + +<% content_for :head do %> + + + + + + +<%= javascript_include_tag "tinder", "data-turbo-track": "reload" %> +<% end %> + + diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index f175d3e..7275cf6 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -5,6 +5,7 @@
  • - View Users
  • - View Domains
  • - Review Domain Requests
  • +
  • - Review Developer App Requests
  • diff --git a/app/views/admin/review.html.erb b/app/views/admin/review.html.erb index 44e7b8a..acfb79a 100644 --- a/app/views/admin/review.html.erb +++ b/app/views/admin/review.html.erb @@ -3,13 +3,13 @@ - <%= form_with method: :post, class: "image", "ng-controller": "CardCtrl", id: "form-{{card.domain_id}}" do |form| %> -

    {{card.host}}.obl.ong

    + <%= form_with method: :post, class: "image", "ng-controller": "CardCtrl", id: "form-{{card.resource_id}}" do |form| %> +

    {{card.name}}.obl.ong

    PLAN

    {{card.plan}}

    - + <% end %>
    diff --git a/app/views/developers/application_mailer/app_created_email.text.erb b/app/views/developers/application_mailer/app_created_email.text.erb new file mode 100644 index 0000000..25fde10 --- /dev/null +++ b/app/views/developers/application_mailer/app_created_email.text.erb @@ -0,0 +1,7 @@ +Your app has been created! +========================== + +Your app <%= @app %>, is now available in your account! + + +If you need any help, don't hesistate to contact team@obl.ong. \ No newline at end of file diff --git a/app/views/developers/applications/index.html.erb b/app/views/developers/applications/index.html.erb index 10fc993..fe4b9ea 100644 --- a/app/views/developers/applications/index.html.erb +++ b/app/views/developers/applications/index.html.erb @@ -2,7 +2,7 @@ <% end %> -
    +

    ← Back to Obl.ong

    Manage your applications

    @@ -26,9 +26,13 @@ <% if @user.admin? %> <%= form_with url: developers_applications_path, method: :post do |form| %>
    - <%= form.label :host, "Host:" %> - <%= form.text_field :host %> - <%= form.submit "Create domain" %> + <%= form.label :name, "Name:" %> + <%= form.text_field :name %> +
    + <%= form.label :redirect_uri, "Redirect URI" %> + <%= form.text_field :redirect_uri %> + + <%= form.submit "Create app" %>
    <% end %> <% end %> diff --git a/app/views/developers/applications/request.html.erb b/app/views/developers/applications/request.html.erb new file mode 100644 index 0000000..35a69c9 --- /dev/null +++ b/app/views/developers/applications/request.html.erb @@ -0,0 +1,54 @@ +

    Request an application

    +

    + +<%= form_with url: provision_developers_applications_path do |form| %> + <%= form.label :name, "What is the name of the app?" %>
    +
    + <%= form.text_field :name, placeholder: "Dynamic" %> +


    + <%= form.label :redirect_uri, "Add a Redirect URI" %>
    +

    This is where we'll redirect after the user authorizes your app

    +
    + <%= form.text_field :redirect_uri, placeholder: "https://oidcdebugger.com/debug" %> +


    + <%= form.label :plan, "What are you planning on using it for?" %>
    +

    Don't worry, it doesn't need to be anything important or serious (it can be if you want though!)


    + <%= form.text_area :plan, placeholder: "I'm going to make a Dynamic DNS app" %> +


    + <%= form.label :coc, "Do you agree to our Code of Conduct?" %>
    +

    We want to make sure our domains aren't used for bad purposes. Please read our Code of Conduct and Acceptable Use Policy: https://github.com/obl-ong/code-of-conduct

    +
    + <%= form.check_box :coc, required: true %> +

    + + <%= form.submit "Request" %> +<% end %> + + + \ No newline at end of file diff --git a/app/views/developers/applications/show.html.erb b/app/views/developers/applications/show.html.erb index b6b28b1..b2040f8 100644 --- a/app/views/developers/applications/show.html.erb +++ b/app/views/developers/applications/show.html.erb @@ -3,7 +3,7 @@
    <%= link_to "Credentials", anchor: "credentials" %> <%= link_to "Scopes", anchor: "scopes" %> - <%= link_to "Redirect URLs", anchor: "redirect_urls" %> + <%= link_to "Redirect URIs", anchor: "redirect_uris" %> <%= link_to "App Info", anchor: "info" %> <%= link_to "Danger Zone", anchor: "danger_zone" %>
    @@ -33,11 +33,14 @@

    Scopes

    -
    - <%= form_with method: :patch, url: add_scope_developers_application_path(@application.id), id: "add_scope" do |form| %> -
    + <%= form.submit "Add" %> +
    + <% end %> @@ -77,31 +80,87 @@ + <% end %>
    <%= s %> <%= raw t("doorkeeper.scopes.#{s}") %> + <%= form_with method: :delete, url: scopes_developers_application_path(id: @application.id), id: "destroy_scope" do |form| %> + <%= form.hidden_field :scope, value: s %> + + <% end %> +
    -
    -

    Redirect URLs

    +
    +

    Redirect URIs

    + <%= form_with method: :post, url: redirect_uris_developers_application_path(id: @application.id), id: "add_scope" do |form| %> +
    + + <%= form.text_field :redirect_uri %> + <%= form.submit "Add" %> +
    + <% end %> + + + + + + + + <% @application.redirect_uri.split("\r\n") do |r| %> + + + + + <% end %> + +
    Redirect URI
    <%= r %> + <%= form_with method: :delete, url: redirect_uris_developers_application_path(id: @application.id), id: "destroy_redirect_url" do |form| %> + <%= form.hidden_field :redirect_uri, value: r %> + + <% end %> +

    App Info

    + + <%= form_with method: :patch, url: developers_application_path(id: @application.id) do |form| %> +
    + + <%= form.text_field :name, value: @application.name %> + <%= form.submit "Update" %> +
    + <% end %>

    Danger Zone

    + + <%= form_with method: :patch, url: developers_application_path(id: @application.id) do |form| %> + +

    +
    + <%= form.check_box :confidential, checked: !@application.confidential %> + <%= form.submit "Update" %> +
    + <% end %> +

    + <%= form_with method: :delete, url: developers_application_path(id: @application.id) do |form| %> + <%= form.submit "Destroy Application", data: {"turbo-confirm": "You sure you want to delete #{@application.name}?"} %> + <% end %>
    - - \ No newline at end of file diff --git a/app/views/developers/doorkeeper/applications/_application.html.erb b/app/views/developers/doorkeeper/applications/_application.html.erb index 025fe43..a459df0 100644 --- a/app/views/developers/doorkeeper/applications/_application.html.erb +++ b/app/views/developers/doorkeeper/applications/_application.html.erb @@ -1,16 +1,17 @@ - +
    -
    - -

    <%= application.name %>

    + <% if application.provisional? then %> + Provisional + <% else %> Live Manage → + <% end %>
    \ No newline at end of file diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 4eeedf0..e629939 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -5,10 +5,14 @@
    - - - - + <% if @developers then %> + <%= image_tag "oblong-dev.png", class:"h-10 xl:h-16" %> + <% else %> + + + + + <% end %>
    Account @@ -19,7 +23,7 @@