mbkauthe 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/docs/db.md +2 -2
- package/index.js +1 -1
- package/lib/routes/misc.js +20 -59
- package/package.json +1 -1
- package/views/loginmbkauthe.handlebars +4 -4
- package/views/sharedStyles.handlebars +22 -3
- package/views/test.handlebars +260 -0
package/README.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml)
|
|
7
|
+
[](https://www.npmjs.com/package/mbkauthe)
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
<p align="center">
|
|
9
|
-
<img height="64px" src="./public/
|
|
11
|
+
<img height="64px" src="./public/logo.png" alt="MBKAuthe" />
|
|
10
12
|
</p>
|
|
11
13
|
|
|
12
14
|
**MBKAuthe** is a production-ready authentication system for Node.js with Express and PostgreSQL. Features include secure login, 2FA, role-based access, OAuth (GitHub & Google), multi-session support, and multi-app user management.
|
package/docs/db.md
CHANGED
|
@@ -193,8 +193,8 @@ The system handles various error cases:
|
|
|
193
193
|
## Login Page Updates
|
|
194
194
|
|
|
195
195
|
The login page now includes:
|
|
196
|
-
- A "
|
|
197
|
-
- A "
|
|
196
|
+
- A "Login with GitHub" button
|
|
197
|
+
- A "Login with Google" button
|
|
198
198
|
- A divider ("or") between regular and OAuth login
|
|
199
199
|
- Proper styling that matches your existing design
|
|
200
200
|
|
package/index.js
CHANGED
|
@@ -70,7 +70,7 @@ if (process.env.test === "dev") {
|
|
|
70
70
|
const port = 5555;
|
|
71
71
|
app.use(router);
|
|
72
72
|
app.get(["/dashboard", "/home", "/"], (req, res) => {
|
|
73
|
-
|
|
73
|
+
return res.redirect("/mbkauthe/");
|
|
74
74
|
});
|
|
75
75
|
app.get("/showmessage", (req, res) => {
|
|
76
76
|
//uncomment line 26 on showmessage.handlebars for testing, after testing comment it back
|
package/lib/routes/misc.js
CHANGED
|
@@ -150,65 +150,26 @@ router.get('/user/profilepic', async (req, res) => {
|
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
// Test route
|
|
153
|
-
router.get('/test', validateSession, LoginLimit, async (req, res) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.actions{margin-top:14px;display:flex;flex-wrap:wrap;gap:8px}
|
|
174
|
-
.btn{display:inline-flex;align-items:center;gap:8px;padding:9px 14px;border-radius:10px;border:1px solid transparent;cursor:pointer;text-decoration:none}
|
|
175
|
-
.btn-primary{background:var(--accent);color:white}
|
|
176
|
-
.btn-outline{background:transparent;border-color:rgba(37,99,235,0.12);color:var(--accent)}
|
|
177
|
-
.btn-danger{background:var(--danger);color:#fff}
|
|
178
|
-
a{color:inherit}
|
|
179
|
-
@media (max-width:640px){.card{grid-template-columns:1fr;align-items:stretch}.avatar{width:80px;height:80px}}
|
|
180
|
-
</style>
|
|
181
|
-
</head>
|
|
182
|
-
<div class="container">
|
|
183
|
-
<div class="card" role="region" aria-label="User Session">
|
|
184
|
-
<div class="avatar" aria-hidden="true">
|
|
185
|
-
<img src="/mbkauthe/user/profilepic?u=${encodeURIComponent(req.session.user.username)}" alt="Avatar for ${req.session.user.username}" title="${req.session.user.fullname || req.session.user.username}" loading="lazy" decoding="async" width="96" height="96" onerror="this.style.display='none';var s=this.nextElementSibling; if(s) s.style.display='flex';" />
|
|
186
|
-
<div class="initials" aria-hidden="true" style="display:none">${(req.session.user.fullname && req.session.user.fullname[0]) || req.session.user.username[0]}</div>
|
|
187
|
-
</div>
|
|
188
|
-
<div class="meta">
|
|
189
|
-
<div>
|
|
190
|
-
<div class="status">✅ Authentication successful</div>
|
|
191
|
-
<h3 class="user-title">${req.session.user.username} <small style="color:var(--muted);font-weight:600">· ${req.session.user.role}</small></h3>
|
|
192
|
-
<p class="user-sub">ID: ${req.session.user.id} · Session: ${req.session.user.sessionId.slice(0, 8)}…</p>
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
<div class="details" aria-live="polite">
|
|
196
|
-
<div>Full Name: ${req.session.user.fullname || 'N/A'}</div>
|
|
197
|
-
<div>Allowed Apps: ${Array.isArray(req.session.user.allowedApps) ? req.session.user.allowedApps.join(', ') : 'N/A'}</div>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<div class="actions">
|
|
201
|
-
<button class="btn btn-primary" onclick="logout()" aria-label="Log out">Logout</button>
|
|
202
|
-
<a class="btn btn-outline" href="https://portal.mbktech.org/">Web Portal</a>
|
|
203
|
-
<a class="btn btn-outline" href="https://portal.mbktech.org/user/settings">User Settings</a>
|
|
204
|
-
<a class="btn btn-outline" href="/mbkauthe/info">Info Page</a>
|
|
205
|
-
<a class="btn btn-outline" href="/mbkauthe/login">Login Page</a>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
`);
|
|
211
|
-
}
|
|
153
|
+
router.get(['/test', '/'], validateSession, LoginLimit, async (req, res) => {
|
|
154
|
+
const { username, fullname, role, id, sessionId, allowedApps } = req.session.user;
|
|
155
|
+
|
|
156
|
+
const sessionExpiry = req.session.cookie?.expires
|
|
157
|
+
? new Date(req.session.cookie.expires).toISOString()
|
|
158
|
+
: null;
|
|
159
|
+
|
|
160
|
+
return res.render('test.handlebars', {
|
|
161
|
+
layout: false,
|
|
162
|
+
username,
|
|
163
|
+
fullname: fullname || 'N/A',
|
|
164
|
+
role,
|
|
165
|
+
id,
|
|
166
|
+
sessionIdShort: sessionId.slice(0, 8),
|
|
167
|
+
profilePicUrl: encodeURIComponent(username),
|
|
168
|
+
displayName: fullname || username,
|
|
169
|
+
initial: (fullname && fullname[0]) || username[0],
|
|
170
|
+
allowedApps: Array.isArray(allowedApps) ? allowedApps.join(', ') : 'N/A',
|
|
171
|
+
sessionExpiry
|
|
172
|
+
});
|
|
212
173
|
});
|
|
213
174
|
|
|
214
175
|
router.post('/test', validateSession, LoginLimit, async (req, res) => {
|
package/package.json
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
{{#if githubLoginEnabled }}
|
|
94
94
|
<a type="button" id="githubLoginBtn" class="btn-social btn-switch-side last-used-parent">
|
|
95
95
|
<i class="fab fa-github"></i>
|
|
96
|
-
<span>
|
|
96
|
+
<span>Login with GitHub</span>
|
|
97
97
|
{{#if lastLoginGithub}}<span class="last-used-badge" aria-hidden="true"
|
|
98
98
|
title="Last used">Last</span>{{/if}}
|
|
99
99
|
</a>
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
{{#if googleLoginEnabled }}
|
|
102
102
|
<a type="button" id="googleLoginBtn" class="btn-social btn-google-side last-used-parent">
|
|
103
103
|
<i class="fab fa-google"></i>
|
|
104
|
-
<span>
|
|
104
|
+
<span>Login with Google</span>
|
|
105
105
|
{{#if lastLoginGoogle}}<span class="last-used-badge" aria-hidden="true"
|
|
106
106
|
title="Last used">Last</span>{{/if}}
|
|
107
107
|
</a>
|
|
@@ -182,13 +182,13 @@
|
|
|
182
182
|
</div>
|
|
183
183
|
<div class="social-icons-row">
|
|
184
184
|
<a type="button" class="swi s mobile-github-btn last-used-parent"
|
|
185
|
-
title="
|
|
185
|
+
title="Login with GitHub">
|
|
186
186
|
<i class="fab fa-github"></i>
|
|
187
187
|
{{#if lastLoginGithub}}<span class="last-used-badge" aria-hidden="true"
|
|
188
188
|
title="Last used">Last</span>{{/if}}
|
|
189
189
|
</a>
|
|
190
190
|
<a type="button" class="swi s mobile-google-btn last-used-parent"
|
|
191
|
-
title="
|
|
191
|
+
title="Login with Google">
|
|
192
192
|
<i class="fab fa-google"></i>
|
|
193
193
|
{{#if lastLoginGoogle}}<span class="last-used-badge" aria-hidden="true"
|
|
194
194
|
title="Last used">Last</span>{{/if}}
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
display: flex;
|
|
49
49
|
justify-content: space-between;
|
|
50
50
|
align-items: center;
|
|
51
|
-
padding:
|
|
51
|
+
padding: 10px 1.5rem;
|
|
52
52
|
max-width: 1400px;
|
|
53
53
|
margin: 0 auto;
|
|
54
54
|
}
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
.logo-text {
|
|
70
70
|
font-size: 2rem;
|
|
71
71
|
font-weight: 700;
|
|
72
|
+
padding-top: 10px;
|
|
72
73
|
color: var(--light);
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
|
|
79
80
|
.logo-comp {
|
|
80
81
|
margin-top: 20px;
|
|
81
|
-
font-size:
|
|
82
|
+
font-size: 1rem;
|
|
82
83
|
font-weight: bold;
|
|
83
84
|
color: var(--text-light);
|
|
84
85
|
}
|
|
@@ -608,7 +609,7 @@
|
|
|
608
609
|
flex-direction: column;
|
|
609
610
|
width: 40%;
|
|
610
611
|
background: linear-gradient(135deg, rgba(33, 150, 243, 0.08), rgba(0, 184, 148, 0.08));
|
|
611
|
-
padding:
|
|
612
|
+
padding: 2.5rem 2rem;
|
|
612
613
|
border-right: 1px solid rgba(0, 184, 148, 0.3);
|
|
613
614
|
position: relative;
|
|
614
615
|
overflow: hidden;
|
|
@@ -803,6 +804,24 @@
|
|
|
803
804
|
}
|
|
804
805
|
}
|
|
805
806
|
|
|
807
|
+
@media (max-width: 400px) {
|
|
808
|
+
|
|
809
|
+
.logo svg,
|
|
810
|
+
.logo-image {
|
|
811
|
+
height: 30px;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.logo-text {
|
|
815
|
+
font-size: 1.6rem;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.mbkauthe-btn-login {
|
|
819
|
+
font-size: 1rem !important;
|
|
820
|
+
padding: 6px !important;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
}
|
|
824
|
+
|
|
806
825
|
@media (max-width: 768px) {
|
|
807
826
|
.login-box {
|
|
808
827
|
padding: 2rem;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
{{> head pageTitle="Session Test" ogUrl="/mbkauthe/test"}}
|
|
5
|
+
|
|
6
|
+
<body>
|
|
7
|
+
{{> header}}
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.session-card {
|
|
11
|
+
background: rgba(10, 20, 20, 0.95);
|
|
12
|
+
backdrop-filter: blur(10px);
|
|
13
|
+
border-radius: var(--border-radius);
|
|
14
|
+
padding: 2.5rem;
|
|
15
|
+
width: 100%;
|
|
16
|
+
max-width: 760px;
|
|
17
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
18
|
+
border: 1px solid rgba(0, 184, 148, 0.2);
|
|
19
|
+
position: relative;
|
|
20
|
+
z-index: 2;
|
|
21
|
+
transition: var(--transition);
|
|
22
|
+
display: grid;
|
|
23
|
+
grid-template-columns: 110px 1fr;
|
|
24
|
+
gap: 2rem;
|
|
25
|
+
align-items: start;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.session-card:hover {
|
|
29
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
|
30
|
+
border-color: rgba(0, 184, 148, 0.3);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.avatar {
|
|
34
|
+
width: 96px;
|
|
35
|
+
height: 96px;
|
|
36
|
+
border-radius: 50%;
|
|
37
|
+
background: linear-gradient(135deg, rgba(0, 184, 148, 0.2), rgba(33, 150, 243, 0.15));
|
|
38
|
+
border: 2px solid rgba(0, 184, 148, 0.3);
|
|
39
|
+
position: relative;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.avatar img {
|
|
47
|
+
width: 100%;
|
|
48
|
+
height: 100%;
|
|
49
|
+
object-fit: cover;
|
|
50
|
+
display: block;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.avatar .initials {
|
|
54
|
+
position: absolute;
|
|
55
|
+
inset: 0;
|
|
56
|
+
display: none;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
color: var(--accent);
|
|
61
|
+
font-size: 2rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.session-meta {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: 1rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.session-status {
|
|
71
|
+
color: var(--success);
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: 8px;
|
|
76
|
+
font-size: 0.95rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.session-title {
|
|
80
|
+
font-size: 1.3rem;
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
color: var(--light);
|
|
83
|
+
margin: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.session-sub {
|
|
87
|
+
color: var(--text-light);
|
|
88
|
+
font-size: 0.85rem;
|
|
89
|
+
margin: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.session-details {
|
|
93
|
+
background: rgba(0, 0, 0, 0.3);
|
|
94
|
+
border: 1px solid rgba(0, 184, 148, 0.15);
|
|
95
|
+
border-radius: var(--border-radius);
|
|
96
|
+
padding: 1rem 1.2rem;
|
|
97
|
+
font-family: 'Courier New', monospace;
|
|
98
|
+
font-size: 0.85rem;
|
|
99
|
+
color: var(--text-light);
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: 6px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.session-details div span {
|
|
106
|
+
color: var(--accent);
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.session-actions {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-wrap: wrap;
|
|
113
|
+
gap: 10px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.btn {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 8px;
|
|
120
|
+
padding: 10px 18px;
|
|
121
|
+
border-radius: var(--border-radius);
|
|
122
|
+
border: none;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
font-size: 0.9rem;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
text-decoration: none;
|
|
127
|
+
transition: var(--transition);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.btn-primary {
|
|
131
|
+
background: var(--accent);
|
|
132
|
+
color: var(--dark);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.btn-primary:hover {
|
|
136
|
+
background: #00a07f;
|
|
137
|
+
box-shadow: 0 4px 12px rgba(0, 184, 148, 0.4);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.btn-outline {
|
|
141
|
+
background: transparent;
|
|
142
|
+
border: 1px solid rgba(0, 184, 148, 0.35);
|
|
143
|
+
color: var(--accent);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.btn-outline:hover {
|
|
147
|
+
background: rgba(0, 184, 148, 0.1);
|
|
148
|
+
border-color: var(--accent);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.btn-danger {
|
|
152
|
+
background: var(--danger);
|
|
153
|
+
color: #fff;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.btn-danger:hover {
|
|
157
|
+
background: #e55f5f;
|
|
158
|
+
box-shadow: 0 4px 12px rgba(255, 118, 117, 0.4);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (max-width: 600px) {
|
|
162
|
+
.session-card {
|
|
163
|
+
grid-template-columns: 1fr;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.avatar {
|
|
167
|
+
width: 80px;
|
|
168
|
+
height: 80px;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
</style>
|
|
172
|
+
|
|
173
|
+
<section class="login-container">
|
|
174
|
+
{{> backgroundElements}}
|
|
175
|
+
|
|
176
|
+
<div class="session-card" role="region" aria-label="User Session">
|
|
177
|
+
<div class="avatar" aria-hidden="true">
|
|
178
|
+
<img src="/mbkauthe/user/profilepic?u={{profilePicUrl}}"
|
|
179
|
+
alt="Avatar for {{username}}"
|
|
180
|
+
title="{{displayName}}"
|
|
181
|
+
loading="lazy" decoding="async" width="96" height="96"
|
|
182
|
+
onerror="this.style.display='none';var s=this.nextElementSibling; if(s) s.style.display='flex';" />
|
|
183
|
+
<div class="initials" aria-hidden="true" style="display:none">{{initial}}</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="session-meta">
|
|
187
|
+
<div>
|
|
188
|
+
<div class="session-status">✅ Authentication successful</div>
|
|
189
|
+
<h3 class="session-title">{{username}} <small style="color:var(--text-light);font-weight:500">· {{role}}</small></h3>
|
|
190
|
+
<p class="session-sub">ID: {{id}} · Session: {{sessionIdShort}}…</p>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="session-details" aria-live="polite">
|
|
194
|
+
<div><span>Full Name:</span> {{fullname}}</div>
|
|
195
|
+
<div><span>Allowed Apps:</span> {{allowedApps}}</div>
|
|
196
|
+
<div><span>Session Expires:</span> <span id="session-expiry-display">{{#if sessionExpiry}}{{sessionExpiry}}{{else}}N/A{{/if}}</span></div>
|
|
197
|
+
{{#if sessionExpiry}}<div id="session-countdown"><span>Time Remaining:</span> <span id="countdown-value">calculating…</span></div>{{/if}}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div class="session-actions">
|
|
201
|
+
<button class="btn btn-danger" onclick="logout()" aria-label="Log out">
|
|
202
|
+
<i class="fas fa-sign-out-alt"></i> Logout
|
|
203
|
+
</button>
|
|
204
|
+
<a class="btn btn-outline" href="https://portal.mbktech.org/">
|
|
205
|
+
<i class="fas fa-globe"></i> Web Portal
|
|
206
|
+
</a>
|
|
207
|
+
<a class="btn btn-outline" href="https://portal.mbktech.org/user/settings">
|
|
208
|
+
<i class="fas fa-user-cog"></i> User Settings
|
|
209
|
+
</a>
|
|
210
|
+
<a class="btn btn-outline" href="/mbkauthe/info">
|
|
211
|
+
<i class="fas fa-info-circle"></i> Info Page
|
|
212
|
+
</a>
|
|
213
|
+
<a class="btn btn-outline" href="/mbkauthe/login">
|
|
214
|
+
<i class="fas fa-sign-in-alt"></i> Login Page
|
|
215
|
+
</a>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</section>
|
|
220
|
+
|
|
221
|
+
<script src="/mbkauthe/main.js"></script>
|
|
222
|
+
{{#if sessionExpiry}}
|
|
223
|
+
<script>
|
|
224
|
+
(function () {
|
|
225
|
+
var expiry = new Date('{{sessionExpiry}}');
|
|
226
|
+
var display = document.getElementById('session-expiry-display');
|
|
227
|
+
var countdown = document.getElementById('countdown-value');
|
|
228
|
+
|
|
229
|
+
// Format the expiry date in local time
|
|
230
|
+
if (display) {
|
|
231
|
+
display.textContent = expiry.toLocaleString();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function updateCountdown() {
|
|
235
|
+
var now = new Date();
|
|
236
|
+
var diff = expiry - now;
|
|
237
|
+
if (diff <= 0) {
|
|
238
|
+
countdown.textContent = 'Expired';
|
|
239
|
+
countdown.style.color = 'var(--danger)';
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
var h = Math.floor(diff / 3600000);
|
|
243
|
+
var m = Math.floor((diff % 3600000) / 60000);
|
|
244
|
+
var s = Math.floor((diff % 60000) / 1000);
|
|
245
|
+
countdown.textContent =
|
|
246
|
+
(h ? h + 'h ' : '') +
|
|
247
|
+
(h || m ? m + 'm ' : '') +
|
|
248
|
+
s + 's';
|
|
249
|
+
|
|
250
|
+
// warn when under 5 minutes
|
|
251
|
+
countdown.style.color = diff < 300000 ? 'var(--warning)' : 'var(--success)';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
updateCountdown();
|
|
255
|
+
setInterval(updateCountdown, 1000);
|
|
256
|
+
})();
|
|
257
|
+
</script>
|
|
258
|
+
{{/if}}
|
|
259
|
+
</body>
|
|
260
|
+
</html>
|