yaag 0.0.1 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5392a92361ab06888e9a382e419a2fd2a9c2eb550073476aa255ebafae4093a3
4
- data.tar.gz: d3783baaf6d30da9bf568c7ce585824880c1ca25e6f62f98758f57e7bc0ad47f
3
+ metadata.gz: 2da4ef8b96e8631b9e35d8ffa355325a3f0a0a0dc3c59a439c39e47e624e5e16
4
+ data.tar.gz: 40e8a8882b657f30850118b72a132b9e61296381df6b52c908eaf71022c46cdb
5
5
  SHA512:
6
- metadata.gz: ccfec1abfd5f86d6be13cef7b367fe8f774cfa9e8f00cf94d20ab595dc3a9577a51163cf71070470d79b3773c4efdde016a906e4cb62ba4391b14b7ae08e01a2
7
- data.tar.gz: 45aa07c8b3c8cac0f0063604f4350aaabc43ddf81dc7a2d3e7c43c63177ae136e021df87b0fcf53ead546400b0d992c6a65171c5e412428bf48c56b640a791a1
6
+ metadata.gz: ca92370e1389ed19923f99a192aa3c87a54c0fd6551620b0ea52edef322a9fa6995aafc6d01ea27044326313f988f884488eac4abae50d9803d24ec790a262b0
7
+ data.tar.gz: 1e220e7aa4138f589a274f6a80f4173372087e2fbdf5f7c56dc02683e9951b42f5fefb12a1fd76741a714194c6066c225c6447df32c9737ec68d3ed21d81da9f
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2025 TODO: Write your name
3
+ Copyright (c) 2025 nu12
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,5 @@
1
+ Description:
2
+ Generates a basic passwordless authentication system with users and sessions.
3
+
4
+ Example:
5
+ `bin/rails generate yaag:install`
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/bundle_helper"
4
+
5
+ module Yaag
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base # :nodoc:
8
+ include Rails::Generators::BundleHelper
9
+ source_root File.expand_path("../templates", __FILE__)
10
+
11
+ class_option :api, type: :boolean,
12
+ desc: "Generate API-only controllers and models, with no view templates"
13
+
14
+ # hook_for :template_engine do |template_engine|
15
+ #invoke template_engine unless options.api?
16
+ # end
17
+
18
+ def create_authentication_files
19
+
20
+ template "app/models/session.rb"
21
+ template "app/models/user.rb"
22
+ template "app/models/current.rb"
23
+
24
+ template "app/controllers/signins_controller.rb"
25
+ template "app/controllers/sessions_controller.rb"
26
+ template "app/controllers/concerns/authentication.rb"
27
+
28
+ template "app/channels/application_cable/connection.rb" # if defined?(ActionCable::Engine)
29
+
30
+ copy_file "app/views/sessions/new.html.erb"
31
+ copy_file "app/views/signins/new.html.erb"
32
+
33
+ #if defined?(ActionMailer::Railtie)
34
+ template "app/mailers/signins_mailer.rb"
35
+
36
+ copy_file "app/views/signins_mailer/token.html.erb"
37
+ copy_file "app/views/signins_mailer/token.text.erb"
38
+ #end
39
+
40
+ template "active_model/passwordless_login.rb", "lib/active_model/passwordless_login.rb"
41
+ end
42
+
43
+ def configure_application_controller
44
+ inject_into_class "app/controllers/application_controller.rb", "ApplicationController", " include Authentication\n" # if File.exist? "app/controllers/application_controller.rb"
45
+ end
46
+
47
+ def configure_authentication_routes
48
+ route "resource :signin, only: [:new, :create]" # if File.exist? "config/routes.rb"
49
+ route "resource :session, only: [:new, :destroy] do\n get \"/:token/create\" => \"sessions#create\", as: :create\nend" # if File.exist? "config/routes.rb"
50
+ end
51
+
52
+ def add_migrations
53
+ generate "migration", "CreateUsers", "email_address:string!:uniq", "--force"
54
+ generate "migration", "CreateSessions", "user:references ip_address:string user_agent:string", "--force"
55
+ end
56
+
57
+ def show_readme
58
+ readme "README" if behavior == :invoke
59
+ end
60
+
61
+ #hook_for :test_framework
62
+ end
63
+ end
64
+ end
@@ -0,0 +1 @@
1
+ ===============================================================================
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module PasswordlessLogin
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_LOGIN_TOKEN_EXPIRES_IN = 15.minutes
8
+
9
+ class << self
10
+ end
11
+
12
+ module ClassMethods
13
+ def has_passwordless_login(attribute = :email_address)
14
+ include InstanceMethodsOnActivation.new(attribute)
15
+
16
+ if respond_to?(:generates_token_for)
17
+ login_token_expires_in = DEFAULT_LOGIN_TOKEN_EXPIRES_IN
18
+
19
+ silence_redefinition_of_method(:"#{attribute}_login_token_expires_in")
20
+ define_method(:"#{attribute}_login_token_expires_in") { login_token_expires_in }
21
+
22
+ generates_token_for :"#{attribute}_login", expires_in: login_token_expires_in do
23
+ public_send(:"#{attribute}")&.first(10)
24
+ end
25
+
26
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
27
+ silence_redefinition_of_method :find_by_#{attribute}_login_token
28
+ def self.find_by_#{attribute}_login_token(token)
29
+ find_by_token_for(:#{attribute}_login, token)
30
+ end
31
+
32
+ silence_redefinition_of_method :find_by_#{attribute}_login_token!
33
+ def self.find_by_#{attribute}_login_token!(token)
34
+ find_by_token_for!(:#{attribute}_login, token)
35
+ end
36
+ RUBY
37
+ end
38
+ end
39
+ end
40
+
41
+ class InstanceMethodsOnActivation < Module
42
+ def initialize(attribute)
43
+ attr_reader attribute
44
+
45
+ define_method("#{attribute}_login_token") do
46
+ generate_token_for(:"#{attribute}_login")
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ module ApplicationCable
2
+ class Connection < ActionCable::Connection::Base
3
+ identified_by :current_user
4
+
5
+ def connect
6
+ set_current_user || reject_unauthorized_connection
7
+ end
8
+
9
+ private
10
+ def set_current_user
11
+ if session = Session.find_by(id: cookies.signed[:session_id])
12
+ self.current_user = session.user
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ module Authentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :require_authentication
6
+ helper_method :authenticated?
7
+ end
8
+
9
+ class_methods do
10
+ def allow_unauthenticated_access(**options)
11
+ skip_before_action :require_authentication, **options
12
+ end
13
+ end
14
+
15
+ private
16
+ def authenticated?
17
+ resume_session
18
+ end
19
+
20
+ def require_authentication
21
+ resume_session || request_authentication
22
+ end
23
+
24
+ def resume_session
25
+ Current.session ||= find_session_by_cookie
26
+ end
27
+
28
+ def find_session_by_cookie
29
+ Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
30
+ end
31
+
32
+ def request_authentication
33
+ session[:return_to_after_authenticating] = request.url
34
+ redirect_to new_signin_path
35
+ end
36
+
37
+ def after_authentication_url
38
+ session.delete(:return_to_after_authenticating) || root_url
39
+ end
40
+
41
+ def start_new_session_for(user)
42
+ user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
43
+ Current.session = session
44
+ cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
45
+ end
46
+ end
47
+
48
+ def terminate_session
49
+ Current.session.destroy
50
+ cookies.delete(:session_id)
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ class SessionsController < ApplicationController
2
+ allow_unauthenticated_access only: %i[ new create ]
3
+ before_action :set_user_by_token, only: %i[ create ]
4
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
5
+
6
+ def new
7
+ end
8
+
9
+ def create
10
+ start_new_session_for @user
11
+ redirect_to after_authentication_url
12
+ end
13
+
14
+ def destroy
15
+ terminate_session
16
+ redirect_to new_signin_path, status: :see_other
17
+ end
18
+ private
19
+ def set_user_by_token
20
+ @user = User.find_by_email_address_login_token!(params[:token])
21
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
22
+ redirect_to new_signin_path, alert: "Signin link is invalid or has expired."
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ class SigninsController < ApplicationController
2
+ allow_unauthenticated_access
3
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_signin_path, alert: "Try again later." }
4
+ def new
5
+ end
6
+
7
+ def create
8
+ user = User.find_or_create_by(email_address: params[:email_address])
9
+ SigninsMailer.token(user).deliver_now
10
+ redirect_to new_session_path, notice: "Sign in address sent to provided e-mail."
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ class SigninsMailer < ApplicationMailer
2
+ def token(user)
3
+ @user = user
4
+ mail subject: "Sign in link", to: user.email_address
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ class Current < ActiveSupport::CurrentAttributes
2
+ attribute :session
3
+ delegate :user, to: :session, allow_nil: true
4
+ end
@@ -0,0 +1,3 @@
1
+ class Session < ApplicationRecord
2
+ belongs_to :user
3
+ end
@@ -0,0 +1,7 @@
1
+ class User < ApplicationRecord
2
+ include ActiveModel::PasswordlessLogin
3
+ has_passwordless_login
4
+ has_many :sessions, dependent: :destroy
5
+
6
+ normalizes :email_address, with: ->(e) { e.strip.downcase }
7
+ end
@@ -0,0 +1,2 @@
1
+ <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
2
+ <%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
@@ -0,0 +1,8 @@
1
+ <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
2
+ <%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
3
+
4
+ <%= form_with url: signin_path do |form| %>
5
+ <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
6
+ <%= form.submit "Sign in" %>
7
+ <% end %>
8
+ <br>
@@ -0,0 +1,8 @@
1
+ <% if @user %>
2
+ <p>
3
+ You can reset your password on
4
+ <%= link_to "this password reset page", create_session_url(@user.email_address_login_token) %>.
5
+
6
+ This link will expire in <%= distance_of_time_in_words(0, @user.email_address_login_token_expires_in) %>.
7
+ </p>
8
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <% if @user %>
2
+ You can reset your password on
3
+ <%= create_session_url(@user.email_address_login_token) %>
4
+
5
+ This link will expire in <%= distance_of_time_in_words(0, @user.email_address_login_token_expires_in) %>.
6
+ <% end %>
@@ -0,0 +1,5 @@
1
+ module Yaag
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Yaag
4
+ end
5
+ end
data/lib/yaag/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yaag
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.11"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yaag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - nu12
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-27 00:00:00.000000000 Z
11
+ date: 2026-01-06 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Passwordless authentication.
14
14
  email:
@@ -21,7 +21,24 @@ files:
21
21
  - LICENSE.txt
22
22
  - README.md
23
23
  - Rakefile
24
+ - lib/generators/yaag/USAGE
25
+ - lib/generators/yaag/install_generator.rb
26
+ - lib/generators/yaag/templates/README
27
+ - lib/generators/yaag/templates/active_model/passwordless_login.rb
28
+ - lib/generators/yaag/templates/app/channels/application_cable/connection.rb
29
+ - lib/generators/yaag/templates/app/controllers/concerns/authentication.rb
30
+ - lib/generators/yaag/templates/app/controllers/sessions_controller.rb
31
+ - lib/generators/yaag/templates/app/controllers/signins_controller.rb
32
+ - lib/generators/yaag/templates/app/mailers/signins_mailer.rb
33
+ - lib/generators/yaag/templates/app/models/current.rb
34
+ - lib/generators/yaag/templates/app/models/session.rb
35
+ - lib/generators/yaag/templates/app/models/user.rb
36
+ - lib/generators/yaag/templates/app/views/sessions/new.html.erb
37
+ - lib/generators/yaag/templates/app/views/signins/new.html.erb
38
+ - lib/generators/yaag/templates/app/views/signins_mailer/token.html.erb
39
+ - lib/generators/yaag/templates/app/views/signins_mailer/token.text.erb
24
40
  - lib/yaag.rb
41
+ - lib/yaag/engine.rb
25
42
  - lib/yaag/version.rb
26
43
  - mise.toml
27
44
  - sig/yaag.rbs