Big first commit
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

authentication-zero and first layout
This commit is contained in:
2026-05-21 02:54:39 +02:00
parent 6e7fe9797a
commit 6f192274ab
49 changed files with 1933 additions and 17 deletions

View File

@@ -20,7 +20,7 @@ gem "tailwindcss-rails"
gem "jbuilder" gem "jbuilder"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7" gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ] gem "tzinfo-data", platforms: %i[ windows jruby ]
@@ -66,3 +66,7 @@ group :test do
gem "capybara" gem "capybara"
gem "selenium-webdriver" gem "selenium-webdriver"
end end
gem "authentication-zero", "~> 4.0"
gem "letter_opener", "~> 1.10", group: :development

View File

@@ -78,7 +78,9 @@ GEM
addressable (2.9.0) addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0) public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3) ast (2.4.3)
authentication-zero (4.0.3)
base64 (0.3.0) base64 (0.3.0)
bcrypt (3.1.22)
bcrypt_pbkdf (1.1.2) bcrypt_pbkdf (1.1.2)
bigdecimal (4.1.2) bigdecimal (4.1.2)
bindex (0.8.1) bindex (0.8.1)
@@ -99,6 +101,8 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.1.0)
logger (~> 1.5)
concurrent-ruby (1.3.6) concurrent-ruby (1.3.6)
connection_pool (3.0.2) connection_pool (3.0.2)
crass (1.0.6) crass (1.0.6)
@@ -155,6 +159,12 @@ GEM
thor (~> 1.3) thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0) zeitwerk (>= 2.6.18, < 3.0)
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lint_roller (1.1.0) lint_roller (1.1.0)
logger (1.7.0) logger (1.7.0)
loofah (2.25.1) loofah (2.25.1)
@@ -391,6 +401,8 @@ PLATFORMS
x86_64-linux-musl x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
authentication-zero (~> 4.0)
bcrypt (~> 3.1.7)
bootsnap bootsnap
brakeman brakeman
bundler-audit bundler-audit
@@ -400,6 +412,7 @@ DEPENDENCIES
importmap-rails importmap-rails
jbuilder jbuilder
kamal kamal
letter_opener (~> 1.10)
propshaft propshaft
puma (>= 5.0) puma (>= 5.0)
rails (~> 8.1.3) rails (~> 8.1.3)
@@ -431,7 +444,9 @@ CHECKSUMS
activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
authentication-zero (4.0.3) sha256=f005531be39355e3c85250759f28a62109a2b0f551cb3fda4ff046b590b2db4a
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032
bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
@@ -441,6 +456,7 @@ CHECKSUMS
bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
@@ -469,6 +485,8 @@ CHECKSUMS
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59 json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75 kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e
letter_opener (1.10.0) sha256=2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://w3.org" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" />
</svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -8,3 +8,29 @@
* *
* Consider organizing styles into separate files for maintainability. * Consider organizing styles into separate files for maintainability.
*/ */
@media (min-width: 768px) {
/* 1. Die Sidebar verkleinert sich flüssig auf 64px */
#desktop-sidebar-toggle:checked ~ aside {
width: 4rem !important; /* md:w-16 */
}
/* 2. Fehlerbehebung: Der rechte Inhaltsbereich wird beim Einklappen unsichtbar */
#desktop-sidebar-toggle:checked ~ aside .footer-collapse-content {
opacity: 0 !important;
visibility: hidden !important;
transition: opacity 0.15s ease-in-out, visibility 0.15s ease-in-out;
}
/* 3. Die Standard-Navigations-Texte oben schrumpfen ebenfalls */
#desktop-sidebar-toggle:checked ~ aside .collapse-text {
max-width: 0 !important;
opacity: 0 !important;
margin-left: 0 !important;
}
/* 4. Der Hauptinhalt rückt nach links nach */
#desktop-sidebar-toggle:checked ~ .main-content {
padding-left: 4rem !important;
}
}

View File

@@ -2,6 +2,20 @@ class ApplicationController < ActionController::Base
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern allow_browser versions: :modern
# Changes to the importmap will invalidate the etag for HTML responses before_action :set_current_request_details
stale_when_importmap_changes before_action :authenticate
private
def authenticate
if session_record = Session.find_by_id(cookies.signed[:session_token])
Current.session = session_record
else
redirect_to sign_in_path
end
end
def set_current_request_details
Current.user_agent = request.user_agent
Current.ip_address = request.ip
end
end end

View File

@@ -0,0 +1,5 @@
class Authentications::EventsController < ApplicationController
def index
@events = Current.user.events.order(created_at: :desc).limit(30)
end
end

View File

@@ -0,0 +1,4 @@
class HomeController < ApplicationController
def index
end
end

View File

@@ -0,0 +1,26 @@
class Identity::EmailVerificationsController < ApplicationController
skip_before_action :authenticate, only: :show
before_action :set_user, only: :show
def show
@user.update! verified: true
redirect_to root_path, notice: "Thank you for verifying your email address"
end
def create
send_email_verification
redirect_to root_path, notice: "We sent a verification email to your email address"
end
private
def set_user
@user = User.find_by_token_for!(:email_verification, params[:sid])
rescue StandardError
redirect_to edit_identity_email_path, alert: "That email verification link is invalid"
end
def send_email_verification
UserMailer.with(user: Current.user).email_verification.deliver_later
end
end

View File

@@ -0,0 +1,36 @@
class Identity::EmailsController < ApplicationController
before_action :set_user
def edit
end
def update
if @user.update(user_params)
redirect_to_root
else
render :edit, status: :unprocessable_entity
end
end
private
def set_user
@user = Current.user
end
def user_params
params.permit(:email, :password_challenge).with_defaults(password_challenge: "")
end
def redirect_to_root
if @user.email_previously_changed?
resend_email_verification
redirect_to root_path, notice: "Your email has been changed"
else
redirect_to root_path
end
end
def resend_email_verification
UserMailer.with(user: @user).email_verification.deliver_later
end
end

View File

@@ -0,0 +1,45 @@
class Identity::PasswordResetsController < ApplicationController
skip_before_action :authenticate
before_action :set_user, only: %i[ edit update ]
layout "auth"
def new
end
def edit
end
def create
if @user = User.find_by(email: params[:email], verified: true)
send_password_reset_email
redirect_to sign_in_path, notice: "Check your email for reset instructions"
else
redirect_to new_identity_password_reset_path, alert: "You can't reset your password until you verify your email"
end
end
def update
if @user.update(user_params)
redirect_to sign_in_path, notice: "Your password was reset successfully. Please sign in"
else
render :edit, status: :unprocessable_entity
end
end
private
def set_user
@user = User.find_by_token_for!(:password_reset, params[:sid])
rescue StandardError
redirect_to new_identity_password_reset_path, alert: "That password reset link is invalid"
end
def user_params
params.permit(:password, :password_confirmation)
end
def send_password_reset_email
UserMailer.with(user: @user).password_reset.deliver_later
end
end

View File

