Skip to content

Commit 8e9becb

Browse files
committed
Add auth/dashboard views, handlers, and Admin layout fixes
- Auth handler: login, register, forgotPassword, resetPassword, logout with mock session auth - Dashboard handler: auth guard, populates prc.authUser + stat/activity/quickAction mock data - Auth views: login, register, forgotPassword, resetPassword with Alpine-driven validation, password strength indicator (bars + requirements checklist), show/hide toggles, server-side error display, and loading states - Dashboard view: stat cards, activity feed, quick actions grid, user mini-card - Alpine components: PasswordStrength.js and AuthForm.js in resources/assets/js/components/ - Admin layout: replace prc.currentUser method calls with prc.authUser struct; fix logout link from /logout to /auth/logout - SCSS: _auth-forms.scss and views/_dashboard.scss partials added to app.scss pipeline Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ewf54RqUdHJTzyyPqtYX4i
1 parent 9bf6655 commit 8e9becb

14 files changed

Lines changed: 1525 additions & 31 deletions

File tree

app/handlers/Auth.bx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* Authentication Handler
3+
*
4+
* Handles login, registration, password recovery, and logout flows.
5+
* Uses session-based mock authentication until cbsecurity / cbauth are
6+
* fully wired to a database. Replace the mock blocks with real service
7+
* calls once the database layer is ready.
8+
*/
9+
class extends="coldbox.system.EventHandler" {
10+
11+
// ============================================================
12+
// Login
13+
// ============================================================
14+
15+
function login( event, rc, prc ){
16+
prc.title = "Sign In";
17+
event.setLayout( "AuthCenter" );
18+
19+
// Show flash message from a previous redirect (e.g. after password reset)
20+
if ( structKeyExists( session, "flash" ) ) {
21+
prc.flash = session.flash;
22+
structDelete( session, "flash" );
23+
}
24+
25+
if ( event.isPost() ) {
26+
var errors = {};
27+
28+
if ( !len( trim( rc.email ?: "" ) ) ) errors[ "email" ] = "Email address is required.";
29+
if ( !len( trim( rc.password ?: "" ) ) ) errors[ "password" ] = "Password is required.";
30+
31+
if ( !structIsEmpty( errors ) ) {
32+
prc.errors = errors;
33+
prc.oldInput = rc;
34+
return event.setView( "auth/login" );
35+
}
36+
37+
// ── MOCK: accept any non-empty credentials ─────────────────────────
38+
// Replace with: auth.authenticate( rc.email, rc.password )
39+
session.currentUser = {
40+
"id" : createUUID(),
41+
"firstName": "John",
42+
"lastName" : "Doe",
43+
"fullName" : "John Doe",
44+
"email" : rc.email,
45+
"role" : "Administrator",
46+
"initials" : "J"
47+
};
48+
// ── END MOCK ───────────────────────────────────────────────────────
49+
50+
return relocate( "dashboard" );
51+
}
52+
53+
event.setView( "auth/login" );
54+
}
55+
56+
// ============================================================
57+
// Register
58+
// ============================================================
59+
60+
function register( event, rc, prc ){
61+
prc.title = "Create Account";
62+
event.setLayout( "AuthSplit" );
63+
64+
if ( event.isPost() ) {
65+
var errors = {};
66+
67+
if ( !len( trim( rc.firstName ?: "" ) ) ) errors[ "firstName" ] = "First name is required.";
68+
if ( !len( trim( rc.lastName ?: "" ) ) ) errors[ "lastName" ] = "Last name is required.";
69+
if ( !len( trim( rc.email ?: "" ) ) ) errors[ "email" ] = "Email address is required.";
70+
if ( len( rc.password ?: "" ) < 8 ) errors[ "password" ] = "Password must be at least 8 characters.";
71+
if ( ( rc.password ?: "" ) != ( rc.passwordConfirm ?: "" ) )
72+
errors[ "passwordConfirm" ] = "Passwords do not match.";
73+
74+
if ( !structIsEmpty( errors ) ) {
75+
prc.errors = errors;
76+
prc.oldInput = rc;
77+
return event.setView( "auth/register" );
78+
}
79+
80+
// ── MOCK: create session user from form data ────────────────────────
81+
// Replace with: UserService.create( populatedUserObject )
82+
session.currentUser = {
83+
"id" : createUUID(),
84+
"firstName": rc.firstName,
85+
"lastName" : rc.lastName,
86+
"fullName" : rc.firstName & " " & rc.lastName,
87+
"email" : rc.email,
88+
"role" : "User",
89+
"initials" : uCase( left( rc.firstName, 1 ) )
90+
};
91+
// ── END MOCK ───────────────────────────────────────────────────────
92+
93+
return relocate( "dashboard" );
94+
}
95+
96+
event.setView( "auth/register" );
97+
}
98+
99+
// ============================================================
100+
// Forgot Password
101+
// ============================================================
102+
103+
function forgotPassword( event, rc, prc ){
104+
prc.title = "Forgot Password";
105+
event.setLayout( "AuthCenter" );
106+
107+
if ( event.isPost() ) {
108+
if ( !len( trim( rc.email ?: "" ) ) ) {
109+
prc.error = "Please enter your email address.";
110+
} else {
111+
// ── MOCK: simulate sending a reset email ──────────────────────
112+
// Replace with: UserService.sendPasswordResetEmail( rc.email )
113+
prc.success = "If an account exists for that address, we've sent a password reset link.";
114+
prc.emailSent = true;
115+
// ── END MOCK ──────────────────────────────────────────────────
116+
}
117+
}
118+
119+
event.setView( "auth/forgotPassword" );
120+
}
121+
122+
// ============================================================
123+
// Reset Password
124+
// ============================================================
125+
126+
function resetPassword( event, rc, prc ){
127+
prc.title = "Set New Password";
128+
prc.token = rc.token ?: ""; // Real impl: validate this token against DB
129+
event.setLayout( "AuthCenter" );
130+
131+
if ( event.isPost() ) {
132+
var errors = {};
133+
134+
if ( len( rc.password ?: "" ) < 8 )
135+
errors[ "password" ] = "Password must be at least 8 characters.";
136+
if ( ( rc.password ?: "" ) != ( rc.passwordConfirm ?: "" ) )
137+
errors[ "passwordConfirm" ] = "Passwords do not match.";
138+
139+
if ( !structIsEmpty( errors ) ) {
140+
prc.errors = errors;
141+
return event.setView( "auth/resetPassword" );
142+
}
143+
144+
// ── MOCK: password updated ─────────────────────────────────────────
145+
// Replace with: UserService.resetPassword( prc.token, rc.password )
146+
session.flash = {
147+
"type" : "success",
148+
"message": "Your password has been reset. Please sign in."
149+
};
150+
// ── END MOCK ──────────────────────────────────────────────────────
151+
152+
return relocate( "auth/login" );
153+
}
154+
155+
event.setView( "auth/resetPassword" );
156+
}
157+
158+
// ============================================================
159+
// Logout
160+
// ============================================================
161+
162+
function logout( event, rc, prc ){
163+
// ── MOCK: clear session ───────────────────────────────────────────────
164+
// Replace with: auth.logout()
165+
structDelete( session, "currentUser" );
166+
// ── END MOCK ─────────────────────────────────────────────────────────
167+
168+
return relocate( "auth/login" );
169+
}
170+
171+
}

