better_auth-hanami 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f1d70c9e2bfc595306586a4978e2de0b8cd160b1ce1a96c60f2aa49afbe35343
4
+ data.tar.gz: d167171822dbb43b4369b6b8c8b9dfbacd45dce56275fb6a8232b5b198c426d7
5
+ SHA512:
6
+ metadata.gz: 01033aa8d2c01ac516572fa5fdc6b455869fcaa6484569fdfe7788127a98f0612cb6aa1a03c65bb10b906066deaa48dfa00b5beea1f3e4c44988d6b08958e834
7
+ data.tar.gz: b64774ddd9392d6911fd6647aaf34506ee1b10035bb9067a0c1cc3b0d0c7a949215b37c349e7177a6d22ae5a54eeaef3ff2ff0b029e8a298e97180235e4b8f68
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - Initial Hanami 2.3+ adapter with Rack route mounting, Sequel persistence, ROM::SQL migration rendering, action helpers, and Rake/generator commands.
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ The MIT License (MIT)
4
+ Copyright (c) 2024 - present
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+ this software and associated documentation files (the "Software"), to deal in
8
+ the Software without restriction, including without limitation the rights to
9
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10
+ the Software, and to permit persons to whom the Software is furnished to do so,
11
+ subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
20
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
21
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
22
+ OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # Better Auth Hanami
2
+
3
+ Hanami adapter for Better Auth Ruby. It mounts the core Rack auth object inside
4
+ Hanami, uses Hanami's ROM/Sequel database gateway for persistence, renders
5
+ ROM::SQL migrations, generates Hanami relations/repos for app queries, and
6
+ provides action helpers plus generator tasks.
7
+
8
+ ## Installation
9
+
10
+ ```ruby
11
+ gem "better_auth-hanami"
12
+ ```
13
+
14
+ ```bash
15
+ bundle install
16
+ ```
17
+
18
+ ## Setup
19
+
20
+ Load the task file from your app Rakefile if your app does not already load
21
+ `lib/tasks`:
22
+
23
+ ```ruby
24
+ # Rakefile
25
+ require "better_auth/hanami"
26
+ load Gem.loaded_specs.fetch("better_auth-hanami").full_gem_path + "/lib/tasks/better_auth.rake"
27
+ ```
28
+
29
+ Generate the provider, route wiring, task wrapper, settings, relations/repos,
30
+ and base migration:
31
+
32
+ ```bash
33
+ bundle exec rake better_auth:init
34
+ ```
35
+
36
+ Run Hanami migrations:
37
+
38
+ ```bash
39
+ bin/hanami db migrate
40
+ ```
41
+
42
+ When you add plugins that introduce schema tables or fields, regenerate both
43
+ the migration and the app query objects before migrating a new app:
44
+
45
+ ```bash
46
+ bundle exec rake better_auth:generate:migration
47
+ bundle exec rake better_auth:generate:relations
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ The install generator creates `config/providers/better_auth.rb`:
53
+
54
+ ```ruby
55
+ Hanami.app.register_provider(:better_auth) do
56
+ prepare do
57
+ require "better_auth/hanami"
58
+ end
59
+
60
+ start do
61
+ BetterAuth::Hanami.configure do |config|
62
+ config.secret = target["settings"].better_auth_secret
63
+ config.base_url = target["settings"].better_auth_url
64
+ config.base_path = "/api/auth"
65
+ config.database = ->(options) {
66
+ BetterAuth::Hanami::SequelAdapter.from_container(target, options)
67
+ }
68
+ config.email_and_password = {enabled: true}
69
+ config.plugins = []
70
+ end
71
+
72
+ auth = BetterAuth::Hanami.auth
73
+ register "better_auth.auth", auth
74
+ register "better_auth.rack_app", BetterAuth::Hanami::MountedApp.new(auth, mount_path: BetterAuth::Hanami.configuration.base_path)
75
+ end
76
+ end
77
+ ```
78
+
79
+ ## Routes
80
+
81
+ The generated `config/routes.rb` includes:
82
+
83
+ ```ruby
84
+ require "better_auth/hanami/routing"
85
+
86
+ module Bookshelf
87
+ class Routes < Hanami::Routes
88
+ include BetterAuth::Hanami::Routing
89
+
90
+ better_auth
91
+ end
92
+ end
93
+ ```
94
+
95
+ By default this mounts Better Auth at `/api/auth`. Customize the path:
96
+
97
+ ```ruby
98
+ better_auth at: "/auth"
99
+ ```
100
+
101
+ ## Action Helpers
102
+
103
+ Include helpers in your base action:
104
+
105
+ ```ruby
106
+ class Action < Hanami::Action
107
+ include BetterAuth::Hanami::ActionHelpers
108
+ end
109
+ ```
110
+
111
+ Use them from an action:
112
+
113
+ ```ruby
114
+ def handle(request, response)
115
+ return unless require_authentication(request, response)
116
+
117
+ response.body = current_user(request).fetch("email")
118
+ end
119
+ ```
120
+
121
+ ## Relations And Repos
122
+
123
+ Better Auth uses `BetterAuth::Hanami::SequelAdapter` for its own reads and
124
+ writes. The generated Hanami relations/repos are for your application code when
125
+ you want to inspect or query Better Auth tables directly:
126
+
127
+ ```ruby
128
+ users = Hanami.app["relations.users"].to_a
129
+ user = Hanami.app["repos.user_repo"].users.by_pk(user_id).one
130
+ ```
131
+
132
+ If you prefer a custom persistence implementation, configure it directly:
133
+
134
+ ```ruby
135
+ BetterAuth::Hanami.configure do |config|
136
+ config.database = ->(options) { MyBetterAuthAdapter.new(options) }
137
+ end
138
+ ```
139
+
140
+ ## Limitations
141
+
142
+ - Supports Hanami 2.3+ only. Better Auth core depends on Rack 3, and Hanami 2.3 is the first Hanami line that allows Rack 3.
143
+ - Hanami 1.x and Hanami 2.2/Rack 2 apps are out of scope for this adapter.
144
+ - The stable command surface is Rake/generator based. A `hanami better_auth ...` command is not exposed because the current public guides do not document a stable third-party Hanami CLI extension API.
145
+ - Apps created with `--skip-db` can use memory storage for development or tests, but production apps should configure Hanami DB or pass an explicit Better Auth adapter.
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ cd packages/better_auth-hanami
151
+ rbenv exec bundle exec rspec
152
+ rbenv exec bundle exec standardrb
153
+ ```
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Hanami
5
+ module ActionHelpers
6
+ def current_session(request)
7
+ data = better_auth_session_data(request)
8
+ data&.fetch(:session, nil) || data&.fetch("session", nil)
9
+ end
10
+
11
+ def current_user(request)
12
+ data = better_auth_session_data(request)
13
+ data&.fetch(:user, nil) || data&.fetch("user", nil)
14
+ end
15
+
16
+ def authenticated?(request)
17
+ !current_user(request).nil?
18
+ end
19
+
20
+ def require_authentication(request, response)
21
+ return true if authenticated?(request)
22
+
23
+ response.status = 401 if response.respond_to?(:status=)
24
+ false
25
+ end
26
+
27
+ private
28
+
29
+ def better_auth_session_data(request)
30
+ env = request_env(request)
31
+ return env["better_auth.session"] if env.key?("better_auth.session")
32
+
33
+ env["better_auth.session"] = resolve_better_auth_session(request)
34
+ end
35
+
36
+ def resolve_better_auth_session(request)
37
+ context = BetterAuth::Endpoint::Context.new(
38
+ path: request_path(request),
39
+ method: request_method(request),
40
+ query: request_params(request),
41
+ body: {},
42
+ params: {},
43
+ headers: {"cookie" => request_cookie(request)},
44
+ context: BetterAuth::Hanami.auth.context,
45
+ request: request
46
+ )
47
+ BetterAuth::Session.find_current(context, disable_refresh: true)
48
+ end
49
+
50
+ def request_env(request)
51
+ request.respond_to?(:env) ? request.env : {}
52
+ end
53
+
54
+ def request_path(request)
55
+ request.respond_to?(:path) ? request.path : "/"
56
+ end
57
+
58
+ def request_method(request)
59
+ request.respond_to?(:request_method) ? request.request_method : "GET"
60
+ end
61
+
62
+ def request_params(request)
63
+ request.respond_to?(:params) ? request.params : {}
64
+ end
65
+
66
+ def request_cookie(request)
67
+ return request.get_header("HTTP_COOKIE") if request.respond_to?(:get_header)
68
+
69
+ headers = request.respond_to?(:headers) ? request.headers : {}
70
+ headers["cookie"] || headers["Cookie"]
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Hanami
5
+ class Configuration
6
+ AUTH_OPTION_NAMES = %i[
7
+ app_name
8
+ base_url
9
+ base_path
10
+ secret
11
+ database
12
+ plugins
13
+ trusted_origins
14
+ rate_limit
15
+ session
16
+ account
17
+ user
18
+ verification
19
+ advanced
20
+ email_and_password
21
+ password_hasher
22
+ email_verification
23
+ social_providers
24
+ experimental
25
+ secondary_storage
26
+ database_hooks
27
+ hooks
28
+ on_api_error
29
+ disabled_paths
30
+ logger
31
+ ].freeze
32
+
33
+ attr_accessor(*AUTH_OPTION_NAMES)
34
+
35
+ def initialize
36
+ @base_path = BetterAuth::Configuration::DEFAULT_BASE_PATH
37
+ @plugins = []
38
+ @trusted_origins = []
39
+ @database = ->(options) { SequelAdapter.from_hanami(options) }
40
+ end
41
+
42
+ def to_auth_options
43
+ AUTH_OPTION_NAMES.each_with_object({}) do |name, options|
44
+ value = public_send(name)
45
+ next if value.nil?
46
+ next if value.respond_to?(:empty?) && value.empty?
47
+
48
+ options[name] = value
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "migration_generator"
5
+ require_relative "relation_generator"
6
+
7
+ module BetterAuth
8
+ module Hanami
9
+ module Generators
10
+ class InstallGenerator
11
+ def initialize(destination_root: Dir.pwd)
12
+ @destination_root = destination_root
13
+ end
14
+
15
+ def run
16
+ create_provider
17
+ create_task
18
+ update_routes
19
+ update_settings
20
+ RelationGenerator.new(destination_root: destination_root).run
21
+ MigrationGenerator.new(destination_root: destination_root).run
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :destination_root
27
+
28
+ def create_provider
29
+ path = File.join(destination_root, "config/providers/better_auth.rb")
30
+ return if File.exist?(path)
31
+
32
+ FileUtils.mkdir_p(File.dirname(path))
33
+ File.write(path, provider_template)
34
+ end
35
+
36
+ def create_task
37
+ path = File.join(destination_root, "lib/tasks/better_auth.rake")
38
+ return if File.exist?(path)
39
+
40
+ FileUtils.mkdir_p(File.dirname(path))
41
+ File.write(path, task_template)
42
+ end
43
+
44
+ def update_routes
45
+ path = File.join(destination_root, "config/routes.rb")
46
+ return unless File.exist?(path)
47
+
48
+ content = File.read(path)
49
+ content = %(require "better_auth/hanami/routing"\n) + content unless content.include?(%("better_auth/hanami/routing"))
50
+ unless content.include?("include BetterAuth::Hanami::Routing")
51
+ content = content.sub("class Routes < Hanami::Routes\n", "class Routes < Hanami::Routes\n include BetterAuth::Hanami::Routing\n better_auth\n")
52
+ end
53
+ File.write(path, content)
54
+ end
55
+
56
+ def update_settings
57
+ path = File.join(destination_root, "config/settings.rb")
58
+ return unless File.exist?(path)
59
+
60
+ content = File.read(path)
61
+ return if content.include?("setting :better_auth_secret")
62
+
63
+ insertion = [
64
+ " setting :better_auth_secret, constructor: Types::String.constrained(min_size: 32)",
65
+ " setting :better_auth_url, constructor: Types::String.optional"
66
+ ].join("\n")
67
+ content = content.sub("class Settings < Hanami::Settings\n", "class Settings < Hanami::Settings\n#{insertion}\n")
68
+ File.write(path, content)
69
+ end
70
+
71
+ def provider_template
72
+ <<~RUBY
73
+ # frozen_string_literal: true
74
+
75
+ Hanami.app.register_provider(:better_auth) do
76
+ prepare do
77
+ require "better_auth/hanami"
78
+ end
79
+
80
+ start do
81
+ BetterAuth::Hanami.configure do |config|
82
+ config.secret = target["settings"].better_auth_secret
83
+ config.base_url = target["settings"].better_auth_url
84
+ config.base_path = "/api/auth"
85
+ config.database = ->(options) {
86
+ BetterAuth::Hanami::SequelAdapter.from_container(target, options)
87
+ }
88
+ config.trusted_origins = [target["settings"].better_auth_url].compact
89
+ config.email_and_password = {enabled: true}
90
+ config.plugins = []
91
+ end
92
+
93
+ auth = BetterAuth::Hanami.auth
94
+ register "better_auth.auth", auth
95
+ register "better_auth.rack_app", BetterAuth::Hanami::MountedApp.new(auth, mount_path: BetterAuth::Hanami.configuration.base_path)
96
+ end
97
+ end
98
+ RUBY
99
+ end
100
+
101
+ def task_template
102
+ <<~RUBY
103
+ # frozen_string_literal: true
104
+
105
+ require "better_auth/hanami"
106
+
107
+ namespace :better_auth do
108
+ desc "Create Better Auth Hanami provider, routes, settings, tasks, and base migration"
109
+ task :init do
110
+ BetterAuth::Hanami::Generators::InstallGenerator.new.run
111
+ end
112
+
113
+ namespace :generate do
114
+ desc "Create the Better Auth Hanami base migration"
115
+ task :migration do
116
+ BetterAuth::Hanami::Generators::MigrationGenerator.new.run
117
+ end
118
+
119
+ desc "Create Hanami relations and repos for Better Auth tables"
120
+ task :relations do
121
+ BetterAuth::Hanami::Generators::RelationGenerator.new.run
122
+ end
123
+ end
124
+ end
125
+ RUBY
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+
6
+ module BetterAuth
7
+ module Hanami
8
+ module Generators
9
+ class MigrationGenerator
10
+ def initialize(destination_root: Dir.pwd, configuration: nil)
11
+ @destination_root = destination_root
12
+ @configuration = configuration
13
+ end
14
+
15
+ def run
16
+ return migration_path if existing_migration?
17
+
18
+ FileUtils.mkdir_p(File.dirname(migration_path))
19
+ File.write(migration_path, BetterAuth::Hanami::Migration.render(generator_config))
20
+ migration_path
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :destination_root, :configuration
26
+
27
+ def existing_migration?
28
+ Dir[File.join(destination_root, "config/db/migrate/*_create_better_auth_tables.rb")].any?
29
+ end
30
+
31
+ def migration_path
32
+ @migration_path ||= File.join(destination_root, "config/db/migrate", "#{timestamp}_create_better_auth_tables.rb")
33
+ end
34
+
35
+ def timestamp
36
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
37
+ end
38
+
39
+ def generator_config
40
+ return configuration if configuration
41
+
42
+ options = BetterAuth::Hanami.configuration.to_auth_options
43
+ options[:secret] ||= BetterAuth::Configuration::DEFAULT_SECRET
44
+ options[:database] ||= :memory
45
+ BetterAuth::Configuration.new(options)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module BetterAuth
6
+ module Hanami
7
+ module Generators
8
+ class RelationGenerator
9
+ def initialize(destination_root: Dir.pwd, configuration: nil)
10
+ @destination_root = destination_root
11
+ @configuration = configuration
12
+ end
13
+
14
+ def run
15
+ create_base_repo
16
+ generator_config && tables.each_value do |table|
17
+ create_relation(table)
18
+ create_repo(table)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :destination_root, :configuration
25
+
26
+ def create_base_repo
27
+ path = File.join(destination_root, "app/repo.rb")
28
+ return if File.exist?(path)
29
+
30
+ FileUtils.mkdir_p(File.dirname(path))
31
+ File.write(path, base_repo_template)
32
+ end
33
+
34
+ def create_relation(table)
35
+ table_name = table.fetch(:model_name)
36
+ path = File.join(destination_root, "app/relations", "#{table_name}.rb")
37
+ return if File.exist?(path)
38
+
39
+ FileUtils.mkdir_p(File.dirname(path))
40
+ File.write(path, relation_template(table_name))
41
+ end
42
+
43
+ def create_repo(table)
44
+ table_name = table.fetch(:model_name)
45
+ path = File.join(destination_root, "app/repos", "#{singular_name(table_name)}_repo.rb")
46
+ return if File.exist?(path)
47
+
48
+ FileUtils.mkdir_p(File.dirname(path))
49
+ File.write(path, repo_template(table_name))
50
+ end
51
+
52
+ def tables
53
+ BetterAuth::Schema.auth_tables(generator_config)
54
+ end
55
+
56
+ def generator_config
57
+ @generator_config ||= begin
58
+ return configuration if configuration
59
+
60
+ options = BetterAuth::Hanami.configuration.to_auth_options
61
+ options[:secret] ||= BetterAuth::Configuration::DEFAULT_SECRET
62
+ options[:database] ||= :memory
63
+ BetterAuth::Configuration.new(options)
64
+ end
65
+ end
66
+
67
+ def app_namespace
68
+ @app_namespace ||= begin
69
+ candidates = [
70
+ File.join(destination_root, "config/app.rb"),
71
+ File.join(destination_root, "config/routes.rb"),
72
+ File.join(destination_root, "config/settings.rb")
73
+ ]
74
+ candidates.filter_map do |path|
75
+ next unless File.exist?(path)
76
+
77
+ File.read(path).match(/module\s+([A-Z][A-Za-z0-9_:]*)/)&.[](1)
78
+ end.first || "Main"
79
+ end
80
+ end
81
+
82
+ def base_repo_template
83
+ <<~RUBY
84
+ # frozen_string_literal: true
85
+
86
+ module #{app_namespace}
87
+ class Repo < Hanami::DB::Repo
88
+ end
89
+ end
90
+ RUBY
91
+ end
92
+
93
+ def relation_template(table_name)
94
+ <<~RUBY
95
+ # frozen_string_literal: true
96
+
97
+ module #{app_namespace}
98
+ module Relations
99
+ class #{class_name(table_name)} < Hanami::DB::Relation
100
+ schema :#{table_name}, infer: true
101
+ end
102
+ end
103
+ end
104
+ RUBY
105
+ end
106
+
107
+ def repo_template(table_name)
108
+ <<~RUBY
109
+ # frozen_string_literal: true
110
+
111
+ module #{app_namespace}
112
+ module Repos
113
+ class #{class_name(singular_name(table_name))}Repo < Repo[:#{table_name}]
114
+ end
115
+ end
116
+ end
117
+ RUBY
118
+ end
119
+
120
+ def class_name(value)
121
+ value.to_s.split("_").map(&:capitalize).join
122
+ end
123
+
124
+ def singular_name(value)
125
+ value.to_s.sub(/ies\z/, "y").sub(/s\z/, "")
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end