@@ -0,0 +1,25 @@
class InvitationsController < ApplicationController
def new
@user = User.new
end
def create
@user = User.create_with(user_params).find_or_initialize_by(email: params[:email])
if @user.save
send_invitation_instructions
redirect_to new_invitation_path, notice: "An invitation email has been sent to #{@user.email}"
else
render :new, status: :unprocessable_entity
end
end
private
def user_params
params.permit(:email).merge(password: SecureRandom.base58, verified: true)
end
def send_invitation_instructions
UserMailer.with(user: @user).invitation_instructions.deliver_later
end
end

View File

@@ -0,0 +1,23 @@
class PasswordsController < ApplicationController
before_action :set_user
def edit
end
def update
if @user.update(user_params)
redirect_to root_path, notice: "Your password has been changed"
else
render :edit, status: :unprocessable_entity
end
end
private
def set_user
@user = Current.user
end
def user_params
params.permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "")
end
end

View File

@@ -0,0 +1,32 @@
class RegistrationsController < ApplicationController
skip_before_action :authenticate
layout "auth"
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
session_record = @user.sessions.create!
cookies.signed.permanent[:session_token] = { value: session_record.id, httponly: true }
send_email_verification
redirect_to root_path, notice: "Welcome! You have signed up successfully"
else
render :new, status: :unprocessable_entity
end
end
private
def user_params
params.permit(:email, :password, :password_confirmation)
end
def send_email_verification
UserMailer.with(user: @user).email_verification.deliver_later
end
end

View File

@@ -0,0 +1,34 @@
class SessionsController < ApplicationController
skip_before_action :authenticate, only: %i[ new create ]
before_action :set_session, only: :destroy
layout "auth", only: %i[ new create ]
def index
@sessions = Current.user.sessions.order(created_at: :desc)
end
def new
end
def create
if user = User.authenticate_by(email: params[:email], password: params[:password])
@session = user.sessions.create!
cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true }
redirect_to root_path, notice: "Signed in successfully"
else
redirect_to sign_in_path(email_hint: params[:email]), alert: "That email or password is incorrect"
end
end
def destroy
@session.destroy; redirect_to(sessions_path, notice: "That session has been logged out")
end
private
def set_session
@session = Current.user.sessions.find(params[:id])
end
end

View File

@@ -1,2 +1,36 @@
module ApplicationHelper module ApplicationHelper
def nav_link_class(target_controller)
base_classes = "flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg transition-colors duration-200"
if controller_name == target_controller
"#{base_classes} bg-blue-50 text-blue-600 font-semibold"
else
"#{base_classes} text-gray-700 hover:bg-gray-50"
end
end
def parse_user_agent(user_agent_string)
ua = user_agent_string.to_s.downcase
# Browser-Erkennung via Case-Regex (nutzt === im Hintergrund)
browser = case ua
when /firefox/ then "Firefox"
when /chrome/ then "Chrome"
when /safari/ then "Safari"
when /edge/ then "Edge"
else "Unbekannter Browser"
end
# Betriebssystem-Erkennung via Case-Regex
os = case ua
when /windows/ then "Windows"
when /macintosh|mac os/ then "macOS"
when /iphone/ then "iPhone"
when /android/ then "Android"
when /linux/ then "Linux"
else "Betriebssystem"
end
"#{browser} auf #{os}"
end
end end

View File

@@ -0,0 +1,22 @@
class UserMailer < ApplicationMailer
def password_reset
@user = params[:user]
@signed_id = @user.generate_token_for(:password_reset)
mail to: @user.email, subject: "Reset your password"
end
def email_verification
@user = params[:user]
@signed_id = @user.generate_token_for(:email_verification)
mail to: @user.email, subject: "Verify your email"
end
def invitation_instructions
@user = params[:user]
@signed_id = @user.generate_token_for(:password_reset)
mail to: @user.email, subject: "Invitation instructions"
end
end

6
app/models/current.rb Normal file
View File

@@ -0,0 +1,6 @@
class Current < ActiveSupport::CurrentAttributes
attribute :session
attribute :user_agent, :ip_address
delegate :user, to: :session, allow_nil: true
end

8
app/models/event.rb Normal file
View File

@@ -0,0 +1,8 @@
class Event < ApplicationRecord
belongs_to :user
before_create do
self.user_agent = Current.user_agent
self.ip_address = Current.ip_address
end
end

11
app/models/session.rb Normal file
View File

@@ -0,0 +1,11 @@
class Session < ApplicationRecord
belongs_to :user
before_create do
self.user_agent = Current.user_agent
self.ip_address = Current.ip_address
end
after_create { user.events.create! action: "signed_in" }
after_destroy { user.events.create! action: "signed_out" }
end

45
app/models/user.rb Normal file
View File

@@ -0,0 +1,45 @@
class User < ApplicationRecord
@@min_length_password = 12
has_secure_password
generates_token_for :email_verification, expires_in: 2.days do
email
end
generates_token_for :password_reset, expires_in: 20.minutes do
password_salt.last(10)
end
has_many :sessions, dependent: :destroy
has_many :events, dependent: :destroy
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, allow_nil: true, length: { minimum: @@min_length_password }
normalizes :email, with: -> { _1.strip.downcase }
before_validation if: :email_changed?, on: :update do
self.verified = false
end
after_update if: :password_digest_previously_changed? do
sessions.where.not(id: Current.session).delete_all
end
after_update if: :email_previously_changed? do
events.create! action: "email_verification_requested"
end
after_update if: :password_digest_previously_changed? do
events.create! action: "password_changed"
end
after_update if: [ :verified_previously_changed?, :verified? ] do
events.create! action: "email_verified"
end
def self.min_length_password
@@min_length_password
end
end

View File

