gitoriaLog in with ident

ident

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit98226b4198226b41antcolony#40: mission references point to the moved missionsmre98226b41/components/apps.hl

7.8 KB

  1. // components/apps.hl — `/apps`: THE ACCOUNT'S APPS (piece 2 of 6, ticket #25; CONCEPT.md
  2. // "Apps: any ident user can register apps. An app gets an API key.").
  3. // Register (name + the origins it runs on), list, edit, new secret, delete. An app has a
  4. // public API key and a secret for its server; the secret is shown ONCE, right after it is
  5. // made — ident stores only its sha256.
  6. //
  7. // The faces' names start with `app…`: a face is found by its EVENT NAME across all
  8. // files (hl:web), so they must not collide with home.hl's.
  9. parent './main.hl'
  10. import { accountOfSession } from '../store.hl'
  11. import { appRows, createApp, updateApp, regenerateSecret, deleteApp } from '../apps.hl'
  12. import { siteName } from '../project.hl'
  13. session = null
  14. me = accountOfSession(session)
  15. signedIn = me != null
  16. signedOut = me == null
  17. meEmail = me != null ? me.email : ''
  18. apps = me != null ? appRows(me.id) : []
  19. hasApps = apps.length > 0
  20. noApps = apps.length == 0
  21. __title = siteName + ' | your apps'
  22. // THE APP FORM — new and edit
  23. editing = false
  24. editId = '' // '' = a new app, else the app's id (a UUID)
  25. editHeading = ''
  26. fName = ''
  27. fOrigins = ''
  28. // THE SECRET, shown once
  29. showSecret = false
  30. secretFor = ''
  31. secret = ''
  32. message = ''
  33. notice = ''
  34. View {
  35. identCard {
  36. if (signedOut) {
  37. h1 { "Your apps" }
  38. p { id = "signinfirst" class = "lead" "Sign in to ident first: " a { href = "/" "sign in" } "." }
  39. }
  40. if (signedIn) {
  41. accountBar {
  42. userEmail { id = "meemail" meEmail }
  43. a { id = "toidentities" href = "/" "Your identities" }
  44. }
  45. if (showSecret) {
  46. secretBox { id = "secretbox"
  47. h2 { "Secret of " secretFor }
  48. 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." }
  49. code { id = "secret" secret }
  50. button { id = "secretdone" type = "button" class = "small" "I copied it" on click(e) { emit hideSecret(e) } }
  51. }
  52. }
  53. if (editing) {
  54. form { id = "appform"
  55. on submit(e) {
  56. e.preventDefault()
  57. emit saveApp(e)
  58. }
  59. h1 { id = "appformheading" editHeading }
  60. label { "Name"
  61. input { id = "fappname" name = "name" maxlength = "60" autocomplete = "off" required = "required" value = fName on input(e) { emit setName(e.target.value) } }
  62. }
  63. label { "Origins " fieldHint { "(where the app runs, like https://tickets.worldapi.org — several separated by spaces; the login button only returns there)" }
  64. input { id = "forigins" name = "origins" autocomplete = "off" required = "required" value = fOrigins on input(e) { emit setOrigins(e.target.value) } }
  65. }
  66. formButtons {
  67. button { id = "saveapp" type = "submit" "Save" }
  68. button { id = "cancelapp" type = "button" class = "quiet" "Cancel" on click(e) { emit closeForm(e) } }
  69. }
  70. }
  71. }
  72. section { id = "appssection"
  73. sectionHead {
  74. h1 { "Your apps" }
  75. button { id = "newapp" type = "button" class = "small" "Register an app" on click(e) { emit openNew(e) } }
  76. }
  77. if (noApps) { p { id = "noapps" class = "lead" "No apps yet. An app you register gets an API key for its “login with ident” button." } }
  78. ul { id = "apps"
  79. for (row of apps) {
  80. li { class = "app"
  81. appMain {
  82. appName { row.name }
  83. appOrigins { row.originsText }
  84. appKey { "API key " code { class = "apikey" row.apiKey } }
  85. }
  86. identityActions {
  87. button { type = "button" class = "quiet small edit" value = row.id "Edit" on click(e) { emit openEdit(e.target.value) } }
  88. button { type = "button" class = "quiet small newsecret" value = row.id "New secret" on click(e) { emit renew(e.target.value) } }
  89. button { type = "button" class = "quiet small danger delete" value = row.id "Delete" on click(e) { emit remove(e.target.value) } }
  90. }
  91. }
  92. }
  93. }
  94. }
  95. }
  96. p { id = "notice" class = "notice" notice }
  97. p { id = "message" class = "message" message }
  98. }
  99. }
  100. on setName(v) { fName = v }
  101. on setOrigins(v) { fOrigins = v }
  102. rowOf = (id) => {
  103. for (row of apps) { if ('' + row.id == '' + id) { return row } }
  104. return null
  105. }
  106. // the origins field: words separated by spaces or commas
  107. originList = (text) => {
  108. let out = []
  109. for (w of text.replaceAll(',', ' ').split(' ')) {
  110. let t = w.trim()
  111. if (t != '') { out.push(t) }
  112. }
  113. return out
  114. }
  115. setApps = (list) => {
  116. apps = list
  117. hasApps = list.length > 0
  118. noApps = list.length == 0
  119. return null
  120. }
  121. on openNew(e) {
  122. message = ''
  123. notice = ''
  124. editId = ''
  125. fName = ''
  126. fOrigins = ''
  127. editHeading = 'Register an app'
  128. editing = true
  129. }
  130. on openEdit(id) {
  131. message = ''
  132. notice = ''
  133. let row = rowOf(id)
  134. if (row == null) { return null }
  135. editId = row.id
  136. fName = row.name
  137. fOrigins = row.originsText
  138. editHeading = 'Edit ' + row.name
  139. editing = true
  140. }
  141. on closeForm(e) {
  142. editing = false
  143. message = ''
  144. }
  145. on hideSecret(e) {
  146. showSecret = false
  147. secret = ''
  148. secretFor = ''
  149. }
  150. on saveApp(e) {
  151. message = ''
  152. notice = ''
  153. let input = { name = fName origins = originList(fOrigins) }
  154. let r = null
  155. if (editId == '') {
  156. r = emit server appCreate(input)
  157. } else {
  158. r = emit server appUpdate('' + editId, input)
  159. }
  160. if (r == null) {
  161. message = 'the server did not answer — try again'
  162. return null
  163. }
  164. if (r.error != null) {
  165. message = r.error
  166. return null
  167. }
  168. setApps(r.apps)
  169. editing = false
  170. if (r.secret != null) {
  171. secretFor = r.app.name
  172. secret = r.secret
  173. showSecret = true
  174. notice = 'App registered.'
  175. } else {
  176. notice = 'App saved.'
  177. }
  178. }
  179. on renew(id) {
  180. message = ''
  181. notice = ''
  182. let row = rowOf(id)
  183. if (row == null) { return null }
  184. if (!confirm('Make a new secret for “' + row.name + '”? The current one stops working at once.')) { return null }
  185. let r = emit server appNewSecret('' + id)
  186. if (r == null) {
  187. message = 'the server did not answer — try again'
  188. return null
  189. }
  190. if (r.error != null) {
  191. message = r.error
  192. return null
  193. }
  194. setApps(r.apps)
  195. secretFor = r.app.name
  196. secret = r.secret
  197. showSecret = true
  198. notice = 'New secret made.'
  199. }
  200. on remove(id) {
  201. message = ''
  202. notice = ''
  203. let row = rowOf(id)
  204. if (row == null) { return null }
  205. if (!confirm('Delete the app “' + row.name + '”? Its API key stops working and the ids its users had in it are gone.')) { return null }
  206. let r = emit server appDelete('' + id)
  207. if (r == null) {
  208. message = 'the server did not answer — try again'
  209. return null
  210. }
  211. if (r.error != null) {
  212. message = r.error
  213. return null
  214. }
  215. setApps(r.apps)
  216. if ('' + editId == '' + id) { editing = false }
  217. showSecret = false
  218. secret = ''
  219. notice = 'App deleted.'
  220. }
  221. // ---- the faces ------------------------------------------------------------------------
  222. // The LAST parameter is the session (the framework appends it); accountOfSession only
  223. // gets the framework's own Session (hl:web never takes it from the peer, hybriel#16/#31).
  224. on server appCreate(input, session) {
  225. let me = accountOfSession(session)
  226. if (me == null) { return { error = 'you are not signed in' } }
  227. let r = createApp(me.id, input)
  228. if (r.error != null) { return r }
  229. return { app = r.app secret = r.secret apps = appRows(me.id) }
  230. }
  231. on server appUpdate(id, input, session) {
  232. let me = accountOfSession(session)
  233. if (me == null) { return { error = 'you are not signed in' } }
  234. let r = updateApp(me.id, id, input)
  235. if (r.error != null) { return r }
  236. return { app = r.app apps = appRows(me.id) }
  237. }
  238. on server appNewSecret(id, session) {
  239. let me = accountOfSession(session)
  240. if (me == null) { return { error = 'you are not signed in' } }
  241. let r = regenerateSecret(me.id, id)
  242. if (r.error != null) { return r }
  243. return { app = r.app secret = r.secret apps = appRows(me.id) }
  244. }
  245. on server appDelete(id, session) {
  246. let me = accountOfSession(session)
  247. if (me == null) { return { error = 'you are not signed in' } }
  248. let r = deleteApp(me.id, id)
  249. if (r.error != null) { return r }
  250. return { deleted = r.deleted apps = appRows(me.id) }
  251. }

Branches

Latest commits

  • 98226b41antcolony#40: mission references point to the moved missionsmre
  • 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