Compare commits

..

13 commits

Author SHA1 Message Date
James f478010699 Update .gitignore 2025-06-25 22:42:56 -05:00
James 77bdebd0b1 Update README.md 2024-11-07 17:46:21 -06:00
James a9f4b05b16 Update README.md 2024-11-04 23:25:40 -06:00
James 2024a8367b link to latest bulma 1.x 2024-11-02 23:08:12 -05:00
James 0cd5093976 Update .gitignore 2024-11-02 22:36:20 -05:00
James 4f3d003a35 Update views/users.erb 2024-11-01 17:03:48 -05:00
James 9f00a34f7d Update views/layout.erb 2024-11-01 17:01:38 -05:00
James 15e6c8dff8 Update views/index.erb 2024-11-01 17:00:55 -05:00
James 3d679586d8 Update views/greeting.erb 2024-11-01 16:59:26 -05:00
James 10bcb49b56 Update README.md 2024-11-01 15:50:21 -05:00
James 57462f98fc Update README.md 2024-11-01 15:43:52 -05:00
James c05c3d3a89 Update views/layout.erb for bulma 1.0.2 2024-11-01 15:10:17 -05:00
James 5a91e0d0dd Expanded setup instructions in Readme. 2024-11-01 14:52:56 -05:00
10 changed files with 61 additions and 344 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
/.rake_tasks~
/temp.db
/vendor/
/bin/

View file

@ -6,8 +6,6 @@ gem 'tilt'
gem 'erubi'
gem 'sequel'
gem 'rake'
gem 'rodauth'
gem 'bcrypt'
# Change to whatever database you plan to use
gem 'sqlite3'

View file

@ -14,6 +14,17 @@ Next Roda app template iterations will add database, then authentication.
## Setup
### Get git
```
sudo apt install git
git config --global user.email "myemail@gmail.com"
git config --global user.name "Full Name"
git config --global credential.helper "cache"
git clone https://path/to/project.git
```
### Prereq installs
Will need ruby; install it via package manager or a ruby manager like rbenv/ruby-build. Will need the roda gem, and then an application server such as puma (recommended), gunicorn, or passenger. My examples will use system ruby and puma.
@ -23,32 +34,64 @@ Will need ruby; install it via package manager or a ruby manager like rbenv/ruby
With this example, will basically just ignore the project's Gemfile. Debian 12 has a pretty current ruby version so just using it.
```
sudo apt install ruby ruby-rack puma ruby-erubi ruby-tilt ruby-sequel ruby-sqlite3 rake ruby-bcrypt
sudo gem install roda rack-unreloader rodauth --no-document
sudo apt install ruby ruby-rack puma ruby-erubi ruby-tilt ruby-sequel ruby-sqlite3 rake
sudo gem install roda rack-unreloader
cd my-project
```
#### Option 2: Bundler
#### Option 2a: Bundler system package
Run the bundle command from the project's root directory
Run the bundle install command from the project's root directory
```
sudo apt install ruby ruby-bundler
cd /path/to/project/
bundle config set --local path 'vendor/bundle'
sudo apt install ruby ruby-bundler ruby-dev gcc pkgconf make g++ libyaml-dev libffi-dev
echo 'gem: --no-document' >> ~/.gemrc
sudo cp ~/.gemrc /root/
bundle config set --global path 'vendor/bundle'
sudo cp -r .bundle /root/
cd my-project
bundle install
```
#### Option 2b: Bundler system gem (recommended)
Similar to above but might as well use the gem install to get the latest bundler. The Debian apt packaged bundler is currently a bit outdated and missing some features compared to the latest.
```
sudo apt install ruby ruby-dev gcc pkgconf make g++ libyaml-dev # zlib1g-dev libffi-dev #(for rails stuff)
echo 'gem: --no-document' >> ~/.gemrc
sudo cp ~/.gemrc /root/
sudo gem install bundler
bundle config set --global path 'vendor/bundle'
sudo cp -r .bundle /root/
cd my-project
bundle install
```
#### Option 3: Rbenv Ruby
Todo
## Run it
In the project root directory:
```
bundle exec puma
# or if you did not use bundler to install puma...
puma
```
This default to development mode. Run it in production mode with:
```
RACK_ENV=production bundle exec puma
# or if you did not install puma
RACK_ENV=production puma
```
@ -56,7 +99,7 @@ For development, just run it like that. For production, probably want to set up
### Run it with systemd in production
Copy the example myapp.service file to `/etc/systemd/system/` and edit accordingly. The example assumes a user named "myapp" with a group name "myapp", the application files are in `/opt/myapp/`, and puma is the system puma.
Copy the example myapp.service file to `/etc/systemd/system/` and edit accordingly. The example assumes a user named "myapp" with a group name "myapp", the application files are in `/opt/myapp/`, and puma is the system puma. If you installed puma with bundler, the exe will be at `/opt/myapp/vendor/bundle/ruby/3.1.0/bin/puma`.
### Notes

