mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 19:55:16 +00:00
Add email auth
This commit is contained in:
parent
a55248e842
commit
4271688194
13 changed files with 234 additions and 8 deletions
|
|
@ -48,3 +48,67 @@
|
|||
.project-durations {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.auth-options {
|
||||
max-width: 400px;
|
||||
margin: 2rem auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-button {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.auth-button.slack {
|
||||
background-color: #4A154B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.auth-button.email {
|
||||
background-color: #0070f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
margin: 1rem 0;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-divider::before,
|
||||
.auth-divider::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 45%;
|
||||
height: 1px;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.auth-divider::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.auth-divider::after {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.email-auth-form .field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.email-auth-form input[type="email"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,32 @@ class SessionsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def email
|
||||
email_address = EmailAddress.find_by(email: params[:email].downcase)
|
||||
|
||||
if email_address
|
||||
token = email_address.user.create_email_signin_token
|
||||
AuthMailer.sign_in_email(email_address, token).deliver_later
|
||||
redirect_to root_path, notice: "Check your email for a sign-in link!"
|
||||
else
|
||||
redirect_to root_path, alert: "Email not found. Please sign in with Slack first."
|
||||
end
|
||||
end
|
||||
|
||||
def token
|
||||
valid_token = SignInToken.where(token: params[:token], used_at: nil)
|
||||
.where("expires_at > ?", Time.current)
|
||||
.first
|
||||
|
||||
if valid_token
|
||||
valid_token.mark_used!
|
||||
session[:user_id] = valid_token.user_id
|
||||
redirect_to root_path, notice: "Successfully signed in!"
|
||||
else
|
||||
redirect_to root_path, alert: "Invalid or expired sign-in link"
|
||||
end
|
||||
end
|
||||
|
||||
def impersonate
|
||||
unless current_user.admin?
|
||||
redirect_to root_path, alert: "You are not authorized to impersonate users"
|
||||
|
|
|
|||
11
app/mailers/auth_mailer.rb
Normal file
11
app/mailers/auth_mailer.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class AuthMailer < ApplicationMailer
|
||||
def sign_in_email(email_address, token)
|
||||
@token = token
|
||||
@user = email_address.user
|
||||
|
||||
mail(
|
||||
to: email_address.email,
|
||||
subject: "Your Harbor sign-in link"
|
||||
)
|
||||
end
|
||||
end
|
||||
15
app/models/email_address.rb
Normal file
15
app/models/email_address.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class EmailAddress < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
validates :email, presence: true,
|
||||
uniqueness: true,
|
||||
format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
|
||||
before_validation :downcase_email
|
||||
|
||||
private
|
||||
|
||||
def downcase_email
|
||||
self.email = email.downcase
|
||||
end
|
||||
end
|
||||
31
app/models/sign_in_token.rb
Normal file
31
app/models/sign_in_token.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
class SignInToken < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
enum :auth_type, {
|
||||
email: 0,
|
||||
slack: 1
|
||||
}
|
||||
|
||||
validates :token, presence: true, uniqueness: true
|
||||
validates :auth_type, presence: true
|
||||
validates :expires_at, presence: true
|
||||
|
||||
before_validation :generate_token, on: :create
|
||||
before_validation :set_expiration, on: :create
|
||||
|
||||
scope :valid, -> { where("expires_at > ? AND used_at IS NULL", Time.current) }
|
||||
|
||||
def mark_used!
|
||||
update!(used_at: Time.current)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_token
|
||||
self.token ||= SecureRandom.urlsafe_base64(32)
|
||||
end
|
||||
|
||||
def set_expiration
|
||||
self.expires_at ||= 30.minutes.from_now
|
||||
end
|
||||
end
|
||||
|
|
@ -2,11 +2,12 @@ class User < ApplicationRecord
|
|||
has_paper_trail
|
||||
encrypts :slack_access_token
|
||||
|
||||
validates :email, presence: true, uniqueness: true
|
||||
validates :slack_uid, presence: true, uniqueness: true
|
||||
validates :username, presence: true
|
||||
|
||||
has_many :heartbeats
|
||||
has_many :email_addresses
|
||||
has_many :sign_in_tokens
|
||||
|
||||
has_many :hackatime_heartbeats,
|
||||
foreign_key: :user_id,
|
||||
|
|
@ -171,4 +172,12 @@ class User < ApplicationRecord
|
|||
|
||||
heartbeats.where(project: active_project).duration_seconds
|
||||
end
|
||||
|
||||
def create_email_signin_token
|
||||
sign_in_tokens.create!(auth_type: :email)
|
||||
end
|
||||
|
||||
def find_valid_token(token)
|
||||
sign_in_tokens.valid.find_by(token: token)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
21
app/views/auth_mailer/sign_in_email.html.erb
Normal file
21
app/views/auth_mailer/sign_in_email.html.erb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome back to Harbor!</h1>
|
||||
<p>
|
||||
Click the link below to sign in to your account:
|
||||
</p>
|
||||
<p>
|
||||
<%= link_to 'Sign in to Harbor', auth_token_url(@token.token) %>
|
||||
</p>
|
||||
<p>
|
||||
This link will expire in 30 minutes and can only be used once.
|
||||
</p>
|
||||
<p>
|
||||
If you didn't request this email, you can safely ignore it.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -38,6 +38,13 @@
|
|||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if Rails.env.development? %>
|
||||
<li>
|
||||
<%= link_to letter_opener_path, class: "nav-item #{current_page?(letter_opener_path) ? 'active' : ''}" do %>
|
||||
Letter Opener
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% admin_tool(nil, "li") do %>
|
||||
<%= link_to avo_path, class: "nav-item #{current_page?(avo_path) ? 'active' : ''}" do %>
|
||||
Avo
|
||||
|
|
|
|||
|
|
@ -19,5 +19,18 @@
|
|||
Loading project durations...
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="auth-options">
|
||||
<%= link_to "Sign in with Slack", slack_auth_path, class: "auth-button slack" %>
|
||||
|
||||
<div class="auth-divider">or</div>
|
||||
|
||||
<%= form_tag email_auth_path, class: "email-auth-form" do %>
|
||||
<div class="field">
|
||||
<%= email_field_tag :email, nil, placeholder: "Enter your email", required: true %>
|
||||
</div>
|
||||
<%= submit_tag "Send sign-in link", class: "auth-button email" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -12,10 +12,12 @@
|
|||
<%= form_with model: @user,
|
||||
url: @is_own_settings ? my_settings_path : settings_user_path(@user),
|
||||
method: :patch do |f| %>
|
||||
<div>
|
||||
<%= f.label "Update my Slack status with my current project" %>
|
||||
<%= f.check_box :uses_slack_status %>
|
||||
</div>
|
||||
<fieldset>
|
||||
<label for="switch-1">
|
||||
<input type="checkbox" id="switch-1" name="switch-1" role="switch" checked="">
|
||||
<%= f.label "Update my Slack status with my current project" %>
|
||||
</label>
|
||||
</fieldset>
|
||||
<%= f.submit "Save Settings" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -47,9 +49,12 @@
|
|||
<h2>Config file</h2>
|
||||
<%= render "wakatime_config_display" %>
|
||||
|
||||
<p style="font-size: 0.8rem; color: var(--muted-color);">
|
||||
This file is located in <code>~/.wakatime.cfg</code> on your computer.
|
||||
You can configure it with <a href="https://github.com/wakatime/wakatime-cli/blob/develop/USAGE.md#ini-config-file">other settings</a> as well.
|
||||
<p>
|
||||
<br>
|
||||
<small>
|
||||
This file is located in <code>~/.wakatime.cfg</code> on your computer.
|
||||
You can configure it with <a href="https://github.com/wakatime/wakatime-cli/blob/develop/USAGE.md#ini-config-file">other settings</a> as well.
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ Rails.application.configure do
|
|||
# Set localhost to be used by links generated in mailer templates.
|
||||
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
|
||||
|
||||
# Preview emails in the browser [https://github.com/ryanb/letter_opener]
|
||||
config.action_mailer.delivery_method = :letter_opener
|
||||
config.action_mailer.perform_deliveries = true
|
||||
|
||||
# Print deprecation notices to the Rails logger.
|
||||
config.active_support.deprecation = :log
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,11 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
# Auth routes
|
||||
get "/auth/slack", to: "sessions#new", as: :slack_auth
|
||||
get "/auth/slack/callback", to: "sessions#create"
|
||||
post "/auth/email", to: "sessions#email", as: :email_auth
|
||||
get "/auth/token/:token", to: "sessions#token", as: :auth_token
|
||||
delete "signout", to: "sessions#destroy", as: "signout"
|
||||
|
||||
resources :leaderboards, only: [ :index ]
|
||||
|
|
|
|||
17
todo.md
Normal file
17
todo.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import old data
|
||||
- [ ] make heartbeats work without users
|
||||
- [ ] user creation without slack signin
|
||||
|
||||
get wakatime working
|
||||
- [x] vs code extension not working
|
||||
- [ ] wtf is the summary endpoint doing?
|
||||
- [x] use proxy to mitm the bulk upload endpoint
|
||||
|
||||
summary api
|
||||
- [ ] give api keys for each user w/ different types of access (?)
|
||||
- [ ] give summary endpoint with generated summaries
|
||||
|
||||
email auth
|
||||
- [x] create emails on user
|
||||
- [x] migrate emails to email addresses model
|
||||
- [ ]
|
||||
Loading…
Add table
Reference in a new issue