ident
All repositories: gitoria
7.8 KB
// components/apps.hl — `/apps`: THE ACCOUNT'S APPS (piece 2 of 6, ticket #25; CONCEPT.md// "Apps: any ident user can register apps. An app gets an API key.").// Register (name + the origins it runs on), list, edit, new secret, delete. An app has a// public API key and a secret for its server; the secret is shown ONCE, right after it is// made — ident stores only its sha256.//// The faces' names start with `app…`: a face is found by its EVENT NAME across all// files (hl:web), so they must not collide with home.hl's.parent './main.hl'import { accountOfSession } from '../store.hl'import { appRows, createApp, updateApp, regenerateSecret, deleteApp } from '../apps.hl'import { siteName } from '../project.hl'session = nullme = accountOfSession(session)signedIn = me != nullsignedOut = me == nullmeEmail = me != null ? me.email : ''apps = me != null ? appRows(me.id) : []hasApps = apps.length > 0noApps = apps.length == 0__title = siteName + ' | your apps'// THE APP FORM — new and editediting = falseeditId = '' // '' = a new app, else the app's id (a UUID)editHeading = ''fName = ''fOrigins = ''// THE SECRET, shown onceshowSecret = falsesecretFor = ''secret = ''message = ''notice = ''View {identCard {if (signedOut) {h1 { "Your apps" }p { id = "signinfirst" class = "lead" "Sign in to ident first: " a { href = "/" "sign in" } "." }}if (signedIn) {accountBar {userEmail { id = "meemail" meEmail }a { id = "toidentities" href = "/" "Your identities" }}if (showSecret) {secretBox { id = "secretbox"h2 { "Secret of " secretFor }p { class = "lead" "Shown only now — copy it to your app's server. Its API calls (the code exchange) use it; it never goes into a web page. Lost it? Make a new one." }code { id = "secret" secret }button { id = "secretdone" type = "button" class = "small" "I copied it" on click(e) { emit hideSecret(e) } }}}if (editing) {form { id = "appform"on submit(e) {e.preventDefault()emit saveApp(e)}h1 { id = "appformheading" editHeading }label { "Name"input { id = "fappname" name = "name" maxlength = "60" autocomplete = "off" required = "required" value = fName on input(e) { emit setName(e.target.value) } }}label { "Origins " fieldHint { "(where the app runs, like https://tickets.worldapi.org — several separated by spaces; the login button only returns there)" }input { id = "forigins" name = "origins" autocomplete = "off" required = "required" value = fOrigins on input(e) { emit setOrigins(e.target.value) } }}formButtons {button { id = "saveapp" type = "submit" "Save" }button { id = "cancelapp" type = "button" class = "quiet" "Cancel" on click(e) { emit closeForm(e) } }}}}section { id = "appssection"sectionHead {h1 { "Your apps" }button { id = "newapp" type = "button" class = "small" "Register an app" on click(e) { emit openNew(e) } }}if (noApps) { p { id = "noapps" class = "lead" "No apps yet. An app you register gets an API key for its “login with ident” button." } }ul { id = "apps"for (row of apps) {li { class = "app"appMain {appName { row.name }appOrigins { row.originsText }appKey { "API key " code { class = "apikey" row.apiKey } }}identityActions {button { type = "button" class = "quiet small edit" value = row.id "Edit" on click(e) { emit openEdit(e.target.value) } }button { type = "button" class = "quiet small newsecret" value = row.id "New secret" on click(e) { emit renew(e.target.value) } }button { type = "button" class = "quiet small danger delete" value = row.id "Delete" on click(e) { emit remove(e.target.value) } }}}}}}}p { id = "notice" class = "notice" notice }p { id = "message" class = "message" message }}}on setName(v) { fName = v }on setOrigins(v) { fOrigins = v }rowOf = (id) => {for (row of apps) { if ('' + row.id == '' + id) { return row } }return null}// the origins field: words separated by spaces or commasoriginList = (text) => {let out = []for (w of text.replaceAll(',', ' ').split(' ')) {let t = w.trim()if (t != '') { out.push(t) }}return out}setApps = (list) => {apps = listhasApps = list.length > 0noApps = list.length == 0return null}on openNew(e) {message = ''notice = ''editId = ''fName = ''fOrigins = ''editHeading = 'Register an app'editing = true}on openEdit(id) {message = ''notice = ''let row = rowOf(id)if (row == null) { return null }editId = row.idfName = row.namefOrigins = row.originsTexteditHeading = 'Edit ' + row.nameediting = true}on closeForm(e) {editing = falsemessage = ''}on hideSecret(e) {showSecret = falsesecret = ''secretFor = ''}on saveApp(e) {message = ''notice = ''let input = { name = fName origins = originList(fOrigins) }let r = nullif (editId == '') {r = emit server appCreate(input)} else {r = emit server appUpdate('' + editId, input)}if (r == null) {message = 'the server did not answer — try again'return null}if (r.error != null) {message = r.errorreturn null}setApps(r.apps)editing = falseif (r.secret != null) {secretFor = r.app.namesecret = r.secretshowSecret = truenotice = 'App registered.'} else {notice = 'App saved.'}}on renew(id) {message = ''notice = ''let row = rowOf(id)if (row == null) { return null }if (!confirm('Make a new secret for “' + row.name + '”? The current one stops working at once.')) { return null }let r = emit server appNewSecret('' + id)if (r == null) {message = 'the server did not answer — try again'return null}if (r.error != null) {message = r.errorreturn null}setApps(r.apps)secretFor = r.app.namesecret = r.secretshowSecret = truenotice = 'New secret made.'}on remove(id) {message = ''notice = ''let row = rowOf(id)if (row == null) { return null }if (!confirm('Delete the app “' + row.name + '”? Its API key stops working and the ids its users had in it are gone.')) { return null }let r = emit server appDelete('' + id)if (r == null) {message = 'the server did not answer — try again'return null}if (r.error != null) {message = r.errorreturn null}setApps(r.apps)if ('' + editId == '' + id) { editing = false }showSecret = falsesecret = ''notice = 'App deleted.'}// ---- the faces ------------------------------------------------------------------------// The LAST parameter is the session (the framework appends it); accountOfSession only// gets the framework's own Session (hl:web never takes it from the peer, hybriel#16/#31).on server appCreate(input, session) {let me = accountOfSession(session)if (me == null) { return { error = 'you are not signed in' } }let r = createApp(me.id, input)if (r.error != null) { return r }return { app = r.app secret = r.secret apps = appRows(me.id) }}on server appUpdate(id, input, session) {let me = accountOfSession(session)if (me == null) { return { error = 'you are not signed in' } }let r = updateApp(me.id, id, input)if (r.error != null) { return r }return { app = r.app apps = appRows(me.id) }}on server appNewSecret(id, session) {let me = accountOfSession(session)if (me == null) { return { error = 'you are not signed in' } }let r = regenerateSecret(me.id, id)if (r.error != null) { return r }return { app = r.app secret = r.secret apps = appRows(me.id) }}on server appDelete(id, session) {let me = accountOfSession(session)if (me == null) { return { error = 'you are not signed in' } }let r = deleteApp(me.id, id)if (r.error != null) { return r }return { deleted = r.deleted apps = appRows(me.id) }}
Branches
- mainmain branch
Latest commits
- ff805b9aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 51a7bcdfident: Hybriel master 73267707 (#122); /code uses the new page() signature; pending address passed as parameter; once-checksmre
- 836f644fident#24: installable app (manifest, service worker, data-free offline /start), own iconmre
- 8bebbbf2deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- cc063ea2deploy.sh: never send .git or .gitignore to Byrodinmre
- 81b15b7bState of 2026-09-27, before the move to gitoriamre