@@ -0,0 +1,180 @@
<!-- <h1>Activity Log</h1>
<div id="sessions">
<% @events.each do |event| %>
<div id="<%= dom_id event %>">
<p>
<strong>User Agent:</strong>
<%= event.user_agent %>
</p>
<p>
<strong>Action:</strong>
<%= event.action %>
</p>
<p>
<strong>Ip Address:</strong>
<%= event.ip_address %>
</p>
<p>
<strong>Created at:</strong>
<%= event.created_at %>
</p>
</div>
<% end %>
</div>
<br>
<div>
<%= link_to "Back", root_path %>
</div>
-->
<% content_for :title, "Konto-Aktivitäten & Logs" %>
<div class="max-w-2xl mx-auto space-y-6">
<!-- DYNAMISCHE TAB-NAVIGATION -->
<div class="border-b border-gray-200 mb-6">
<nav class="flex space-x-6" aria-label="Tabs">
<!-- Tab 1: Sicherheit (Inaktiv) -->
<%= link_to edit_password_path, class: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 border-b-2 py-4 px-1 text-sm font-medium flex items-center gap-2 transition" do %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>
Sicherheit
<% end %>
<!-- Tab 2: Sitzungen & Geräte (Inaktiv) -->
<%= link_to sessions_path, class: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 border-b-2 py-4 px-1 text-sm font-medium flex items-center gap-2 transition" do %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" /></svg>
Sitzungen & Geräte
<% end %>
<!-- Tab 3: Aktivitäten (Aktiv) -->
<span class="border-blue-500 text-blue-600 border-b-2 py-4 px-1 text-sm font-semibold flex items-center gap-2" aria-current="page">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" /></svg>
Aktivitäten
</span>
</nav>
</div>
<!-- Info-Header -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6 flex flex-col sm:flex-row items-start sm:items-center gap-4 justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-blue-50 text-blue-600 rounded-lg shrink-0">
<!-- Heroicon: clock -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
</svg>
</div>
<div>
<h2 class="text-lg font-bold text-gray-800">Sicherheits-Historie</h2>
<p class="text-sm text-gray-500">Chronologische Übersicht der letzten 30 kritischen und erfolgreichen Ereignisse deines Kontos.</p>
</div>
</div>
</div>
<!-- TIMELINE LOGBUCH -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8">
<div class="flow-root">
<% if @events.any? %>
<ul class="-mb-8">
<% @events.each_with_index do |event, index| %>
<% is_last = (index == @events.size - 1) %>
<li>
<div class="relative pb-8">
<!-- Vertikale Verbindungslinie -->
<% unless is_last %>
<span class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true"></span>
<% end %>
<div class="relative flex space-x-3">
<div>
<!-- Dynamische Farb- und Icon-Weiche je nach Event-Typ -->
<% case event.action %>
<% when "signed_in" %>
<span class="h-8 w-8 rounded-full bg-blue-50 flex items-center justify-center ring-8 ring-white">
<svg class="h-4 w-4 text-blue-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
</svg>
</span>
<% when "password_changed" %>
<span class="h-8 w-8 rounded-full bg-amber-50 flex items-center justify-center ring-8 ring-white">
<svg class="h-4 w-4 text-amber-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
</svg>
</span>
<% when "two_factor_activated" %>
<span class="h-8 w-8 rounded-full bg-green-50 flex items-center justify-center ring-8 ring-white">
<svg class="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296A3.745 3.745 0 0 1 21 12z" />
</svg>
</span>
<% else %>
<!-- Roter Kreis für fehlgeschlagene Anmeldungen oder Ausloggen -->
<span class="h-8 w-8 rounded-full bg-red-50 flex items-center justify-center ring-8 ring-white">
<svg class="h-4 w-4 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25z" />
</svg>
</span>
<% end %>
</div>
<!-- Textinhalte des Events -->
<div class="flex-1 min-w-0 pt-1.5 flex justify-between space-x-4">
<div class="min-w-0">
<p class="text-sm font-medium text-gray-800">
<% case event.action %>
<% when "login_success" %>
Erfolgreiche Anmeldung im System
<% when "password_changed" %>
Passwort erfolgreich geändert
<% when "two_factor_activated" %>
Zwei-Faktor-Authentifizierung aktiviert
<% when "login_failed" %>
<span class="text-red-600 font-semibold">Fehlgeschlagener Anmeldeversuch</span>
<% else %>
<%= event.action.humanize %>
<% end %>
</p>
<!-- Metadaten: IP und gekürzter User-Agent -->
<p class="text-xs text-gray-400 font-mono mt-0.5 truncate">
IP: <%= event.ip_address.presence || "Unbekannt" %>
<% if event.respond_to?(:user_agent) && event.user_agent.present? %>
• <%= parse_user_agent(event.user_agent) %>
<% end %>
</p>
</div>
<!-- Zeitstempel über Schätzwert "vor X Tagen" -->
<div class="text-right text-xs whitespace-nowrap text-gray-400 pt-0.5 shrink-0">
<time class="font-medium" datetime="<%= event.created_at.iso8601 %>">
<%= time_ago_in_words(event.created_at) %> vor
</time>
</div>
</div>
</div>
</div>
</li>
<% end %>
</ul>
<% else %>
<!-- Empty State falls noch gar keine Logs da sind -->
<div class="text-center py-8 text-gray-400">
<p class="text-sm">Bisher wurden keine Aktivitäten aufgezeichnet.</p>
</div>
<% end %>
</div>
</div>
</div>

View File

@@ -0,0 +1,31 @@
<p style="color: green"><%= notice %></p>
<p>Signed as <%= Current.user.email %></p>
<h2>Login and verification</h2>
<div>
<%= link_to "Change password", edit_password_path %>
</div>
<div>
<%= link_to "Change email address", edit_identity_email_path %>
</div>
<div>
<%= link_to "Send invitation", new_invitation_path %>
</div>
<h2>Access history</h2>
<div>
<%= link_to "Devices & Sessions", sessions_path %>
</div>
<div>
<%= link_to "Activity Log", authentications_events_path %>
</div>
<br>
<%= button_to "Log out", Current.session, method: :delete %>

View File

@@ -0,0 +1,43 @@
<p style="color: red"><%= alert %></p>
<% if Current.user.verified? %>
<h1>Change your email</h1>
<% else %>
<h1>Verify your email</h1>
<p>We sent a verification email to the address below. Check that email and follow those instructions to confirm it's your email address.</p>
<p><%= button_to "Re-send verification email", identity_email_verification_path %></p>
<% end %>
<%= form_with(url: identity_email_path, method: :patch) do |form| %>
<% if @user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% @user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :email, "New email", style: "display: block" %>
<%= form.email_field :email, required: true, autofocus: true %>
</div>
<div>
<%= form.label :password_challenge, style: "display: block" %>
<%= form.password_field :password_challenge, required: true, autocomplete: "current-password" %>
</div>
<div>
<%= form.submit "Save changes" %>
</div>
<% end %>
<br>
<div>
<%= link_to "Back", root_path %>
</div>

View File

@@ -0,0 +1,128 @@
<!-- <h1>Reset your password</h1>
<%= form_with(url: identity_password_reset_path, method: :patch) do |form| %>
<% if @user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% @user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form.hidden_field :sid, value: params[:sid] %>
<div>
<%= form.label :password, "New password", style: "display: block" %>
<%= form.password_field :password, required: true, autofocus: true, autocomplete: "new-password" %>
<div>12 characters minimum.</div>
</div>
<div>
<%= form.label :password_confirmation, "Confirm new password", style: "display: block" %>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password" %>
</div>
<div>
<%= form.submit "Save changes" %>
</div>
<% end %>
-->
<div class="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto w-full max-w-md">
<!-- BRANDING: Groß, zentriert und modern -->
<div class="flex flex-col items-center justify-center gap-3 mb-8">
<!-- Größeres Heroicon: cube (h-16 w-16 statt h-8) -->
<svg class="h-16 w-16 text-blue-600 drop-shadow-sm" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" />
</svg>
<!-- Größerer, fetterer Text (text-4xl font-black) -->
<span class="text-4xl font-black text-gray-900 tracking-tight">Vault171</span>
</div>
<!-- Dezente Trennlinie mit Untertitel -->
<div class="relative mb-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 text-sm">
<span class="bg-gray-50 px-3 text-xs font-semibold text-gray-400 uppercase tracking-widest">Sicherheitsbereich</span>
</div>
</div>
<h2 class="text-center text-xl font-bold text-gray-800">Neues Passwort vergeben</h2>
<p class="mt-1.5 text-center text-sm text-gray-500">
Wählen Sie ein neues, sicheres Passwort für Ihr Benutzerkonto.
</p>
</div>
<div class="mt-8 sm:mx-auto w-full max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-xl sm:px-10 border border-gray-200">
<!-- Der Formular-Builder nutzt das von authentication-zero bereitgestellte Token -->
<%= form_with(url: identity_password_reset_path, method: :patch, class: "bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8 space-y-6") do |form| %>
<!-- Validierungsfehler anzeigen -->
<% if @user.errors.any? %>
<div class="p-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200" role="alert">
<div class="flex items-center gap-2 font-bold mb-2">
<svg class="h-5 w-5 text-red-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
<span>Fehler beim Speichern:</span>
</div>
<ul class="list-disc list-inside space-y-0.5 text-xs">
<% @user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<!-- passwort reset token -->
<%= form.hidden_field :sid, value: params[:sid] %>
<!-- Feld 1: Neues Passwort -->
<div>
<%= form.label :password, "Neues Passwort", class: "block text-sm font-medium text-gray-700 mb-1.5" %>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25z" />
</svg>
</div>
<%= form.password_field :password, required: true, autofocus: true, autocomplete: "new-password", class: "block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50" %>
</div>
<p class="mt-1.5 text-xs text-gray-400">Mindestens <%= User.min_length_password %> Zeichen lang.</p>
</div>
<!-- Feld 2: Passwort bestätigen -->
<div>
<%= form.label :password_confirmation, "Passwort bestätigen", class: "block text-sm font-medium text-gray-700 mb-1.5" %>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25z" />
</svg>
</div>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", class: "block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50" %>
</div>
</div>
<!-- Absendeknopf -->
<div>
<%= form.submit "Passwort aktualisieren", class: "w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors cursor-pointer" %>
</div>
<% end %>
</div>
</div>
</div>

