Add login via nostr (web extension)
This commit is contained in:
@@ -42,6 +42,11 @@
|
||||
focus:ring-red-500 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn-outline-purple {
|
||||
@apply border-2 border-purple-500 hover:bg-purple-100
|
||||
focus:ring-purple-400 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@apply bg-gray-100 hover:bg-gray-200 text-gray-400
|
||||
focus:ring-gray-300 focus:ring-opacity-75;
|
||||
|
||||
62
app/controllers/users/sessions_controller.rb
Normal file
62
app/controllers/users/sessions_controller.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Users::SessionsController < Devise::SessionsController
|
||||
# before_action :configure_sign_in_params, only: [:create]
|
||||
|
||||
# GET /resource/sign_in
|
||||
def new
|
||||
session[:shared_secret] = SecureRandom.base64(12)
|
||||
super
|
||||
end
|
||||
|
||||
# POST /resource/sign_in
|
||||
# def create
|
||||
# super
|
||||
# end
|
||||
|
||||
# DELETE /resource/sign_out
|
||||
# def destroy
|
||||
# super
|
||||
# end
|
||||
|
||||
# POST /users/nostr_login
|
||||
def nostr_login
|
||||
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
||||
|
||||
is_valid_sig = signed_event.verify_signature
|
||||
is_valid_auth = NostrManager::VerifyAuth.call(
|
||||
event: signed_event,
|
||||
challenge: session[:shared_secret]
|
||||
)
|
||||
|
||||
session[:shared_secret] = nil
|
||||
|
||||
unless is_valid_sig && is_valid_auth
|
||||
flash[:alert] = "Login verification failed"
|
||||
http_status :unauthorized and return
|
||||
end
|
||||
|
||||
user = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
|
||||
|
||||
if user.present?
|
||||
set_flash_message!(:notice, :signed_in)
|
||||
sign_in("user", user)
|
||||
render json: { redirect_url: after_sign_in_path_for(user) }, status: :ok
|
||||
else
|
||||
flash[:alert] = "Failed to find your account. Nostr login may be disabled."
|
||||
http_status :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def set_flash_message(key, kind, options = {})
|
||||
# Hide flash message after redirecting from a signin route while logged in
|
||||
super unless key == :alert && kind == "already_authenticated"
|
||||
end
|
||||
|
||||
# If you have extra params to permit, append them to the sanitizer.
|
||||
# def configure_sign_in_params
|
||||
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
|
||||
# end
|
||||
end
|
||||
53
app/javascript/controllers/nostr_login_controller.js
Normal file
53
app/javascript/controllers/nostr_login_controller.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Connects to data-controller="nostr-login"
|
||||
export default class extends Controller {
|
||||
static targets = [ "loginForm", "loginButton" ]
|
||||
static values = { site: String, sharedSecret: String }
|
||||
|
||||
connect() {
|
||||
if (window.nostr) {
|
||||
this.loginButtonTarget.disabled = false
|
||||
this.loginFormTarget.classList.remove("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
async login () {
|
||||
this.loginButtonTarget.disabled = true
|
||||
|
||||
try {
|
||||
// Auth based on NIP-42
|
||||
const signedEvent = await window.nostr.signEvent({
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 22242,
|
||||
tags: [
|
||||
["site", this.siteValue],
|
||||
["challenge", this.sharedSecretValue]
|
||||
],
|
||||
content: ""
|
||||
})
|
||||
|
||||
const res = await fetch("/users/nostr_login", {
|
||||
method: "POST", credentials: "include", headers: {
|
||||
"Accept": "application/json", 'Content-Type': 'application/json',
|
||||
"X-CSRF-Token": this.csrfToken
|
||||
}, body: JSON.stringify({ signed_event: signedEvent })
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
res.json().then(r => { window.location.href = r.redirect_url })
|
||||
} else {
|
||||
window.location.reload()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Unable to authenticate:', error.message)
|
||||
} finally {
|
||||
this.loginButtonTarget.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
get csrfToken () {
|
||||
const element = document.head.querySelector('meta[name="csrf-token"]')
|
||||
return element.getAttribute("content")
|
||||
}
|
||||
}
|
||||
@@ -55,4 +55,27 @@
|
||||
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<div data-controller="nostr-login"
|
||||
data-nostr-login-target="loginForm"
|
||||
data-nostr-login-site-value="<%= Setting.accounts_domain %>"
|
||||
data-nostr-login-shared-secret-value="<%= session[:shared_secret] %>"
|
||||
class="hidden">
|
||||
<div class="relative my-6">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-gray-200"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<span class="bg-white px-2 text-sm text-gray-500 italic">or</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<button disabled tabindex="5"
|
||||
class="w-full btn-md btn-gray text-purple-600"
|
||||
data-nostr-login-target="loginButton"
|
||||
data-action="nostr-login#login">
|
||||
Log in with Nostr
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
Reference in New Issue
Block a user