12
app.rb
View file

@ -7,19 +7,8 @@ class App < Roda
plugin :render, escape: true
plugin :route_csrf
#secret = ENV['SESSION_SECRET']
secret = 'hgfde456789ijhgt67uhgfdswertgbvfghjhgfde456789ijhgt67uhgfdswertgbvfghj'
plugin :sessions, secret: secret
plugin :rodauth do
enable :login, :logout, :create_account
require_email_address_logins? false
require_login_confirmation? false
hmac_secret secret
end
route do |r|
check_csrf!
r.rodauth
r.root do
view :index
@ -31,7 +20,6 @@ class App < Roda
end
r.on 'hello' do
rodauth.require_authentication
r.is String do |name|
@page_title = 'A Custom Greeting'
@name = name.capitalize

View file

@ -1,240 +0,0 @@
# frozen_string_literal: true
Sequel.migration do
up do
extension :date_arithmetic
# Used by the account verification and close account features
create_table(:account_statuses) do
Integer :id, primary_key: true
String :name, null: false, unique: true
end
from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])
db = self
create_table(:accounts) do
primary_key :id, type: :Bignum
foreign_key :status_id, :account_statuses, null: false, default: 1
if db.database_type == :postgres
citext :email, null: false
constraint :valid_email, email: /^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/
else
String :email, null: false
end
if db.supports_partial_indexes?
index :email, unique: true, where: {status_id: [1, 2]}
else
index :email, unique: true
end
end
deadline_opts = proc do |days|
if database_type == :mysql
{null: false}
else
{null: false, default: Sequel.date_add(Sequel::CURRENT_TIMESTAMP, days: days)}
end
end
# Used by the audit logging feature
json_type = case database_type
when :postgres
:jsonb
when :sqlite, :mysql
:json
else
String
end
create_table(:account_authentication_audit_logs) do
primary_key :id, type: :Bignum
foreign_key :account_id, :accounts, null: false, type: :Bignum
DateTime :at, null: false, default: Sequel::CURRENT_TIMESTAMP
String :message, null: false
column :metadata, json_type
index [:account_id, :at], name: :audit_account_at_idx
index :at, name: :audit_at_idx
end
# Used by the password reset feature
create_table(:account_password_reset_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
DateTime :deadline, deadline_opts[1]
DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
end
# Used by the jwt refresh feature
create_table(:account_jwt_refresh_keys) do
primary_key :id, type: :Bignum
foreign_key :account_id, :accounts, null: false, type: :Bignum
String :key, null: false
DateTime :deadline, deadline_opts[1]
index :account_id, name: :account_jwt_rk_account_id_idx
end
# Used by the account verification feature
create_table(:account_verification_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
DateTime :requested_at, null: false, default: Sequel::CURRENT_TIMESTAMP
DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
end
# Used by the verify login change feature
create_table(:account_login_change_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
String :login, null: false
DateTime :deadline, deadline_opts[1]
end
# Used by the remember me feature
create_table(:account_remember_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
DateTime :deadline, deadline_opts[14]
end
# Used by the lockout feature
create_table(:account_login_failures) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
Integer :number, null: false, default: 1
end
create_table(:account_lockouts) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
DateTime :deadline, deadline_opts[1]
DateTime :email_last_sent
end
# Used by the email auth feature
create_table(:account_email_auth_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
DateTime :deadline, deadline_opts[1]
DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
end
# Used by the password expiration feature
create_table(:account_password_change_times) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
DateTime :changed_at, null: false, default: Sequel::CURRENT_TIMESTAMP
end
# Used by the account expiration feature
create_table(:account_activity_times) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
DateTime :last_activity_at, null: false
DateTime :last_login_at, null: false
DateTime :expired_at
end
# Used by the single session feature
create_table(:account_session_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
end
# Used by the active sessions feature
create_table(:account_active_session_keys) do
foreign_key :account_id, :accounts, type: :Bignum
String :session_id
Time :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
primary_key [:account_id, :session_id]
end
# Used by the webauthn feature
create_table(:account_webauthn_user_ids) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :webauthn_id, null: false
end
create_table(:account_webauthn_keys) do
foreign_key :account_id, :accounts, type: :Bignum
String :webauthn_id
String :public_key, null: false
Integer :sign_count, null: false
Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
primary_key [:account_id, :webauthn_id]
end
# Used by the otp feature
create_table(:account_otp_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
Integer :num_failures, null: false, default: 0
Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
end
# Used by the recovery codes feature
create_table(:account_recovery_codes) do
foreign_key :id, :accounts, type: :Bignum
String :code
primary_key [:id, :code]
end
# Used by the sms codes feature
create_table(:account_sms_codes) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :phone_number, null: false
Integer :num_failures
String :code
DateTime :code_issued_at, null: false, default: Sequel::CURRENT_TIMESTAMP
end
case database_type
when :postgres
user = get(Sequel.lit('current_user')) + '_password'
run "GRANT REFERENCES ON accounts TO #{user}"
when :mysql, :mssql
user = if database_type == :mysql
get(Sequel.lit('current_user')).sub(/_password@/, '@')
else
get(Sequel.function(:DB_NAME))
end
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_statuses TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON accounts TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_authentication_audit_logs TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_password_reset_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_jwt_refresh_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_verification_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_login_change_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_remember_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_login_failures TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_email_auth_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_lockouts TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_password_change_times TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_activity_times TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_session_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_active_session_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_user_ids TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_otp_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_recovery_codes TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_sms_codes TO #{user}"
end
end
down do
drop_table(:account_sms_codes,
:account_recovery_codes,
:account_otp_keys,
:account_webauthn_keys,
:account_webauthn_user_ids,
:account_session_keys,
:account_active_session_keys,
:account_activity_times,
:account_password_change_times,
:account_email_auth_keys,
:account_lockouts,
:account_login_failures,
:account_remember_keys,
:account_login_change_keys,
:account_verification_keys,
:account_jwt_refresh_keys,
:account_password_reset_keys,
:account_authentication_audit_logs,
:accounts,
:account_statuses)
end
end

View file

@ -1,75 +0,0 @@
# frozen_string_literal: true
require 'rodauth/migrations'
Sequel.migration do
up do
create_table(:account_password_hashes) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :password_hash, null: false
end
Rodauth.create_database_authentication_functions(self)
case database_type
when :postgres
user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
run "REVOKE ALL ON account_password_hashes FROM public"
run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT(id) ON account_password_hashes TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{user}"
when :mysql
user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
db_name = get(Sequel.function(:database))
run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT (id) ON account_password_hashes TO #{user}"
when :mssql
user = get(Sequel.function(:DB_NAME))
run "GRANT EXECUTE ON rodauth_get_salt TO #{user}"
run "GRANT EXECUTE ON rodauth_valid_password_hash TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT ON account_password_hashes(id) TO #{user}"
end
# Used by the disallow_password_reuse feature
create_table(:account_previous_password_hashes) do
primary_key :id, type: :Bignum
foreign_key :account_id, :accounts, type: :Bignum
String :password_hash, null: false
end
Rodauth.create_database_previous_password_check_functions(self)
case database_type
when :postgres
user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
run "REVOKE ALL ON account_previous_password_hashes FROM public"
run "REVOKE ALL ON FUNCTION rodauth_get_previous_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_previous_password_hash_match(int8, text) FROM public"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT(id, account_id) ON account_previous_password_hashes TO #{user}"
run "GRANT USAGE ON account_previous_password_hashes_id_seq TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_get_previous_salt(int8) TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_previous_password_hash_match(int8, text) TO #{user}"
when :mysql
user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
db_name = get(Sequel.function(:database))
run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT (id, account_id) ON account_previous_password_hashes TO #{user}"
when :mssql
user = get(Sequel.function(:DB_NAME))
run "GRANT EXECUTE ON rodauth_get_previous_salt TO #{user}"
run "GRANT EXECUTE ON rodauth_previous_password_hash_match TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT ON account_previous_password_hashes(id, account_id) TO #{user}"
end
end
down do
Rodauth.drop_database_previous_password_check_functions(self)
Rodauth.drop_database_authentication_functions(self)
drop_table(:account_previous_password_hashes, :account_password_hashes)
end
end

View file

@ -1,3 +1,3 @@
<p class="subtitle is-3">
<p class="subtitle">
Hello, <%= @name %>!
</p>

View file

@ -1,3 +1,3 @@
<p class="subtitle is-3">
<p class="subtitle">
Welcome to my new page! Running in <%= ENV['RACK_ENV'] %> mode!
</p>

View file

@ -1,16 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= @page_title || "My Website" %></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title is-1"><%= @page_title || 'Page Title Placeholder' %></h1>
<% # maybe put a flash section here some day %>
<h1 class="title">
<%= @page_title || 'Page Title Placeholder' %>
</h1>
<%== yield %>
</div>
</section>

View file

@ -1,5 +1,5 @@
<% for u in @users do %>
<p class="subtitle is-3">
<p class="subtitle">
Hello, <%= u.name %>! You're #<%= u.id %>!
</p>
<% end %>