View File

@@ -0,0 +1,72 @@
<!-- <p style="color: red"><%= alert %></p>
<h1>Forgot your password?</h1>
<%= form_with(url: identity_password_reset_path) do |form| %>
<div>
<%= form.label :email, style: "display: block" %>
<%= form.email_field :email, required: true, autofocus: true %>
</div>
<div>
<%= form.submit "Send password reset email" %>
</div>
<% end %>
-->
<div class="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto w-full max-w-md">
<!-- Heroicon: key -->
<div class="flex justify-center text-blue-600">
<svg class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
</svg>
</div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">Passwort zurücksetzen</h2>
<p class="mt-2 text-center text-sm text-gray-600">
Geben Sie Ihre E-Mail-Adresse ein. Wir senden Ihnen einen Link zum Zurücksetzen.
</p>
</div>
<div class="mt-8 sm:mx-auto w-full max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-xl sm:px-10 border border-gray-200">
<%= form_with(url: identity_password_reset_path, class: "space-y-6") do |form| %>
<!-- Fehleranzeige bei Fehlern der Passwort-Validierung -->
<% if alert %>
<div class="p-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200" role="alert">
<ul class="list-disc list-inside space-y-0.5 text-xs">
<%= alert %>
</ul>
</div>
<% end %>
<div>
<%= form.label :email, "E-Mail-Adresse", class: "block text-sm font-medium text-gray-700 mb-1.5" %>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<!-- Heroicon: envelope -->
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0l-7.5-4.615a2.25 2.25 0 0 1-1.07-1.916V6.75" />
</svg>
</div>
<%= form.email_field :email, required: true, autofocus: true, autocomplete: "email", class: "block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50" %>
</div>
</div>
<div class="flex items-center justify-between text-sm">
<%= link_to "← Zurück zum Login", sign_in_path, class: "font-medium text-gray-600 hover:text-gray-500 transition-colors" %>
</div>
<div>
<%= form.submit "Link anfordern", class: "w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors cursor-pointer" %>
</div>
<% end %>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<p style="color: green"><%= notice %></p>
<h1>Send invitation</h1>
<%= form_with(url: invitation_path) do |form| %>
<% if @user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% @user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :email, style: "display: block" %>
<%= form.email_field :email, required: true, autofocus: true %>
</div>
<div>
<%= form.submit "Send an invitation" %>
</div>
<% end %>
<br>
<div>
<%= link_to "Back", root_path %>
</div>

View File

@@ -9,8 +9,6 @@
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
<%= yield :head %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
@@ -21,11 +19,130 @@
<%# Includes all stylesheet files in app/assets/stylesheets %> <%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %> <%= javascript_importmap_tags %>
<!-- Globaler Platzhalter für dynamische Scripte (wie den QR-Kamera-Scanner) -->
<%= yield :head %>
</head> </head>
<body> <body class="bg-gray-100 text-gray-800 antialiased">
<main class="container mx-auto mt-28 px-5 flex">
<%= yield %> <!-- APPLIKATIONS-LAYOUT FÜR EINGELOGGTE NUTZER -->
</main> <div class="min-h-screen bg-gray-100 flex relative overflow-x-hidden">
<!-- STEUERUNG 1: Mobile Sidebar (Standard-Burger via CSS) -->
<input type="checkbox" id="mobile-sidebar-toggle" class="peer hidden" />
<!-- STEUERUNG 2: Desktop Sidebar (Einklappen via CSS) -->
<input type="checkbox" id="desktop-sidebar-toggle" class="hidden" />
<!-- DUNKLES HINTERGRUND-OVERLAY (Ausblend-Animation nur auf Mobile aktiv) -->
<label for="mobile-sidebar-toggle"
class="fixed inset-0 bg-gray-900/50 z-40 md:hidden backdrop-blur-sm
opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out
peer-checked:opacity-100 peer-checked:pointer-events-auto"></label>
<!-- SIDEBAR NAVIGATION -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col fixed h-full z-50
transition-all duration-300 ease-in-out transform -translate-x-full
md:translate-x-0 peer-checked:translate-x-0">
<!-- Logo-Bereich (Desktop-Klickfläche zum Einklappen) -->
<div class="h-16 flex items-center justify-between px-4 border-b border-gray-200 min-w-0">
<label for="desktop-sidebar-toggle" class="flex items-center gap-3 select-none group w-full overflow-hidden pointer-events-none md:cursor-pointer md:pointer-events-auto">
<svg class="h-6 w-6 text-blue-600 shrink-0 transition-transform md:group-hover:scale-110 ml-1" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" /></svg>
<span class="collapse-text text-xl font-bold text-gray-800 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Vault171</span>
</label>
<!-- Schließen X (Nur auf Smartphones aktiv) -->
<label for="mobile-sidebar-toggle" class="md:hidden p-1 text-gray-400 hover:text-gray-600 rounded-lg cursor-pointer">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
</label>
</div>
<!-- Navigations-Links mit dynamischen Helper-Klassen -->
<nav class="flex-1 p-3 space-y-1 overflow-hidden">
<%= link_to root_path, class: nav_link_class("dashboard") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h2.25A1.125 1.125 0 0 1 7.5 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" /></svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Dashboard</span>
<% end %>
<%= link_to "", class: nav_link_class("items") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.007 5.25H3.75v.008h.008V12Zm0 5.25H3.75v.008h.008v-.008Z" /></svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Bestandsliste</span>
<% end %>
<%= link_to "", class: nav_link_class("inbound_shipments") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Wareneingang</span>
<% end %>
<%= link_to "", class: nav_link_class("categories") do %>
<svg class="h-5 w-5 shrink-0 ml-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621 Clyde5.04 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>
<span class="collapse-text ml-3 transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap">Kategorien</span>
<% end %>
</nav>
<!-- DER NEUE, AUFGERÄUMTE SIDEBAR-FOOTER -->
<div class="border-t border-gray-200 bg-gray-50 flex flex-col p-3 space-y-1 shrink-0">
<!-- Link 1: Mein Profil (Aufgebaut exakt wie ein normaler Nav-Link) -->
<%= link_to edit_password_path, class: "flex items-center gap-3 px-3 py-2 rounded-lg text-gray-700 hover:bg-gray-200/50 transition group relative" do %>
<!-- Das Icon (Deine Initialen im blauen Kreis) -->
<div class="w-5 h-5 rounded-full bg-blue-600 text-white font-bold text-[9px] flex items-center justify-center shrink-0">
<%= Current.user.email.first(2).upcase %>
</div>
<!-- Der Text blendet weich aus wie bei den anderen Links -->
<span class="collapse-text transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap text-xs font-semibold">
Mein Profil
</span>
<% end %>
<!-- Link 2: Ausloggen (Eigene Zeile darunter) -->
<%= link_to Current.session, data: { turbo_method: :delete }, class: "flex items-center gap-3 px-3 py-2 rounded-lg text-gray-500 hover:text-red-600 hover:bg-red-50 transition group relative" do %>
<!-- Heroicon: log-out -->
<svg class="h-5 w-5 shrink-0 text-gray-400 group-hover:text-red-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>
<span class="collapse-text transition-all duration-300 ease-in-out max-w-[180px] opacity-100 overflow-hidden whitespace-nowrap text-xs font-medium">
Ausloggen
</span>
<% end %>
</div>
</aside>
<!-- HAUPTBEREICH (Inhaltsfläche rückt bei verkleinerter Sidebar nach) -->
<div class="main-content flex-1 flex flex-col min-h-screen w-full transition-all duration-300 ease-in-out pl-0 md:pl-64">
<!-- OBERE LEISTE (Top Bar) -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 md:px-6 sticky top-0 z-30">
<div class="flex items-center gap-3">
<!-- BURGER BUTTON (Nur mobil sichtbar) -->
<label for="mobile-sidebar-toggle" class="md:hidden p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition cursor-pointer">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>
</label>
<h1 class="text-base md:text-lg font-bold text-gray-800"><%= yield :title %></h1>
</div>
<div>
<%= yield :top_bar_actions %>
</div>
</header>
<!-- INHALT DER JEWEILIGEN VIEW -->
<main class="p-4 md:p-6 flex-1 max-w-5xl w-full mx-auto">
<!-- Platzhalter für Flash-Meldungen (z.B. erfolgreiches Speichern) -->
<% flash.each do |type, message| %>
<div class="mb-4 p-4 text-sm rounded-lg border <%= type == 'notice' ? 'bg-green-50 text-green-800 border-green-200' : 'bg-red-50 text-red-800 border-red-200' %>">
<%= message %>
</div>
<% end %>
<%= yield %>
</main>
</div>
</div>
<!-- Globaler Platzhalter für das optionale Turbo-Frame-Modal (z.B. neue Personen anlegen) -->
<%= turbo_frame_tag "modal" %>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "Vault17" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="Vault17">
<meta name="mobile-web-app-capable" content="yes">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<!-- Globaler Platzhalter für dynamische Scripte (wie den QR-Kamera-Scanner) -->
<%= yield :head %>
</head>
<body class="bg-gray-50 text-gray-800 antialiased">
<!-- Hier wird direkt das zentrierte Card-Login-Formular reingeladen -->
<%= yield %>
</body>
</html>

View File