app/handlers/Dashboard.bx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Dashboard Handler
3+
*
4+
* Protected section of the application. Enforces authentication and
5+
* populates the view data used by the Admin layout and dashboard views.
6+
*
7+
* Replace the session-based mock auth check with cbsecurity's secure()
8+
* or annotation-based protection once the security layer is configured.
9+
*/
10+
class extends="coldbox.system.EventHandler" {
11+
12+
function index( event, rc, prc ){
13+
// ── MOCK auth guard ───────────────────────────────────────────────────
14+
// Replace with cbsecurity annotation: @secured on the class or action,
15+
// or call: secure() which redirects via cbsecurity rules.
16+
if ( !structKeyExists( session, "currentUser" ) ) {
17+
return relocate( "auth/login" );
18+
}
19+
// ── END MOCK ─────────────────────────────────────────────────────────
20+
21+
prc.title = "Dashboard";
22+
23+
// Populate display user for the Admin layout (prc.authUser).
24+
// The Admin layout reads this struct — it does NOT call methods on a User object,
25+
// so it works for both this mock and a real authenticated user.
26+
var cu = session.currentUser;
27+
prc.authUser = {
28+
"name" : cu.fullName ?: ( cu.firstName ?: "Admin" ) & " " & ( cu.lastName ?: "" ),
29+
"email" : cu.email ?: "",
30+
"role" : cu.role ?: "User",
31+
"initials": cu.initials ?: uCase( left( cu.firstName ?: "A", 1 ) )
32+
};
33+
34+
// ── Mock data ─────────────────────────────────────────────────────────
35+
36+
prc.stats = [
37+
{
38+
"label" : "Total Users",
39+
"value" : "2,847",
40+
"change" : "+12.5%",
41+
"up" : true,
42+
"icon" : "ph-users",
43+
"iconCss": "bg-gradient-brand text-white"
44+
},
45+
{
46+
"label" : "Monthly Revenue",
47+
"value" : "$48,290",
48+
"change" : "+8.2%",
49+
"up" : true,
50+
"icon" : "ph-currency-dollar",
51+
"iconCss": "bg-success bg-opacity-10 text-success"
52+
},
53+
{
54+
"label" : "Active Sessions",
55+
"value" : "142",
56+
"change" : "-3.1%",
57+
"up" : false,
58+
"icon" : "ph-monitor",
59+
"iconCss": "bg-info bg-opacity-10 text-info"
60+
},
61+
{
62+
"label" : "Conversion Rate",
63+
"value" : "3.2%",
64+
"change" : "+0.4%",
65+
"up" : true,
66+
"icon" : "ph-trend-up",
67+
"iconCss": "bg-warning bg-opacity-10 text-warning"
68+
}
69+
];
70+
71+
prc.activities = [
72+
{
73+
"icon" : "ph-user-plus",
74+
"iconCss": "bg-gradient-brand text-white",
75+
"text" : "John Smith created a new account",
76+
"time" : "2 minutes ago"
77+
},
78+
{
79+
"icon" : "ph-currency-dollar",
80+
"iconCss": "bg-success bg-opacity-10 text-success",
81+
"text" : "Payment of $1,200 received from Acme Corp",
82+
"time" : "15 minutes ago"
83+
},
84+
{
85+
"icon" : "ph-file-text",
86+
"iconCss": "bg-info bg-opacity-10 text-info",
87+
"text" : "Monthly analytics report generated",
88+
"time" : "1 hour ago"
89+
},
90+
{
91+
"icon" : "ph-shield-check",
92+
"iconCss": "bg-warning bg-opacity-10 text-warning",
93+
"text" : "Security audit completed — no issues found",
94+
"time" : "3 hours ago"
95+
},
96+
{
97+
"icon" : "ph-database",
98+
"iconCss": "bg-secondary bg-opacity-10 text-secondary",
99+
"text" : "Automated database backup completed",
100+
"time" : "6 hours ago"
101+
}
102+
];
103+
104+
prc.quickActions = [
105+
{ "label": "New User", "icon": "ph-user-plus", "href": "##" },
106+
{ "label": "Generate Report","icon": "ph-chart-bar", "href": "##" },
107+
{ "label": "View Analytics","icon": "ph-trend-up", "href": "##" },
108+
{ "label": "Settings", "icon": "ph-gear", "href": "##" }
109+
];
110+
111+
// ── END mock data ─────────────────────────────────────────────────────
112+
113+
event.setLayout( "Admin" );
114+
event.setView( "dashboard/index" );
115+
}
116+
117+
}

