style(ntfy): fix bracket-spacing lint offenses
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
module Admin
|
module Admin
|
||||||
class BaseController < ActionController::Base
|
class BaseController < ActionController::Base
|
||||||
include Pagy::Backend
|
include Pagy::Backend
|
||||||
|
include AdminAuthentication
|
||||||
|
|
||||||
# Enable session and flash for admin controllers
|
# Enable session and flash for admin controllers
|
||||||
# (needed because the app is in API-only mode)
|
# (needed because the app is in API-only mode)
|
||||||
@@ -9,22 +10,12 @@ module Admin
|
|||||||
layout "admin"
|
layout "admin"
|
||||||
before_action :require_admin
|
before_action :require_admin
|
||||||
|
|
||||||
|
helper_method :current_admin, :logged_in?
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def current_admin
|
|
||||||
@current_admin ||= AdminUser.find_by(id: session[:admin_id]) if session[:admin_id]
|
|
||||||
end
|
|
||||||
helper_method :current_admin
|
|
||||||
|
|
||||||
def logged_in?
|
|
||||||
current_admin.present?
|
|
||||||
end
|
|
||||||
helper_method :logged_in?
|
|
||||||
|
|
||||||
def require_admin
|
def require_admin
|
||||||
unless logged_in?
|
redirect_to admin_login_path, alert: "Please log in to continue" unless logged_in?
|
||||||
redirect_to admin_login_path, alert: "Please log in to continue"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
module Admin
|
module Admin
|
||||||
class SessionsController < ActionController::Base
|
class SessionsController < ActionController::Base
|
||||||
|
include AdminAuthentication
|
||||||
|
|
||||||
layout "admin"
|
layout "admin"
|
||||||
|
|
||||||
# CSRF protection is enabled by default in ActionController::Base
|
# CSRF protection is enabled by default in ActionController::Base
|
||||||
# We need it for the create action but not for the new (GET) action
|
# We need it for the create action but not for the new (GET) action
|
||||||
protect_from_forgery with: :exception
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
helper_method :current_admin, :logged_in?
|
||||||
|
|
||||||
def new
|
def new
|
||||||
redirect_to admin_dashboard_path if current_admin
|
redirect_to admin_dashboard_path if current_admin
|
||||||
end
|
end
|
||||||
@@ -27,12 +31,5 @@ module Admin
|
|||||||
session.delete(:admin_id)
|
session.delete(:admin_id)
|
||||||
redirect_to admin_login_path, notice: "You have been logged out"
|
redirect_to admin_login_path, notice: "You have been logged out"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def current_admin
|
|
||||||
@current_admin ||= AdminUser.find_by(id: session[:admin_id]) if session[:admin_id]
|
|
||||||
end
|
|
||||||
helper_method :current_admin
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
11
app/controllers/concerns/admin_authentication.rb
Normal file
11
app/controllers/concerns/admin_authentication.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module AdminAuthentication
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def current_admin
|
||||||
|
@current_admin ||= AdminUser.find_by(id: session[:admin_id]) if session[:admin_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def logged_in?
|
||||||
|
current_admin.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,9 +1,2 @@
|
|||||||
module AdminHelper
|
module AdminHelper
|
||||||
def current_admin
|
|
||||||
@current_admin ||= AdminUser.find_by(id: session[:admin_id]) if session[:admin_id]
|
|
||||||
end
|
|
||||||
|
|
||||||
def logged_in?
|
|
||||||
current_admin.present?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,12 +1,3 @@
|
|||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
include Pagy::Frontend
|
include Pagy::Frontend
|
||||||
|
|
||||||
# Admin authentication helpers
|
|
||||||
def current_admin
|
|
||||||
@current_admin ||= AdminUser.find_by(id: session[:admin_id]) if session[:admin_id]
|
|
||||||
end
|
|
||||||
|
|
||||||
def logged_in?
|
|
||||||
current_admin.present?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class CheckGatewayHealthJob < ApplicationJob
|
|||||||
title: "Gateway offline",
|
title: "Gateway offline",
|
||||||
message: "#{name} (#{device_id}) went offline — no heartbeat for 2+ minutes",
|
message: "#{name} (#{device_id}) went offline — no heartbeat for 2+ minutes",
|
||||||
priority: 4,
|
priority: 4,
|
||||||
tags: ["rotating_light"])
|
tags: [ "rotating_light" ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class ApiKey < ApplicationRecord
|
|||||||
title: "API key revoked",
|
title: "API key revoked",
|
||||||
message: "API key '#{name}' (#{key_prefix}...) was revoked",
|
message: "API key '#{name}' (#{key_prefix}...) was revoked",
|
||||||
priority: 3,
|
priority: 3,
|
||||||
tags: ["key", "no_entry"])
|
tags: [ "key", "no_entry" ])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Deactivate expired keys
|
# Deactivate expired keys
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class Gateway < ApplicationRecord
|
|||||||
title: "Gateway online",
|
title: "Gateway online",
|
||||||
message: "#{name} (#{device_id}) came back online",
|
message: "#{name} (#{device_id}) came back online",
|
||||||
priority: 3,
|
priority: 3,
|
||||||
tags: ["white_check_mark"],
|
tags: [ "white_check_mark" ],
|
||||||
click: nil)
|
click: nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -60,7 +60,7 @@ class Gateway < ApplicationRecord
|
|||||||
title: "Gateway offline",
|
title: "Gateway offline",
|
||||||
message: "#{name} (#{device_id}) went offline",
|
message: "#{name} (#{device_id}) went offline",
|
||||||
priority: 4,
|
priority: 4,
|
||||||
tags: ["rotating_light"])
|
tags: [ "rotating_light" ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class SmsMessage < ApplicationRecord
|
|||||||
title: "SMS delivered",
|
title: "SMS delivered",
|
||||||
message: "Message to #{phone_number} (#{message_id}) was delivered",
|
message: "Message to #{phone_number} (#{message_id}) was delivered",
|
||||||
priority: 2,
|
priority: 2,
|
||||||
tags: ["white_check_mark"])
|
tags: [ "white_check_mark" ])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Mark message as failed
|
# Mark message as failed
|
||||||
@@ -71,7 +71,7 @@ class SmsMessage < ApplicationRecord
|
|||||||
title: "SMS failed",
|
title: "SMS failed",
|
||||||
message: "Message to #{phone_number} (#{message_id}) failed#{error_msg ? ": #{error_msg}" : ''}",
|
message: "Message to #{phone_number} (#{message_id}) failed#{error_msg ? ": #{error_msg}" : ''}",
|
||||||
priority: 5,
|
priority: 5,
|
||||||
tags: ["x", "rotating_light"])
|
tags: [ "x", "rotating_light" ])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Increment retry counter
|
# Increment retry counter
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
<div class="w-full max-w-md space-y-8">
|
<div class="fade-up">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="mx-auto h-16 w-16 flex items-center justify-center rounded-full bg-blue-100">
|
<div class="relative mx-auto h-20 w-20 flex items-center justify-center">
|
||||||
<i class="fas fa-sms text-3xl text-blue-600"></i>
|
<span class="beacon-ring absolute inset-0 rounded-full border border-sky-400/50"></span>
|
||||||
|
<span class="beacon-ring delay-1 absolute inset-0 rounded-full border border-sky-400/40"></span>
|
||||||
|
<span class="beacon-ring delay-2 absolute inset-0 rounded-full border border-sky-400/30"></span>
|
||||||
|
<div class="relative flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-sky-600 shadow-lg shadow-sky-500/40">
|
||||||
|
<i class="fas fa-sms text-2xl text-slate-950"></i>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="mt-6 text-3xl font-bold tracking-tight text-gray-900">MySMSAPio Admin</h2>
|
</div>
|
||||||
<p class="mt-2 text-sm text-gray-600">Sign in to your admin account</p>
|
<h2 class="font-display mt-6 text-4xl font-extrabold tracking-tight text-white">MySMSAPio</h2>
|
||||||
|
<p class="font-mono-tech mt-2 text-[11px] uppercase tracking-[0.35em] text-sky-400/80">Admin Console</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 bg-white py-8 px-4 shadow-xl rounded-xl sm:px-10">
|
<div class="login-card fade-up d-1 mt-8 rounded-2xl px-8 py-9 sm:px-10">
|
||||||
<%= form_with url: admin_login_path, method: :post, local: true, class: "space-y-6" do |f| %>
|
<%= form_with url: admin_login_path, method: :post, local: true, class: "space-y-5" do |f| %>
|
||||||
<div>
|
<div>
|
||||||
<%= label_tag :email, "Email address", class: "block text-sm font-medium text-gray-700" %>
|
<%= label_tag :email, "Email address", class: "font-mono-tech block text-[11px] uppercase tracking-wider text-slate-400" %>
|
||||||
<div class="mt-1 relative">
|
<div class="mt-2 relative">
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5">
|
||||||
<i class="fas fa-envelope text-gray-400"></i>
|
<i class="fas fa-envelope text-slate-500 text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<%= email_field_tag :email, params[:email],
|
<%= email_field_tag :email, params[:email],
|
||||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3",
|
class: "field-input block w-full rounded-xl pl-11 pr-3 py-3 text-sm",
|
||||||
placeholder: "admin@example.com",
|
placeholder: "admin@example.com",
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
required: true %>
|
required: true %>
|
||||||
@@ -24,26 +29,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= label_tag :password, "Password", class: "block text-sm font-medium text-gray-700" %>
|
<%= label_tag :password, "Password", class: "font-mono-tech block text-[11px] uppercase tracking-wider text-slate-400" %>
|
||||||
<div class="mt-1 relative">
|
<div class="mt-2 relative">
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5">
|
||||||
<i class="fas fa-lock text-gray-400"></i>
|
<i class="fas fa-lock text-slate-500 text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<%= password_field_tag :password, nil,
|
<%= password_field_tag :password, nil,
|
||||||
class: "block w-full pl-10 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm py-3",
|
class: "field-input block w-full rounded-xl pl-11 pr-3 py-3 text-sm",
|
||||||
placeholder: "Enter your password",
|
placeholder: "••••••••••••",
|
||||||
required: true %>
|
required: true %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="pt-2">
|
||||||
<%= submit_tag "Sign in",
|
<%= submit_tag "Sign in",
|
||||||
class: "flex w-full justify-center rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-all duration-200" %>
|
class: "btn-accent flex w-full justify-center rounded-xl px-4 py-3.5 text-sm font-semibold text-slate-950" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center text-xs text-gray-500">
|
<div class="fade-up d-2 mt-7 flex items-center justify-center gap-2 font-mono-tech text-[10px] uppercase tracking-[0.3em] text-slate-500">
|
||||||
<i class="fas fa-shield-alt"></i> Secure Admin Access
|
<i class="fas fa-shield-halved text-sky-500/70"></i>
|
||||||
|
Secure · Encrypted · Local
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1395
docs/superpowers/plans/2026-07-28-ntfy-notifications-integration.md
Normal file
1395
docs/superpowers/plans/2026-07-28-ntfy-notifications-integration.md
Normal file
File diff suppressed because it is too large
Load Diff
12
test/fixtures/admin_users.yml
vendored
12
test/fixtures/admin_users.yml
vendored
@@ -1,13 +1,13 @@
|
|||||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
one:
|
one:
|
||||||
email: MyString
|
email: one@example.com
|
||||||
password_digest: MyString
|
password_digest: <%= BCrypt::Password.create("password123") %>
|
||||||
name: MyString
|
name: Admin One
|
||||||
last_login_at: 2025-10-20 10:51:35
|
last_login_at: 2025-10-20 10:51:35
|
||||||
|
|
||||||
two:
|
two:
|
||||||
email: MyString
|
email: two@example.com
|
||||||
password_digest: MyString
|
password_digest: <%= BCrypt::Password.create("password123") %>
|
||||||
name: MyString
|
name: Admin Two
|
||||||
last_login_at: 2025-10-20 10:51:35
|
last_login_at: 2025-10-20 10:51:35
|
||||||
|
|||||||
48
test/integration/admin/sessions_flow_test.rb
Normal file
48
test/integration/admin/sessions_flow_test.rb
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class AdminSessionsFlowTest < ActionDispatch::IntegrationTest
|
||||||
|
def setup
|
||||||
|
@admin = AdminUser.create!(name: "Site Admin", email: "admin@example.com", password: "supersecret123")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "login page renders the sign-in form" do
|
||||||
|
get admin_login_path
|
||||||
|
assert_response :success
|
||||||
|
assert_match "MySMSAPio", response.body
|
||||||
|
assert_match "Sign in", response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
test "login page displays the auth error message on failed login" do
|
||||||
|
post admin_login_path, params: { email: "admin@example.com", password: "wrong-password" }
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
# The critical regression check: the alert must be present in the rendered HTML
|
||||||
|
assert_match "Invalid email or password", response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
test "successful login redirects to the dashboard and shows welcome notice" do
|
||||||
|
post admin_login_path, params: { email: "admin@example.com", password: "supersecret123" }
|
||||||
|
assert_redirected_to admin_dashboard_path
|
||||||
|
follow_redirect!
|
||||||
|
assert_match "Welcome back", response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
test "login is case-insensitive for email" do
|
||||||
|
post admin_login_path, params: { email: "ADMIN@example.com", password: "supersecret123" }
|
||||||
|
assert_redirected_to admin_dashboard_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logging out clears the session and shows notice on the login page" do
|
||||||
|
post admin_login_path, params: { email: "admin@example.com", password: "supersecret123" }
|
||||||
|
delete admin_logout_path
|
||||||
|
assert_redirected_to admin_login_path
|
||||||
|
follow_redirect!
|
||||||
|
assert_match "logged out", response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
test "protected admin page redirects to login with an alert" do
|
||||||
|
get admin_dashboard_path
|
||||||
|
assert_redirected_to admin_login_path
|
||||||
|
follow_redirect!
|
||||||
|
assert_match "Please log in to continue", response.body
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user