@@ -0,0 +1,143 @@
<!-- <p style="color: red"><%= alert %></p>
<h1>Change your password</h1>
<%= form_with(url: password_path, method: :patch) do |form| %>
<% if @user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% @user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :password_challenge, style: "display: block" %>
<%= form.password_field :password_challenge, required: true, autofocus: true, autocomplete: "current-password" %>
</div>
<div>
<%= form.label :password, "New password", style: "display: block" %>
<%= form.password_field :password, required: true, autocomplete: "new-password" %>
<div>12 characters minimum.</div>
</div>
<div>
<%= form.label :password_confirmation, "Confirm new password", style: "display: block" %>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password" %>
</div>
<div>
<%= form.submit "Save changes" %>
</div>
<% end %>
<br>
<div>
<%= link_to "Back", root_path %>
</div>
-->
<% content_for :title, "Mein Profil" %>
<div class="max-w-2xl mx-auto space-y-6">
<!-- INTERNE PROFIL-NAVIGATION (Tabs) -->
<div class="border-b border-gray-200">
<nav class="flex space-x-6" aria-label="Tabs">
<!-- Aktiver Tab: Profil & Sicherheit -->
<span class="border-blue-500 text-blue-600 border-b-2 py-4 px-1 text-sm font-semibold flex items-center gap-2">
<!-- Heroicon: user -->
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>
Sicherheit
</span>
<!-- Link zur Sitzungsübersicht von authentication-zero -->
<%= link_to sessions_path, class: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 border-b-2 py-4 px-1 text-sm font-medium flex items-center gap-2 transition" do %>
<!-- Heroicon: computer-desktop -->
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" /></svg>
Sitzungen & Geräte
<% end %>
<%= link_to authentications_events_path, class: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 border-b-2 py-4 px-1 text-sm font-medium flex items-center gap-2 transition" do %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" /></svg>
Aktivitäten
<% end %>
</nav>
</div>
<!-- Sektion 1: Benutzerdaten (Schreibgeschützt) -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8">
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 rounded-full bg-blue-600 text-white font-bold text-lg flex items-center justify-center shrink-0">
<%= Current.user.email.first(2).upcase %>
</div>
<div>
<h2 class="text-lg font-bold text-gray-800">Kontoinformationen</h2>
<p class="text-sm text-gray-500">Deine registrierten Profildaten in Vault171.</p>
</div>
</div>
<hr class="border-gray-200 mb-4">
<div>
<label class="block text-sm font-medium text-gray-400 uppercase tracking-wider mb-1">E-Mail-Adresse</label>
<div class="flex items-center gap-2 text-gray-800 font-medium">
<!-- Heroicon: envelope -->
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0l-7.5-4.615a2.25 2.25 0 0 1-1.07-1.916V6.75" /></svg>
<span><%= Current.user.email %></span>
</div>
</div>
</div>
<!-- Sektion 2: Passwort ändern Formular -->
<%= form_with(url: password_path, method: :patch, class: "bg-white border border-gray-200 rounded-xl shadow-sm p-6 md:p-8 space-y-6") do |form| %>
<!-- Fehleranzeige bei Fehlern der Passwort-Validierung -->
<% if @user.errors.any? %>
<div class="p-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200" role="alert">
<ul class="list-disc list-inside space-y-0.5 text-xs">
<% @user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<h3 class="text-lg font-bold text-gray-800">Sicherheit</h3>
<p class="text-sm text-gray-500 mt-1">Hier kannst du dein aktuelles Passwort aktualisieren.</p>
</div>
<hr class="border-gray-200">
<!-- Feld: Aktuelles Passwort (Wichtig zur Verifizierung) -->
<div>
<%= form.label :password_challenge, "Aktuelles Passwort", class: "block text-sm font-medium mb-2 text-gray-700" %>
<%= form.password_field :password_challenge, required: true, class: "py-2.5 px-4 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50 focus:border-blue-500 focus:ring-blue-500" %>
</div>
<!-- Felder für das neue Passwort -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<%= form.label :password, "Neues Passwort", class: "block text-sm font-medium mb-2 text-gray-700" %>
<%= form.password_field :password, required: true, autocomplete: "new-password", class: "py-2.5 px-4 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50 focus:border-blue-500 focus:ring-blue-500" %>
</div>
<div>
<%= form.label :password_confirmation, "Neues Passwort bestätigen", class: "block text-sm font-medium mb-2 text-gray-700" %>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", class: "py-2.5 px-4 block w-full border border-gray-300 rounded-lg text-sm bg-gray-50/50 focus:border-blue-500 focus:ring-blue-500" %>
</div>
</div>
<!-- Buttons -->
<div class="flex justify-end gap-x-2 pt-4 border-t border-gray-200">
<%= link_to "Abbrechen", root_path, class: "py-2.5 px-4 inline-flex items-center text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition" %>
<%= form.submit "Passwort aktualisieren", class: "py-2.5 px-4 inline-flex items-center text-sm font-semibold rounded-lg bg-blue-600 text-white hover:bg-blue-700 shadow-sm cursor-pointer transition" %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,121 @@
<!-- <h1>Sign up</h1>
<%= form_with(url: sign_up_path) do |form| %>
<% if @user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% @user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :email, style: "display: block" %>
<%= form.email_field :email, value: @user.email, required: true, autofocus: true, autocomplete: "email" %>
</div>
<div>
<%= form.label :password, style: "display: block" %>
<%= form.password_field :password, required: true, autocomplete: "new-password" %>
<div>12 characters minimum.</div>
</div>
<div>
<%= form.label :password_confirmation, style: "display: block" %>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password" %>
</div>
<div>
<%= form.submit "Sign up" %>
</div>
<% end %>
-->
<div class="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto w-full max-w-md">
<!-- App Logo (Paket) -->
<div class="flex justify-center text-blue-600">
<svg class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM3 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 019.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
</svg>
</div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">Konto erstellen</h2>
<p class="mt-2 text-center text-sm text-gray-600">
Oder <%= link_to "mit bestehendem Konto anmelden", sign_in_path, class: "font-medium text-blue-600 hover:text-blue-500 transition-colors" %>
</p>
</div>
<div class="mt-8 sm:mx-auto w-full max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-xl sm:px-10 border border-gray-200">
<%= form_with(url: sign_up_path, class: "space-y-6") do |form| %>
<!-- Fehlermeldungen bei Validierungsfehlern -->
<% if @user.errors.any? %>
<div class="p-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200" role="alert">
<div class="flex items-center gap-2 font-bold mb-2">
<svg class="h-5 w-5 text-red-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
<span>Registrierung fehlgeschlagen:</span>
</div>
<ul class="list-disc list-inside space-y-0.5 text-xs">
<% @user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<!-- Feld 1: E-Mail-Adresse -->
<div>
<%= form.label :email, "E-Mail-Adresse", class: "block text-sm font-medium text-gray-700 mb-1.5" %>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0l-7.5-4.615a2.25 2.25 0 01-1.07-1.916V6.75" />
</svg>
</div>
<%= form.email_field :email, required: true, autofocus: true, autocomplete: "email", class: "block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50" %>
</div>
</div>
<!-- Feld 2: Passwort -->
<div>
<%= form.label :password, "Passwort", class: "block text-sm font-medium text-gray-700 mb-1.5" %>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
</div>
<%= form.password_field :password, required: true, autocomplete: "new-password", class: "block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50" %>
</div>
<p class="mt-1.5 text-xs text-gray-400">Das Passwort muss mindestens 8 Zeichen lang sein.</p>
</div>
<!-- Feld 3: Passwort bestätigen -->
<div>
<%= form.label :password_confirmation, "Passwort bestätigen", class: "block text-sm font-medium text-gray-700 mb-1.5" %>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
</div>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", class: "block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50" %>
</div>
</div>
<!-- Submit Button -->
<div>
<%= form.submit "Konto registrieren", class: "w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors cursor-pointer" %>
</div>
<% end %>
</div>
</div>
</div>

View File

@@ -0,0 +1,144 @@
<!-- <p style="color: green"><%= notice %></p>
<h1>Devices & Sessions</h1>
<div id="sessions">
<% @sessions.each do |session| %>
<div id="<%= dom_id session %>">
<p>
<strong>User Agent:</strong>
<%= session.user_agent %>
</p>
<p>
<strong>Ip Address:</strong>
<%= session.ip_address %>
</p>
<p>
<strong>Created at:</strong>
<%= session.created_at %>
</p>
</div>
<p>
<%= button_to "Log out", session, method: :delete %>
</p>
<% end %>
</div>
<br>
<div>
<%= link_to "Back", root_path %>
</div>
-->
<% content_for :title, "Aktive Sitzungen & Geräte" %>
<div class="max-w-2xl mx-auto space-y-6">
<div class="border-b border-gray-200 mb-6">
<nav class="flex space-x-6" aria-label="Tabs">
<!-- Inaktiv verlinkt -->
<%= link_to edit_password_path, class: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 border-b-2 py-4 px-1 text-sm font-medium flex items-center gap-2 transition" do %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>
Sicherheit
<% end %>
<!-- Aktiv -->
<span class="border-blue-500 text-blue-600 border-b-2 py-4 px-1 text-sm font-semibold flex items-center gap-2">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" /></svg>
Sitzungen & Geräte
</span>
<!-- Diesen Link fügst du einfach in die Navigationsleisten der anderen beiden Views ein -->
<%= link_to authentications_events_path, class: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 border-b-2 py-4 px-1 text-sm font-medium flex items-center gap-2 transition" do %>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" /></svg>
Aktivitäten
<% end %>
</nav>
</div>
<!-- Info-Header -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6 flex flex-col sm:flex-row items-start sm:items-center gap-4 justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-blue-50 text-blue-600 rounded-lg shrink-0">
<!-- Heroicon: computer-desktop -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>
</div>
<div>
<h2 class="text-lg font-bold text-gray-800">Geräte-Verwaltung</h2>
<p class="text-sm text-gray-500">Hier siehst du alle Browser und Geräte, mit denen du aktuell in Vault171 angemeldet bist.</p>
</div>
</div>
<!-- Alle anderen Sitzungen beenden -->
</div>
<!-- GERÄTELISTE -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 class="font-bold text-gray-700 text-sm">Angemeldete Geräte</h3>
</div>
<ul class="divide-y divide-gray-200">
<% @sessions.each do |session| %>
<li class="p-6 flex items-center justify-between gap-4">
<div class="flex items-start gap-4 min-w-0">
<!-- Icon-Zuweisung je nach Gerätetyp (Desktop vs. Mobile) -->
<div class="p-2.5 bg-gray-100 text-gray-500 rounded-lg shrink-0 mt-0.5">
<% if session.user_agent.to_s.downcase.include?('mobile') || session.user_agent.to_s.downcase.include?('iphone') %>
<!-- Heroicon: device-phone-mobile -->
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 18.75h3" /></svg>
<% else %>
<!-- Heroicon: computer-desktop -->
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" /></svg>
<% end %>
</div>
<!-- Details zur Session -->
<div class="min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<h4 class="text-sm font-semibold text-gray-900 truncate">
<%= parse_user_agent(session.user_agent) %>
</h4>
<!-- Status-Badge für die aktuelle Sitzung -->
<% if session == Current.session %>
<span class="inline-flex items-center gap-1.5 py-0.5 px-2 rounded-full text-[10px] font-medium bg-green-100 text-green-800 border border-green-200">
<span class="h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse"></span>
Dieses Gerät
</span>
<% end %>
</div>
<p class="text-xs text-gray-500 mt-0.5 font-mono">IP: <%= session.ip_address || "Unbekannt" %></p>
<p class="text-xs text-gray-400 mt-1">
Letzte Aktivität: <%= l(session.updated_at, format: :short) %>
</p>
</div>
</div>
<!-- Einzelne Session beenden (außer man ist es selbst) -->
<div class="shrink-0">
<% if session != Current.session %>
<%= link_to session, method: :delete, title: "Gerät abmelden", data: { turbo_method: :delete, turbo_confirm: "Möchtest du dieses Gerät wirklich abmelden?" } do %>
<!-- Heroicon: trash -->
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
<% end %>
<% end %>
</div>
</li>
<% end %>
</ul>
</div>
</div>

