Skip to content

Commit 862b7d3

Browse files
authored
fix: disable button for offline door (#11)
* feat: authgate * feat: access, git history is ducked cus i accidently nuked stuff * audit logs * docs: keys, audit, and access * fix: silly type error * door button should be disabled when offline * feat: logs for access remote * auth method * linting and formating * Add Docker workflow for building images * test * lint check * sign in bomboclat lint * members * add fetchAccessType and submit form on ctrl+enther
1 parent dc71ad2 commit 862b7d3

25 files changed

Lines changed: 5750 additions & 1289 deletions

.github/workflows/docker.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Docker
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
env:
12+
IMAGE_NAME: gatekeeper-frontend
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v7
19+
20+
- name: Build image
21+
run: docker build . --file Dockerfile

.github/workflows/lint.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Lint
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
lint:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v7
16+
17+
- name: Install and lint
18+
run: |
19+
npm ci
20+
npm run lint
21+
npm run format:check

.prettierignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.next
3+
out
4+
build
5+
pnpm-lock.yaml

.prettierrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"semi": true,
3+
"singleQuote": false,
4+
"trailingComma": "es5"
5+
}

README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,27 @@ Web interface for [gatekeeper-mqtt](https://github.com/ComputerScienceHouse/gate
55
Built with Next.js 15, next-auth v5 (CSH SSO), react-bootstrap, and [csh-material-bootstrap](https://github.com/ComputerScienceHouse/csh-material-bootstrap).
66

77
## Features
8+
89
### Doors
10+
911
- **Doors dashboard** — live online/offline status for all doors, updated every 30 seconds
1012
- **Unlock** — send an unlock command to any door with a single click
1113
- **Access feedback** — door-specific error messages on 403 (e.g. safety seminar, RTP status)
1214

1315
### Logs
16+
1417
- **Access logs** — log viewer for door access events
1518

1619
### Keys
20+
1721
- **Keys Management** — Disable/Delete user keys using a simple lookup
1822

1923
### AccessGate
24+
2025
- Enforce RTPs to state a reason in order to access Keys/Logs page
2126

2227
### Audit
28+
2329
- **Audit Logs** — log viewer for page access events
2430

2531
## Prerequisites
@@ -37,12 +43,12 @@ cp .env.local.example .env.local
3743

3844
Edit `.env.local`:
3945

40-
| Variable | Description |
41-
|----------|-------------|
46+
| Variable | Description |
47+
| --------------------- | ------------------------------------------------------------------------------------- |
4248
| `NEXT_PUBLIC_API_URL` | Base URL of the gatekeeper-mqtt API, no trailing slash (e.g. `http://localhost:3001`) |
43-
| `AUTH_SECRET` | Session encryption secret — generate with `openssl rand -base64 32` |
44-
| `AUTH_OIDC_ID` | OIDC client ID from CSH SSO |
45-
| `AUTH_OIDC_SECRET` | OIDC client secret from CSH SSO |
49+
| `AUTH_SECRET` | Session encryption secret — generate with `openssl rand -base64 32` |
50+
| `AUTH_OIDC_ID` | OIDC client ID from CSH SSO |
51+
| `AUTH_OIDC_SECRET` | OIDC client secret from CSH SSO |
4652

4753
The OIDC client must have `http://localhost:3000/api/auth/callback/csh` in its allowed redirect URIs (replace `localhost:3000` with your deployment URL in production).
4854

@@ -68,4 +74,3 @@ npm test
6874
npm run build
6975
npm start
7076
```
71-

app/audit/page.tsx

Lines changed: 95 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ function formatTimestamp(iso: string): string {
3434
});
3535
}
3636

37-
async function fetchAuditEntries(token: string, cursor?: string, search?: string): Promise<AuditResponse> {
37+
async function fetchAuditEntries(
38+
token: string,
39+
cursor?: string,
40+
search?: string
41+
): Promise<AuditResponse> {
3842
const params = new URLSearchParams();
3943
if (cursor) params.set("cursor", cursor);
4044
if (search) params.set("search", search);
@@ -43,13 +47,13 @@ async function fetchAuditEntries(token: string, cursor?: string, search?: string
4347

4448
function AuditPageInner() {
4549
const { data: session } = useSession();
46-
const [entries, setEntries] = useState<AuditEntry[]>([]);
47-
const [loading, setLoading] = useState(true);
48-
const [search, setSearch] = useState("");
49-
const [searchInput, setSearchInput] = useState("");
50-
const [cursorStack, setCursorStack] = useState<Array<string | null>>([null]);
51-
const [nextCursor, setNextCursor] = useState<string | null>(null);
52-
const [pageIndex, setPageIndex] = useState(0);
50+
const [entries, setEntries] = useState<AuditEntry[]>([]);
51+
const [loading, setLoading] = useState(true);
52+
const [search, setSearch] = useState("");
53+
const [searchInput, setSearchInput] = useState("");
54+
const [cursorStack, setCursorStack] = useState<Array<string | null>>([null]);
55+
const [nextCursor, setNextCursor] = useState<string | null>(null);
56+
const [pageIndex, setPageIndex] = useState(0);
5357
const token = session?.accessToken ?? "";
5458
const sessionError = session?.error;
5559

@@ -62,27 +66,34 @@ function AuditPageInner() {
6266
return () => clearTimeout(t);
6367
}, [searchInput]);
6468

65-
const loadPage = useCallback(async (idx: number, cursor: string | null) => {
66-
if (!token) return;
67-
setLoading(true);
68-
try {
69-
const data = await fetchAuditEntries(token, cursor ?? undefined, search);
70-
setEntries(data.entries);
71-
setNextCursor(data.cursor);
72-
setPageIndex(idx);
73-
setCursorStack((prev) => {
74-
if (idx + 1 < prev.length) return prev;
75-
if (!data.cursor) return prev;
76-
const next = [...prev];
77-
next[idx + 1] = data.cursor;
78-
return next;
79-
});
80-
} catch (err) {
81-
console.error("Failed to load audit entries");
82-
} finally {
83-
setLoading(false);
84-
}
85-
}, [token, search]);
69+
const loadPage = useCallback(
70+
async (idx: number, cursor: string | null) => {
71+
if (!token) return;
72+
setLoading(true);
73+
try {
74+
const data = await fetchAuditEntries(
75+
token,
76+
cursor ?? undefined,
77+
search
78+
);
79+
setEntries(data.entries);
80+
setNextCursor(data.cursor);
81+
setPageIndex(idx);
82+
setCursorStack((prev) => {
83+
if (idx + 1 < prev.length) return prev;
84+
if (!data.cursor) return prev;
85+
const next = [...prev];
86+
next[idx + 1] = data.cursor;
87+
return next;
88+
});
89+
} catch (err) {
90+
console.error("Failed to load audit entries");
91+
} finally {
92+
setLoading(false);
93+
}
94+
},
95+
[token, search]
96+
);
8697

8798
useEffect(() => {
8899
setCursorStack([null]);
@@ -94,7 +105,8 @@ function AuditPageInner() {
94105
const hasPrev = pageIndex > 0;
95106
const hasNext = nextCursor !== null;
96107

97-
const goPrev = () => hasPrev && loadPage(pageIndex - 1, cursorStack[pageIndex - 1]);
108+
const goPrev = () =>
109+
hasPrev && loadPage(pageIndex - 1, cursorStack[pageIndex - 1]);
98110
const goNext = () => {
99111
if (!hasNext) return;
100112
const nextIdx = pageIndex + 1;
@@ -104,10 +116,28 @@ function AuditPageInner() {
104116
const PaginationControls = () => (
105117
<ul className="pagination pagination-sm justify-content-center mb-0">
106118
<li className={`page-item ${!hasPrev ? "disabled" : ""}`}>
107-
<a className="page-link" href="#" onClick={(e) => { e.preventDefault(); goPrev(); }}>Prev</a>
119+
<a
120+
className="page-link"
121+
href="#"
122+
onClick={(e) => {
123+
e.preventDefault();
124+
goPrev();
125+
}}
126+
>
127+
Prev
128+
</a>
108129
</li>
109130
<li className={`page-item ${!hasNext ? "disabled" : ""}`}>
110-
<a className="page-link" href="#" onClick={(e) => { e.preventDefault(); goNext(); }}>Next</a>
131+
<a
132+
className="page-link"
133+
href="#"
134+
onClick={(e) => {
135+
e.preventDefault();
136+
goNext();
137+
}}
138+
>
139+
Next
140+
</a>
111141
</li>
112142
</ul>
113143
);
@@ -117,7 +147,9 @@ function AuditPageInner() {
117147
<div className="row mb-3 align-items-center">
118148
<div className="col-12 col-md-4 mb-2 mb-md-0">
119149
<div className="input-group">
120-
<span className="input-group-text"><Icon path={mdiMagnify} size={0.75} /></span>
150+
<span className="input-group-text">
151+
<Icon path={mdiMagnify} size={0.75} />
152+
</span>
121153
<input
122154
type="text"
123155
className="form-control"
@@ -131,7 +163,10 @@ function AuditPageInner() {
131163

132164
<div className="card">
133165
<div className="card-header d-flex justify-content-between align-items-center">
134-
<span><Icon path={mdiHistory} size={0.85} className="me-2" />Audit Logs</span>
166+
<span>
167+
<Icon path={mdiHistory} size={0.85} className="me-2" />
168+
Audit Logs
169+
</span>
135170
</div>
136171
<div className="card-body py-2 border-bottom">
137172
<PaginationControls />
@@ -143,17 +178,32 @@ function AuditPageInner() {
143178
</div>
144179
) : entries.length === 0 ? (
145180
<div className="card-body text-center py-5 text-muted">
146-
<Icon path={mdiHistory} size={2} className="mb-3 opacity-25 d-block mx-auto" />
181+
<Icon
182+
path={mdiHistory}
183+
size={2}
184+
className="mb-3 opacity-25 d-block mx-auto"
185+
/>
147186
<p className="mb-0">No entries match your filters.</p>
148187
{search && (
149-
<button className="btn btn-link btn-sm mt-2" onClick={() => { setSearchInput(""); setSearch(""); }}>
188+
<button
189+
className="btn btn-link btn-sm mt-2"
190+
onClick={() => {
191+
setSearchInput("");
192+
setSearch("");
193+
}}
194+
>
150195
Clear filters
151196
</button>
152197
)}
153198
</div>
154199
) : (
155200
<div className="table-responsive">
156-
<Table hover size="sm" className="mb-0" style={{ fontSize: "0.875rem" }}>
201+
<Table
202+
hover
203+
size="sm"
204+
className="mb-0"
205+
style={{ fontSize: "0.875rem" }}
206+
>
157207
<thead>
158208
<tr>
159209
<th style={{ width: "16%" }}>Timestamp</th>
@@ -166,10 +216,14 @@ function AuditPageInner() {
166216
<tbody>
167217
{entries.map((entry) => (
168218
<tr key={entry._id}>
169-
<td style={{ whiteSpace: "nowrap" }}>{formatTimestamp(entry.timestamp)}</td>
219+
<td style={{ whiteSpace: "nowrap" }}>
220+
{formatTimestamp(entry.timestamp)}
221+
</td>
170222
<td>{entry.username}</td>
171223
<td>{entry.name}</td>
172-
<td><span>{entry.action}</span></td>
224+
<td>
225+
<span>{entry.action}</span>
226+
</td>
173227
<td>{entry.reason}</td>
174228
</tr>
175229
))}
@@ -182,9 +236,7 @@ function AuditPageInner() {
182236
className="card-footer d-grid align-items-center"
183237
style={{ gridTemplateColumns: "1fr auto 1fr" }}
184238
>
185-
<small className="text-muted">
186-
Page {pageIndex + 1} &nbsp;
187-
</small>
239+
<small className="text-muted">Page {pageIndex + 1} &nbsp;</small>
188240
<div className="justify-self-center">
189241
<PaginationControls />
190242
</div>
@@ -201,4 +253,4 @@ export default function AuditPage() {
201253
<AuditPageInner />
202254
</AuthGate>
203255
);
204-
}
256+
}

app/auth-error/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ function AuthErrorContent() {
1010
const params = useSearchParams();
1111
const error = params.get("error") ?? "Unknown";
1212
const description = params.get("error_description");
13-
const message = AUTH_ERROR_MESSAGES[error] ?? `An authentication error occurred (${error}).`;
13+
const message =
14+
AUTH_ERROR_MESSAGES[error] ??
15+
`An authentication error occurred (${error}).`;
1416

1517
return (
1618
<Container className="mt-5" style={{ maxWidth: 480 }}>

0 commit comments

Comments
 (0)