-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generate invitations to enrol new admin users
* Adds new endpoint to handle invitations * Enables generating invitation link to share from admin user show page * Adds a signup page using token details * updates validations on password, so that password is optional
- Loading branch information
1 parent
2f05ae1
commit 65dd55d
Showing
14 changed files
with
328 additions
and
8 deletions.
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
app/assets/javascripts/koi/controllers/clipboard_controller.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Controller } from "@hotwired/stimulus"; | ||
|
||
export default class ClipboardController extends Controller { | ||
static targets = ["source"]; | ||
|
||
static classes = ["supported"]; | ||
|
||
connect() { | ||
if ("clipboard" in navigator) { | ||
this.element.classList.add(this.supportedClass); | ||
} | ||
} | ||
|
||
copy(event) { | ||
event.preventDefault(); | ||
navigator.clipboard.writeText(this.sourceTarget.value); | ||
|
||
this.element.classList.add("copied"); | ||
setTimeout(() => { | ||
this.element.classList.remove("copied"); | ||
}, 2000); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
.copy-to-clipboard { | ||
position: relative; | ||
|
||
.copy-to-clipboard-feedback { | ||
animation: ease-in-out 0.5s; | ||
display: none; | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
left: 0; | ||
margin-top: -1em; | ||
z-index: 10; | ||
white-space: nowrap; | ||
text-align: center; | ||
|
||
&:after { | ||
content: "Link copied to your clipboard!"; | ||
display: inline-block; | ||
padding: 0.33em 1em 0.33em 1em; | ||
border-radius: 3rem; | ||
color: white; | ||
background-color: black; | ||
} | ||
} | ||
|
||
&.copied .copy-to-clipboard-feedback { | ||
display: block; | ||
} | ||
|
||
.clipboard-button { | ||
display: none; | ||
} | ||
|
||
&.clipboard--supported .clipboard-button { | ||
display: initial; | ||
} | ||
} | ||
|
||
.actions .action.copy-to-clipboard { | ||
display: flex; | ||
flex: auto; | ||
|
||
input[type="text"] { | ||
flex: auto; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
@use "actions-group"; | ||
@use "clipboard"; | ||
@use "document-field"; | ||
@use "image-field"; | ||
@use "index-actions"; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# frozen_string_literal: true | ||
|
||
module Admin | ||
class TokensController < ApplicationController | ||
include Koi::Controller::JsonWebToken | ||
|
||
skip_before_action :authenticate_admin, only: %i[show update] | ||
before_action :set_token, only: %i[show update] | ||
|
||
def create | ||
admin = Admin::User.find(params[:id]) | ||
token = encode_token(admin_id: admin.id, exp: 5.minutes.from_now.to_i, iat: Time.now.to_i) | ||
|
||
render locals: { token: } | ||
end | ||
|
||
def update | ||
return redirect_to admin_dashboard_path, status: :see_other if admin_signed_in? | ||
|
||
return redirect_to new_admin_session_path, status: :see_other, notice: "invalid token" if @token.blank? | ||
|
||
admin = Admin::User.find(@token[:admin_id]) | ||
sign_in_admin(admin) | ||
|
||
redirect_to admin_admin_user_path(admin) | ||
end | ||
|
||
def show | ||
return redirect_to new_admin_session_path, notice: "Token invalid or consumed already" if @token.blank? | ||
|
||
admin = Admin::User.find(@token[:admin_id]) | ||
|
||
if token_utilised?(admin, @token) | ||
return redirect_to new_admin_session_path, notice: "Token invalid or consumed already" | ||
end | ||
|
||
render locals: { admin:, token: params[:token] }, layout: "koi/login" | ||
end | ||
|
||
private | ||
|
||
def set_token | ||
@token = decode_token(params[:token]) | ||
end | ||
|
||
def token_utilised?(admin, token) | ||
admin.current_sign_in_at.present? || (admin.last_sign_in_at.present? && admin.last_sign_in_at.to_i > token[:iat]) | ||
end | ||
|
||
def sign_in_admin(admin) | ||
admin.current_sign_in_at = Time.current | ||
admin.current_sign_in_ip = request.remote_ip | ||
admin.sign_in_count = 1 | ||
|
||
# disable validations to allow saving without password or passkey credentials | ||
admin.save!(validate: false) | ||
session[:admin_user_id] = admin.id | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# frozen_string_literal: true | ||
|
||
module Koi | ||
module Controller | ||
module JsonWebToken | ||
extend ActiveSupport::Concern | ||
|
||
SECRET_KEY = Rails.application.secret_key_base | ||
|
||
def encode_token(**payload) | ||
JWT.encode(payload, SECRET_KEY) | ||
end | ||
|
||
def decode_token(token) | ||
payload = JWT.decode(token, SECRET_KEY)[0] | ||
HashWithIndifferentAccess.new(payload) | ||
rescue JWT::DecodeError | ||
nil | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<%= turbo_stream.replace "invite" do %> | ||
<div class="action copy-to-clipboard govuk-input__wrapper" data-controller="clipboard" data-clipboard-supported-class="clipboard--supported"> | ||
<%= text_field_tag :invite_link, admin_token_url(token), readonly: true, data: { clipboard_target: "source" } %> | ||
<button class="govuk-input__suffix clipboard-button" aria-hidden="true" data-action="clipboard#copy"> | ||
Copy link | ||
</button> | ||
<div class="copy-to-clipboard-feedback" role="alert"></div> | ||
</div> | ||
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<%= render "layouts/koi/navigation_header" %> | ||
<%= form_with(url: accept_admin_session_path) do |form| %> | ||
<%= tag.input name: :token, type: :hidden, value: token %> | ||
<p>Welcome to Koi Admin</p> | ||
<%= render Koi::SummaryListComponent.new(model: admin, class: "item-table") do |builder| %> | ||
<%= builder.text :name %> | ||
<%= builder.text :email %> | ||
<% end %> | ||
<div class="actions-group"> | ||
<%= form.admin_save "Sign in" %> | ||
</div> | ||
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# frozen_string_literal: true | ||
|
||
require "rails_helper" | ||
|
||
RSpec.describe Admin::TokensController do | ||
subject { action && response } | ||
|
||
def jwt_token(**payload) | ||
JWT.encode(payload, Rails.application.secret_key_base) | ||
end | ||
|
||
describe "GET /admin/token/:token" do | ||
let(:action) { get admin_token_path(token) } | ||
let(:admin) { create(:admin, password: "") } | ||
let(:token) { jwt_token(admin_id: admin.id, exp: 5.seconds.from_now.to_i, iat: Time.now.to_i) } | ||
|
||
it { is_expected.to be_successful } | ||
|
||
context "with used token" do | ||
let(:admin) { create(:admin, last_sign_in_at: Time.now) } | ||
let(:token) { jwt_token(admin_id: admin.id, exp: 5.seconds.from_now.to_i, iat: 1.hour.ago.to_i) } | ||
|
||
it { is_expected.to redirect_to(new_admin_session_path) } | ||
|
||
it "shows a flash message" do | ||
action | ||
expect(flash[:notice]).to match(/Token invalid or consumed already/) | ||
end | ||
end | ||
|
||
context "with invalid token" do | ||
let(:token) { "token" } | ||
|
||
it { is_expected.to redirect_to(new_admin_session_path) } | ||
|
||
it "shows a flash message" do | ||
action | ||
expect(flash[:notice]).to match(/Token invalid or consumed already/) | ||
end | ||
end | ||
end | ||
|
||
describe "POST /admin/admin_users/:id/invite" do | ||
let(:action) { post invite_admin_admin_user_path(admin), as: :turbo_stream } | ||
let(:admin) { create(:admin) } | ||
|
||
include_context "with admin session" | ||
|
||
it_behaves_like "requires admin" | ||
|
||
it { is_expected.to be_successful } | ||
|
||
it "renders the token" do | ||
action | ||
expect(response.body).to have_css("turbo-stream[action=replace][target='invite']") | ||
end | ||
end | ||
|
||
describe "POST /admin/session/accept" do | ||
let(:action) { post accept_admin_session_path, params: { token: } } | ||
let(:admin) { create(:admin, password: "") } | ||
let(:token) { jwt_token(admin_id: admin.id, exp: 5.seconds.from_now.to_i, iat: Time.now.to_i) } | ||
|
||
it { is_expected.to redirect_to(admin_admin_user_path(admin)) } | ||
|
||
it "updates the admin login details" do | ||
expect { action }.to change { admin.reload.current_sign_in_at }.from(nil).to be_present | ||
end | ||
|
||
context "when admin is signed in" do | ||
let(:admin) { create(:admin) } | ||
|
||
include_context "with admin session" | ||
|
||
it { is_expected.to redirect_to(admin_dashboard_path) } | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# frozen_string_literal: true | ||
|
||
require "rails_helper" | ||
|
||
RSpec.describe "admin/invites" do | ||
def encode_token(**args) | ||
JWT.encode(args, Rails.application.secret_key_base) | ||
end | ||
|
||
it "creates an invitation" do | ||
admin = create(:admin) | ||
visit "/admin" | ||
|
||
fill_in "Email", with: admin.email | ||
fill_in "Password", with: admin.password | ||
click_on "Log in" | ||
|
||
visit "/admin/admin_users/new" | ||
|
||
fill_in "Email", with: "[email protected]" | ||
fill_in "Name", with: "John Doe" | ||
|
||
click_button "Save" | ||
|
||
expect(page).to have_css("button", text: "Invite") | ||
|
||
click_button "Invite" | ||
|
||
expect(page).to have_css("input[type=text][value*=token]") | ||
end | ||
|
||
it "can accept an invitation" do | ||
admin = create(:admin, password: "") | ||
token = encode_token(admin_id: admin.id, exp: 1.hour.from_now.to_i, ist: Time.now.to_i) | ||
|
||
visit "admin/token/#{token}" | ||
|
||
expect(page).to have_content(/Welcome to Koi Admin/) | ||
|
||
click_button "Sign in" | ||
|
||
expect(page).to have_current_path(admin_admin_user_path(admin)) | ||
end | ||
end |