View File

@@ -0,0 +1,90 @@
<!-- <p style="color: green"><%= notice %></p>
<p style="color: red"><%= alert %></p>
<h1>Sign in</h1>
<%= form_with(url: sign_in_path) do |form| %>
<div>
<%= form.label :email, style: "display: block" %>
<%= form.email_field :email, value: params[:email_hint], required: true, autofocus: true, autocomplete: "email" %>
</div>
<div>
<%= form.label :password, style: "display: block" %>
<%= form.password_field :password, required: true, autocomplete: "current-password" %>
</div>
<div>
<%= form.submit "Sign in" %>
</div>
<% end %>
<div>
<%= link_to "Sign up", sign_up_path %> |
<%= link_to "Forgot your password?", new_identity_password_reset_path %>
</div>
-->
<%# -------------------- %>
<div class="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto w-full max-w-md">
<!-- App Logo / Name -->
<div class="flex justify-center text-blue-600">
<svg class="h-16 w-16 text-blue-600 drop-shadow-sm" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l5.25 3.03M12 12.75v9" />
</svg>
</div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">In Vault171 einloggen</h2>
<p class="mt-2 text-center text-sm text-gray-600">
oder <%= link_to "ein neues Konto erstellen", sign_up_path, class: "font-medium text-blue-600 hover:text-blue-500 transition" %>
</p>
</div>
<div class="mt-8 sm:mx-auto w-full max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-xl sm:px-10 border border-gray-200">
<!-- Alert / Fehlermeldung bei falschem Passwort -->
<% if flash[:alert] %>
<div class="mb-4 p-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200 flex items-start gap-2" role="alert">
<svg class="h-5 w-5 text-red-500 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
<span class="font-medium"><%= alert %></span>
</div>
<% end %>
<%= form_with(url: sign_in_path, class: "space-y-6") do |form| %>
<div>
<%= form.label :email, "E-Mail-Adresse", class: "block text-sm font-medium text-gray-700 mb-1.5" %>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0l-7.5-4.615a2.25 2.25 0 01-1.07-1.916V6.75" />
</svg>
</div>
<%= form.email_field :email, value: params[:email], required: true, autofocus: true, autocomplete: "email", class: "block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50" %>
</div>
</div>
<div>
<div class="flex items-center justify-between mb-1.5">
<%= form.label :password, "Passwort", class: "block text-sm font-medium text-gray-700" %>
<div class="text-sm">
<%= link_to "Passwort vergessen?", new_identity_password_reset_path, class: "font-medium text-blue-600 hover:text-blue-500 transition" %>
</div>
</div>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
</div>
<%= form.password_field :password, required: true, autocomplete: "current-password", class: "block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 bg-gray-50/50" %>
</div>
</div>
<div>
<%= form.submit "Anmelden", class: "w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition cursor-pointer" %>
</div>
<% end %>
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<p>Hey there,</p>
<p>This is to confirm that <%= @user.email %> is the email you want to use on your account. If you ever lose your password, that's where we'll email a reset link.</p>
<p><strong>You must hit the link below to confirm that you received this email.</strong></p>
<p><%= link_to "Yes, use this email for my account", identity_email_verification_url(sid: @signed_id) %></p>
<hr>
<p>Have questions or need help? Just reply to this email and our support team will help you sort it out.</p>

View File

@@ -0,0 +1,11 @@
<p>Hey there,</p>
<p>Someone has invited you to the application, you can accept it through the link below.</p>
<p><%= link_to "Accept invitation", edit_identity_password_reset_url(sid: @signed_id) %></p>
<p>If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password.</p>
<hr>
<p>Have questions or need help? Just reply to this email and our support team will help you sort it out.</p>

View File

@@ -0,0 +1,11 @@
<p>Hey there,</p>
<p>Can't remember your password for <strong><%= @user.email %></strong>? That's OK, it happens. Just hit the link below to set a new one.</p>
<p><%= link_to "Reset my password", edit_identity_password_reset_url(sid: @signed_id) %></p>
<p>If you did not request a password reset you can safely ignore this email, it expires in 20 minutes. Only someone with access to this email account can reset your password.</p>
<hr>
<p>Have questions or need help? Just reply to this email and our support team will help you sort it out.</p>

View File

@@ -1,4 +1,19 @@
Rails.application.routes.draw do Rails.application.routes.draw do
namespace :authentications do
resources :events, only: :index
end
resource :invitation, only: [ :new, :create ]
get "sign_in", to: "sessions#new"
post "sign_in", to: "sessions#create"
get "sign_up", to: "registrations#new"
post "sign_up", to: "registrations#create"
resources :sessions, only: [ :index, :show, :destroy ]
resource :password, only: [ :edit, :update ]
namespace :identity do
resource :email, only: [ :edit, :update ]
resource :email_verification, only: [ :show, :create ]
resource :password_reset, only: [ :new, :edit, :create, :update ]
end
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
@@ -10,5 +25,5 @@ Rails.application.routes.draw do
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
# Defines the root path route ("/") # Defines the root path route ("/")
# root "posts#index" root "home#index"
end end

View File

@@ -0,0 +1,12 @@
class CreateUsers < ActiveRecord::Migration[8.1]
def change
create_table :users do |t|
t.string :email, null: false, index: { unique: true }
t.string :password_digest, null: false
t.boolean :verified, null: false, default: false
t.timestamps
end
end
end

View File

@@ -0,0 +1,11 @@
class CreateSessions < ActiveRecord::Migration[8.1]
def change
create_table :sessions do |t|
t.references :user, null: false, foreign_key: true
t.string :user_agent
t.string :ip_address
t.timestamps
end
end
end

View File

