mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 22:15:14 +00:00
may i remind you, mr. wofford, you're under OAuth (#284)
This commit is contained in:
parent
3831b52fad
commit
9dfabf49f9
20 changed files with 487 additions and 11 deletions
2
Gemfile
2
Gemfile
|
|
@ -124,3 +124,5 @@ group :test do
|
|||
end
|
||||
|
||||
gem "htmlcompressor", "~> 0.4.0"
|
||||
|
||||
gem "doorkeeper", "~> 5.8"
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@ GEM
|
|||
reline (>= 0.3.8)
|
||||
device_detector (1.1.3)
|
||||
domain_name (0.6.20240107)
|
||||
doorkeeper (5.8.2)
|
||||
railties (>= 5)
|
||||
dotenv (3.1.8)
|
||||
dotenv-rails (3.1.8)
|
||||
dotenv (= 3.1.8)
|
||||
|
|
@ -527,6 +529,7 @@ DEPENDENCIES
|
|||
capybara
|
||||
countries
|
||||
debug
|
||||
doorkeeper (~> 5.8)
|
||||
dotenv-rails
|
||||
flamegraph
|
||||
geocoder
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ aside.nav {
|
|||
transform: translateX(0);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1000;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.nav > ul {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
module Api
|
||||
module V1
|
||||
module Authenticated
|
||||
class ApplicationController < ActionController::API
|
||||
include Doorkeeper::Rails::Helpers
|
||||
before_action :doorkeeper_authorize!
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
@current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/controllers/api/v1/authenticated/me_controller.rb
Normal file
14
app/controllers/api/v1/authenticated/me_controller.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
module Api
|
||||
module V1
|
||||
module Authenticated
|
||||
class MeController < ApplicationController
|
||||
def index
|
||||
render json: {
|
||||
emails: current_user.email_addresses&.map(&:email)|| [],
|
||||
slack_id: current_user.slack_uid
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,7 +2,7 @@ class SessionsController < ApplicationController
|
|||
def new
|
||||
redirect_uri = url_for(action: :create, only_path: false)
|
||||
Rails.logger.info "Starting Slack OAuth flow with redirect URI: #{redirect_uri}"
|
||||
redirect_to User.authorize_url(redirect_uri, close_window: params[:close_window].present?),
|
||||
redirect_to User.authorize_url(redirect_uri, close_window: params[:close_window].present?, continue_param: params[:continue]),
|
||||
host: "https://slack.com",
|
||||
allow_other_host: "https://slack.com"
|
||||
end
|
||||
|
|
@ -30,6 +30,8 @@ class SessionsController < ApplicationController
|
|||
state = JSON.parse(params[:state]) rescue {}
|
||||
if state["close_window"]
|
||||
redirect_to close_window_path
|
||||
elsif state["continue"]
|
||||
redirect_to state["continue"], notice: "Successfully signed in with Slack!"
|
||||
else
|
||||
redirect_to root_path, notice: "Successfully signed in with Slack!"
|
||||
end
|
||||
|
|
@ -82,11 +84,12 @@ class SessionsController < ApplicationController
|
|||
|
||||
def email
|
||||
email = params[:email].downcase
|
||||
continue_param = params[:continue]
|
||||
|
||||
if Rails.env.production?
|
||||
HandleEmailSigninJob.perform_later(email)
|
||||
HandleEmailSigninJob.perform_later(email, continue_param)
|
||||
else
|
||||
HandleEmailSigninJob.perform_now(email)
|
||||
HandleEmailSigninJob.perform_now(email, continue_param)
|
||||
end
|
||||
|
||||
redirect_to root_path(sign_in_email: true), notice: "Check your email for a sign-in link!"
|
||||
|
|
@ -142,7 +145,12 @@ class SessionsController < ApplicationController
|
|||
if valid_token
|
||||
valid_token.mark_used!
|
||||
session[:user_id] = valid_token.user_id
|
||||
redirect_to root_path, notice: "Successfully signed in!"
|
||||
|
||||
if valid_token.continue_param.present?
|
||||
redirect_to valid_token.continue_param, notice: "Successfully signed in!"
|
||||
else
|
||||
redirect_to root_path, notice: "Successfully signed in!"
|
||||
end
|
||||
else
|
||||
redirect_to root_path, alert: "Invalid or expired link"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -76,6 +76,11 @@ class StaticPagesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def minimal_login
|
||||
@continue_param = params[:continue] if params[:continue].present?
|
||||
render :minimal_login, layout: "doorkeeper/application"
|
||||
end
|
||||
|
||||
def mini_leaderboard
|
||||
@leaderboard = Leaderboard.where.associated(:entries)
|
||||
.where(start_date: Date.current)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
class HandleEmailSigninJob < ApplicationJob
|
||||
queue_as :latency_10s
|
||||
|
||||
def perform(email)
|
||||
def perform(email, continue_param = nil)
|
||||
email_address = ActiveRecord::Base.transaction do
|
||||
EmailAddress.find_by(email: email) || begin
|
||||
user = User.create!
|
||||
|
|
@ -9,7 +9,7 @@ class HandleEmailSigninJob < ApplicationJob
|
|||
end
|
||||
end
|
||||
|
||||
token = email_address.user.create_email_signin_token.token
|
||||
token = email_address.user.create_email_signin_token(continue_param: continue_param).token
|
||||
LoopsMailer.sign_in_email(email_address.email, token).deliver_now
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -257,10 +257,11 @@ class User < ApplicationRecord
|
|||
})
|
||||
end
|
||||
|
||||
def self.authorize_url(redirect_uri, close_window: false)
|
||||
def self.authorize_url(redirect_uri, close_window: false, continue_param: nil)
|
||||
state = {
|
||||
token: SecureRandom.hex(24),
|
||||
close_window: close_window
|
||||
close_window: close_window,
|
||||
continue: continue_param
|
||||
}.to_json
|
||||
|
||||
params = {
|
||||
|
|
@ -405,8 +406,8 @@ class User < ApplicationRecord
|
|||
heartbeats.where(source_type: :direct_entry).order(time: :desc).first
|
||||
end
|
||||
|
||||
def create_email_signin_token
|
||||
sign_in_tokens.create!(auth_type: :email)
|
||||
def create_email_signin_token(continue_param: nil)
|
||||
sign_in_tokens.create!(auth_type: :email, continue_param: continue_param)
|
||||
end
|
||||
|
||||
def find_valid_token(token)
|
||||
|
|
|
|||
43
app/views/doorkeeper/authorizations/new.html.erb
Normal file
43
app/views/doorkeeper/authorizations/new.html.erb
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<header class="page-header" role="banner" style="margin: 0 auto;">
|
||||
<h1><%= t('.title') %></h1>
|
||||
</header>
|
||||
<p class="h4">
|
||||
<%= raw t('.prompt', client_name: content_tag(:strong, class: 'text-info') { @pre_auth.client.name }) %>
|
||||
</p>
|
||||
|
||||
<% if @pre_auth.scopes.count > 0 %>
|
||||
<div id="oauth-permissions">
|
||||
<p><%= t('.able_to') %>:</p>
|
||||
|
||||
<ul class="text-info">
|
||||
<% @pre_auth.scopes.each do |scope| %>
|
||||
<li><%= t scope, scope: [:doorkeeper, :scopes] %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="actions">
|
||||
<%= 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", style: "background-color: oklch(70.03% 0.194 144.71); border: none" %>
|
||||
<% 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 %>
|
||||
</div>
|
||||
30
app/views/layouts/doorkeeper/application.html.erb
Normal file
30
app/views/layouts/doorkeeper/application.html.erb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('doorkeeper.layouts.application.title') %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<%= stylesheet_link_tag :app %>
|
||||
<%= csrf_meta_tags %>
|
||||
<style>
|
||||
input[type="submit"] {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<div style="margin 0 auto;">
|
||||
<%- if flash[:notice].present? %>
|
||||
<div class="alert alert-info">
|
||||
<%= flash[:notice] %>
|
||||
</div>
|
||||
<% end -%>
|
||||
|
||||
<%= yield %>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -94,5 +94,10 @@
|
|||
GoodJob
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% admin_tool(nil, "li") do %>
|
||||
<%= link_to oauth_applications_path, class: "nav-item #{current_page?(oauth_applications_path) ? 'active' : ''}", data: { action: "click->nav#clickLink" } do %>
|
||||
OAuth2 apps
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</aside>
|
||||
|
|
|
|||
11
app/views/static_pages/minimal_login.html.erb
Normal file
11
app/views/static_pages/minimal_login.html.erb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<header class="page-header" role="banner" style="margin: 0 auto;">
|
||||
<h1>Log in to Hackatime</h1>
|
||||
</header>
|
||||
<%= link_to "Sign in with Hack Club Slack", slack_auth_path(continue: @continue_param), class: "auth-button slack" %>
|
||||
<%= form_tag email_auth_path, class: "email-auth-form", data: { turbo: false } do %>
|
||||
<%= hidden_field_tag :continue, @continue_param if @continue_param %>
|
||||
<div class="field">
|
||||
<%= email_field_tag :email, params[:email], placeholder: "Enter your email", required: true, class: "email-input" %>
|
||||
</div>
|
||||
<%= submit_tag "Send magic link", class: "auth-button email" %>
|
||||
<% end %>
|
||||
21
config/initializers/doorkeeper.rb
Normal file
21
config/initializers/doorkeeper.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Doorkeeper.configure do
|
||||
base_controller "ApplicationController"
|
||||
|
||||
resource_owner_authenticator do
|
||||
current_user || redirect_to(minimal_login_path(continue: request.fullpath))
|
||||
end
|
||||
|
||||
admin_authenticator do
|
||||
if current_user
|
||||
head :forbidden unless current_user.admin?
|
||||
else
|
||||
redirect_to sign_in_url
|
||||
end
|
||||
end
|
||||
|
||||
access_token_expires_in 16.years
|
||||
|
||||
reuse_access_token
|
||||
end
|
||||
|
|
@ -14,4 +14,6 @@ Rails.configuration.to_prepare do
|
|||
# Monkeypatch Airtable rate limit to be more conservative
|
||||
Norairrecord::Client.send(:remove_const, :AIRTABLE_RPS_LIMIT) if Norairrecord::Client.const_defined?(:AIRTABLE_RPS_LIMIT)
|
||||
Norairrecord::Client.const_set(:AIRTABLE_RPS_LIMIT, 2) # Set to 2 requests per second
|
||||
|
||||
Doorkeeper::ApplicationsController.layout "application" # show oauth2 admin in normal hackatime ui
|
||||
end
|
||||
|
|
|
|||
157
config/locales/doorkeeper.en.yml
Normal file
157
config/locales/doorkeeper.en.yml
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
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_code_challenge: 'Code challenge is required.'
|
||||
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:
|
||||
zero: 'The authorization server does not support PKCE as there are no accepted code_challenge_method values.'
|
||||
one: 'The code_challenge_method must be %{challenge_methods}.'
|
||||
other: 'The code_challenge_method must be one of %{challenge_methods}.'
|
||||
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'
|
||||
scopes:
|
||||
read: View basic info about your Hackatime account
|
||||
|
|
@ -8,6 +8,8 @@ class AdminConstraint
|
|||
end
|
||||
|
||||
Rails.application.routes.draw do
|
||||
use_doorkeeper
|
||||
|
||||
constraints AdminConstraint do
|
||||
mount GoodJob::Engine => "good_job"
|
||||
mount AhoyCaptain::Engine => "/ahoy_captain"
|
||||
|
|
@ -56,6 +58,8 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
get "/minimal_login", to: "static_pages#minimal_login", as: :minimal_login
|
||||
|
||||
# Auth routes
|
||||
get "/auth/slack", to: "sessions#new", as: :slack_auth
|
||||
get "/auth/slack/callback", to: "sessions#create"
|
||||
|
|
@ -127,6 +131,10 @@ Rails.application.routes.draw do
|
|||
get "heartbeats/most_recent", to: "heartbeats#most_recent"
|
||||
get "heartbeats", to: "heartbeats#index"
|
||||
end
|
||||
|
||||
namespace :authenticated do
|
||||
resources :me, only: [ :index ]
|
||||
end
|
||||
end
|
||||
|
||||
# wakatime compatible summary
|
||||
|
|
|
|||
99
db/migrate/20250608195535_create_doorkeeper_tables.rb
Normal file
99
db/migrate/20250608195535_create_doorkeeper_tables.rb
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateDoorkeeperTables < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :oauth_applications do |t|
|
||||
t.string :name, null: false
|
||||
t.string :uid, null: false
|
||||
# Remove `null: false` or use conditional constraint if you are planning to use public clients.
|
||||
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, <model>, column: :resource_owner_id
|
||||
# add_foreign_key :oauth_access_tokens, <model>, column: :resource_owner_id
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class AddContinueParamToSignInTokens < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :sign_in_tokens, :continue_param, :string
|
||||
end
|
||||
end
|
||||
47
db/schema.rb
generated
47
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_05_31_120000) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_08_205244) do
|
||||
create_schema "pganalyze"
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
|
|
@ -299,6 +299,48 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_31_120000) do
|
|||
t.index ["airtable_id"], name: "index_neighborhood_ysws_submissions_on_airtable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "oauth_access_grants", force: :cascade do |t|
|
||||
t.bigint "resource_owner_id", null: false
|
||||
t.bigint "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.bigint "resource_owner_id"
|
||||
t.bigint "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 "physical_mails", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.integer "mission_type", null: false
|
||||
|
|
@ -401,6 +443,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_31_120000) do
|
|||
t.datetime "used_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "continue_param"
|
||||
t.index ["token"], name: "index_sign_in_tokens_on_token"
|
||||
t.index ["user_id"], name: "index_sign_in_tokens_on_user_id"
|
||||
end
|
||||
|
|
@ -463,6 +506,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_31_120000) do
|
|||
add_foreign_key "leaderboard_entries", "leaderboards"
|
||||
add_foreign_key "leaderboard_entries", "users"
|
||||
add_foreign_key "mailing_addresses", "users"
|
||||
add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id"
|
||||
add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id"
|
||||
add_foreign_key "physical_mails", "users"
|
||||
add_foreign_key "project_repo_mappings", "repositories"
|
||||
add_foreign_key "project_repo_mappings", "users"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue