code wiki / _hdl_build / nx_hr_admin_page.nx
nx_hr_admin_page.nx source
↩ module page · 98 lines · 6761 B
1// nx_hr_admin_page.nx -- the sovereign /hr ADMIN CONSOLE page (server-rendered, ZERO third-party JS). It renders
2// the live roster from the HR store -- the "logins you see" the operator asked for -- and the invite/suspend
3// controls, so Nishi HR does the admin work through a real surface. OWNER-GATED: the daemon serves this ONLY to an
4// active superadmin (it passes viewer_is_super, computed from the caller's session uid == HR cred_id); a non-owner
5// gets an "owner-only" stub with NO roster (leak-check). The owner row shows no suspend control (no self-lockout,
6// by construction). Forms POST to /hr/invite and /hr/suspend (the daemon's owner-gated handlers).
7// roster row source: distinct cred_ids in the append-only store, LATEST record wins (handle/level/status).
8import "nx_hr.nx" // hr_puts / hr_putn (storage-agnostic string helpers)
9import "nx_hr_sov.nx" // CUTOVER: roster read from the seg_store (hrs_index_count / hrs_index_get_cred / hrs_get / hrs_field)
10import "nx_syscalls.nx"
11
12func hap_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } return v }
13
14const HAP_MAX: i64 = 256
15const HAP_BUF: i64 = 1048576
16
17func hap_eq64(a: *u8, b: *u8) -> i64 { var i: i64=0; while i<64 { if a[i]!=b[i] { return 0 } i=i+1 } return 1 }
18func hap_cpy(dst: *u8, src: *u8, n: i64) -> i64 { var i: i64=0; while i<n { dst[i]=src[i]; i=i+1 } return 0 }
19func hap_putrange(out: *u8, o: i64, src: *u8, s: i64, e: i64) -> i64 { var oo: i64=o; var i: i64=s; while i<e { out[oo]=src[i]; oo=oo+1; i=i+1 } return oo }
20func hap_role(out: *u8, o: i64, lvl: i64) -> i64 {
21 if lvl >= 3 { return hr_puts(out, o, "owner (super-admin)" as *u8) }
22 if lvl == 1 { return hr_puts(out, o, "member" as *u8) }
23 return hr_puts(out, o, "none" as *u8)
24}
25
26const HAP_HEAD: *u8 = "<!doctype html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi HR</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:5vh auto;padding:0 18px;color:#16202e}h1{font-size:1.25rem}table{border-collapse:collapse;width:100%;margin:12px 0}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #e1e6f0}th{color:#667;font-size:.85rem}.b{font-weight:600}.s-active{color:#137333}.s-invited{color:#a56300}.s-suspended{color:#a50e0e}form{display:inline;margin:0}input,select{padding:7px;border:1px solid #b9c2d6;border-radius:5px}button{padding:7px 12px;background:#0b2545;color:#fff;border:0;border-radius:5px;cursor:pointer}.sus{background:#a50e0e;padding:4px 9px;font-size:.85rem}</style></head><body>" as *u8
27
28// emit the full /hr console into `out`; returns length. viewer_is_super gates the whole roster.
29func hap_emit(hr_store: *u8, viewer_is_super: i64, out: *u8, cap: i64) -> i64 {
30 var o: i64 = 0
31 o = hr_puts(out, o, HAP_HEAD)
32 if viewer_is_super == 0 {
33 o = hr_puts(out, o, "<h1>Nishi HR</h1><p>This console is owner-only. Your account is not a super-admin.</p></body></html>" as *u8)
34 return o
35 }
36 o = hr_puts(out, o, "<h1>Nishi HR — family roster</h1><p style=\"color:#667;font-size:.9rem\">The logins on this realm. Members sign up on the home network using the handle you invited.</p>" as *u8)
37 o = hr_puts(out, o, "<table><tr><th>handle</th><th>role</th><th>status</th><th></th></tr>" as *u8)
38
39 // collect distinct cred_ids + LATEST (handle, level, status) from the SEG_STORE via the roster index (cutover)
40 let hbuf: *u8 = sys_mmap(HAP_MAX*64)
41 let hlen: *i64 = sys_mmap(HAP_MAX*8) as *i64
42 let lvls: *i64 = sys_mmap(HAP_MAX*8) as *i64
43 let sbuf: *u8 = sys_mmap(HAP_MAX*16)
44 let slen: *i64 = sys_mmap(HAP_MAX*8) as *i64
45 var ndist: i64 = 0
46 let ncreds: i64 = hrs_index_count(hr_store)
47 let credbuf: *u8 = sys_mmap(72)
48 let ok: *i64 = sys_mmap(64*8) as *i64; let ov: *i64 = sys_mmap(64*8) as *i64
49 var ci: i64 = 0
50 while ci < ncreds {
51 if hrs_index_get_cred(hr_store, ci, credbuf) == 1 { if ndist < HAP_MAX {
52 let nf: i64 = hrs_get(hr_store, credbuf, ok, ov, 64)
53 if nf > 0 {
54 let hf: *u8 = hrs_field(ok, ov, nf, "handle" as *u8)
55 let lf: *u8 = hrs_field(ok, ov, nf, "level" as *u8)
56 let sf: *u8 = hrs_field(ok, ov, nf, "status" as *u8)
57 var hl: i64 = 0; if (hf as i64)!=0 { while hf[hl]!=(0 as u8){hl=hl+1} } if hl>63 {hl=63}
58 if (hf as i64)!=0 { hap_cpy((hbuf as i64 + ndist*64) as *u8, hf, hl) } hlen[ndist]=hl
59 if (lf as i64)!=0 { lvls[ndist]=hap_atoi(lf) } else { lvls[ndist]=0 }
60 var sl: i64 = 0; if (sf as i64)!=0 { while sf[sl]!=(0 as u8){sl=sl+1} } if sl>15 {sl=15}
61 if (sf as i64)!=0 { hap_cpy((sbuf as i64 + ndist*16) as *u8, sf, sl) } slen[ndist]=sl
62 ndist=ndist+1
63 }
64 } }
65 ci = ci + 1
66 }
67
68 // emit a row per distinct cred
69 var d: i64 = 0
70 while d < ndist {
71 let hp: *u8 = (hbuf as i64 + d*64) as *u8
72 let sp: *u8 = (sbuf as i64 + d*16) as *u8
73 o = hr_puts(out, o, "<tr><td class=b>" as *u8)
74 o = hap_putrange(out, o, hp, 0, hlen[d])
75 o = hr_puts(out, o, "</td><td>" as *u8)
76 o = hap_role(out, o, lvls[d])
77 o = hr_puts(out, o, "</td><td class=\"s-" as *u8)
78 o = hap_putrange(out, o, sp, 0, slen[d])
79 o = hr_puts(out, o, "\">" as *u8)
80 o = hap_putrange(out, o, sp, 0, slen[d])
81 o = hr_puts(out, o, "</td><td>" as *u8)
82 // owner gets NO suspend control (no self-lockout, by construction)
83 if lvls[d] >= 3 { o = hr_puts(out, o, "—" as *u8) }
84 else {
85 o = hr_puts(out, o, "<form method=post action=/hr/suspend><input type=hidden name=handle value=\"" as *u8)
86 o = hap_putrange(out, o, hp, 0, hlen[d])
87 o = hr_puts(out, o, "\"><button class=sus>suspend</button></form>" as *u8)
88 }
89 o = hr_puts(out, o, "</td></tr>" as *u8)
90 d = d + 1
91 }
92 o = hr_puts(out, o, "</table>" as *u8)
93
94 // invite control (POSTs to the daemon's owner-gated handler)
95 o = hr_puts(out, o, "<h3>Invite a family member</h3><form method=post action=/hr/invite><input name=handle placeholder=\"handle (their first name)\"> <select name=level><option value=1>member</option><option value=3>owner</option></select> <button>Invite</button></form>" as *u8)
96 o = hr_puts(out, o, "<p style=\"color:#667;font-size:.85rem;margin-top:16px\">Invited members sign up on the home network — they choose their own passphrase (the server never sees it).</p><p style=\"margin-top:8px\"><a href=/hr/audit>View the audit trail →</a></p></body></html>" as *u8)
97 return o
98}