@@ -0,0 +1,12 @@
class CreateEvents < ActiveRecord::Migration[8.1]
def change
create_table :events do |t|
t.references :user, null: false, foreign_key: true
t.string :action, null: false
t.string :user_agent
t.string :ip_address
t.timestamps
end
end
end

44
db/schema.rb generated Normal file
View File

@@ -0,0 +1,44 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_05_20_205436) do
create_table "events", force: :cascade do |t|
t.string "action", null: false
t.datetime "created_at", null: false
t.string "ip_address"
t.datetime "updated_at", null: false
t.string "user_agent"
t.integer "user_id", null: false
t.index ["user_id"], name: "index_events_on_user_id"
end
create_table "sessions", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "ip_address"
t.datetime "updated_at", null: false
t.string "user_agent"
t.integer "user_id", null: false
t.index ["user_id"], name: "index_sessions_on_user_id"
end
create_table "users", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "email", null: false
t.string "password_digest", null: false
t.datetime "updated_at", null: false
t.boolean "verified", default: false, null: false
t.index ["email"], name: "index_users_on_email", unique: true
end
add_foreign_key "events", "users"
add_foreign_key "sessions", "users"
end

View File

@@ -0,0 +1,34 @@
require "test_helper"
class Identity::EmailVerificationsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = sign_in_as(users(:lazaro_nixon))
@user.update! verified: false
end
test "should send a verification email" do
assert_enqueued_email_with UserMailer, :email_verification, params: { user: @user } do
post identity_email_verification_url
end
assert_redirected_to root_url
end
test "should verify email" do
sid = @user.generate_token_for(:email_verification)
get identity_email_verification_url(sid: sid, email: @user.email)
assert_redirected_to root_url
end
test "should not verify email with expired token" do
sid = @user.generate_token_for(:email_verification)
travel 3.days
get identity_email_verification_url(sid: sid, email: @user.email)
assert_redirected_to edit_identity_email_url
assert_equal "That email verification link is invalid", flash[:alert]
end
end

View File

@@ -0,0 +1,25 @@
require "test_helper"
class Identity::EmailsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = sign_in_as(users(:lazaro_nixon))
end
test "should get edit" do
get edit_identity_email_url
assert_response :success
end
test "should update email" do
patch identity_email_url, params: { email: "new_email@hey.com", password_challenge: "Secret1*3*5*" }
assert_redirected_to root_url
end
test "should not update email with wrong password challenge" do
patch identity_email_url, params: { email: "new_email@hey.com", password_challenge: "SecretWrong1*3" }
assert_response :unprocessable_entity
assert_select "li", /Password challenge is invalid/
end
end

View File

@@ -0,0 +1,65 @@
require "test_helper"
class Identity::PasswordResetsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:lazaro_nixon)
end
test "should get new" do
get new_identity_password_reset_url
assert_response :success
end
test "should get edit" do
sid = @user.generate_token_for(:password_reset)
get edit_identity_password_reset_url(sid: sid)
assert_response :success
end
test "should send a password reset email" do
assert_enqueued_email_with UserMailer, :password_reset, params: { user: @user } do
post identity_password_reset_url, params: { email: @user.email }
end
assert_redirected_to sign_in_url
end
test "should not send a password reset email to a nonexistent email" do
assert_no_enqueued_emails do
post identity_password_reset_url, params: { email: "invalid_email@hey.com" }
end
assert_redirected_to new_identity_password_reset_url
assert_equal "You can't reset your password until you verify your email", flash[:alert]
end
test "should not send a password reset email to a unverified email" do
@user.update! verified: false
assert_no_enqueued_emails do
post identity_password_reset_url, params: { email: @user.email }
end
assert_redirected_to new_identity_password_reset_url
assert_equal "You can't reset your password until you verify your email", flash[:alert]
end
test "should update password" do
sid = @user.generate_token_for(:password_reset)
patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" }
assert_redirected_to sign_in_url
end
test "should not update password with expired token" do
sid = @user.generate_token_for(:password_reset)
travel 30.minutes
patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" }
assert_redirected_to new_identity_password_reset_url
assert_equal "That password reset link is invalid", flash[:alert]
end
end

View File

@@ -0,0 +1,24 @@
require "test_helper"
class PasswordsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = sign_in_as(users(:lazaro_nixon))
end
test "should get edit" do
get edit_password_url
assert_response :success
end
test "should update password" do
patch password_url, params: { password_challenge: "Secret1*3*5*", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" }
assert_redirected_to root_url
end
test "should not update password with wrong password challenge" do
patch password_url, params: { password_challenge: "SecretWrong1*3", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" }
assert_response :unprocessable_entity
assert_select "li", /Password challenge is invalid/
end
end

View File

@@ -0,0 +1,16 @@
require "test_helper"
class RegistrationsControllerTest < ActionDispatch::IntegrationTest
test "should get new" do
get sign_up_url
assert_response :success
end
test "should sign up" do
assert_difference("User.count") do
post sign_up_url, params: { email: "lazaronixon@hey.com", password: "Secret1*3*5*", password_confirmation: "Secret1*3*5*" }
end
assert_redirected_to root_url
end
end

View File

@@ -0,0 +1,46 @@
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:lazaro_nixon)
end
test "should get index" do
sign_in_as @user
get sessions_url
assert_response :success
end
test "should get new" do
get sign_in_url
assert_response :success
end
test "should sign in" do
post sign_in_url, params: { email: @user.email, password: "Secret1*3*5*" }
assert_redirected_to root_url
get root_url
assert_response :success
end
test "should not sign in with wrong credentials" do
post sign_in_url, params: { email: @user.email, password: "SecretWrong1*3" }
assert_redirected_to sign_in_url(email_hint: @user.email)
assert_equal "That email or password is incorrect", flash[:alert]
get root_url
assert_redirected_to sign_in_url
end
test "should sign out" do
sign_in_as @user
delete session_url(@user.sessions.last)
assert_redirected_to sessions_url
follow_redirect!
assert_redirected_to sign_in_url
end
end

6
test/fixtures/users.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
lazaro_nixon:
email: lazaronixon@hotmail.com
password_digest: <%= BCrypt::Password.create("Secret1*3*5*") %>
verified: true

View File

@@ -0,0 +1,19 @@
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
setup do
@user = users(:lazaro_nixon)
end
test "password_reset" do
mail = UserMailer.with(user: @user).password_reset
assert_equal "Reset your password", mail.subject
assert_equal [@user.email], mail.to
end
test "email_verification" do
mail = UserMailer.with(user: @user).email_verification
assert_equal "Verify your email", mail.subject
assert_equal [@user.email], mail.to
end
end

View File

@@ -2,14 +2,15 @@ ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment" require_relative "../config/environment"
require "rails/test_help" require "rails/test_help"
module ActiveSupport class ActiveSupport::TestCase
class TestCase # Run tests in parallel with specified workers
# Run tests in parallel with specified workers parallelize(workers: :number_of_processors)
parallelize(workers: :number_of_processors)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all fixtures :all
# Add more helper methods to be used by all tests here... # Add more helper methods to be used by all tests here...
def sign_in_as(user)
post(sign_in_url, params: { email: user.email, password: "Secret1*3*5*" }); user
end end
end end