app/layouts/Admin.bxm

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,22 +128,22 @@
128128

129129
<!--- ── Footer: user info ────────────────────────────────── --->
130130
<!---
131-
Replace the placeholder values with values from prc set by your auth interceptor:
132-
prc.currentUser.getFullName() / prc.currentUser.getRole()
133-
For a dropdown, the sidebar has overflow:hidden; use data-bs-strategy="fixed" on
134-
the .dropdown-menu or navigate to a dedicated profile page instead.
131+
prc.authUser is a flat display struct populated by every protected handler:
132+
{ name, email, role, initials }
133+
The sidebar has overflow:hidden so Bootstrap dropdowns clip — use a profile
134+
page link here instead of a dropdown.
135135
--->
136136
<div class="sidebar-footer">
137137
<a href="/profile" class="sidebar-user text-decoration-none">
138138
<span class="sidebar-user-avatar">
139-
#( prc.keyExists( "currentUser" ) ? left( prc.currentUser.getFullName(), 1 ) : "A" )#
139+
#( prc.keyExists( "authUser" ) ? prc.authUser.initials : "A" )#
140140
</span>
141141
<span class="sidebar-user-info">
142142
<span class="sidebar-user-name">
143-
#( prc.keyExists( "currentUser" ) ? prc.currentUser.getFullName() : "Admin User" )#
143+
#( prc.keyExists( "authUser" ) ? prc.authUser.name : "Admin User" )#
144144
</span>
145145
<span class="sidebar-user-role">
146-
#( prc.keyExists( "currentUser" ) ? prc.currentUser.getRole() : "Administrator" )#
146+
#( prc.keyExists( "authUser" ) ? prc.authUser.role : "Administrator" )#
147147
</span>
148148
</span>
149149
</a>
@@ -208,18 +208,18 @@
208208
data-bs-toggle="dropdown"
209209
aria-expanded="false">
210210
<span class="topbar-user-avatar">
211-
#( prc.keyExists( "currentUser" ) ? left( prc.currentUser.getFullName(), 1 ) : "A" )#
211+
#( prc.keyExists( "authUser" ) ? prc.authUser.initials : "A" )#
212212
</span>
213213
<span class="topbar-user-name">
214-
#( prc.keyExists( "currentUser" ) ? prc.currentUser.getFullName() : "Admin" )#
214+
#( prc.keyExists( "authUser" ) ? prc.authUser.name : "Admin" )#
215215
</span>
216216
<i class="ph ph-caret-down topbar-user-chevron" aria-hidden="true"></i>
217217
</button>
218218
<ul class="dropdown-menu dropdown-menu-end"
219219
aria-labelledby="topbarUserMenu">
220220
<li>
221221
<span class="dropdown-header">
222-
#( prc.keyExists( "currentUser" ) ? prc.currentUser.getEmail() : "admin@example.com" )#
222+
#( prc.keyExists( "authUser" ) ? prc.authUser.email : "admin@example.com" )#
223223
</span>
224224
</li>
225225
<li><hr class="dropdown-divider"></li>
@@ -237,7 +237,7 @@
237237
</li>
238238
<li><hr class="dropdown-divider"></li>
239239
<li>
240-
<a class="dropdown-item text-danger" href="/logout">
240+
<a class="dropdown-item text-danger" href="/auth/logout">
241241
<i class="ph ph-sign-out" aria-hidden="true"></i>
242242
Sign out
243243
</a>

0 commit comments

Comments
 (0)