gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit205d5fe4205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre205d5fe4/components/main.hl

5.8 KB

  1. // components/main.hl — THE SHELL: the application header (brand; top right the login: ident's
  2. // "Log in with ident" button, or who you are + Log out), the display-name prompt of a first login,
  3. // and the slot every page renders into. All CSS is in styles.hl.
  4. // THE SELECTOR (gitoria#14): `<ident-selector key=IDENT_API_KEY>` from <ident>/selector.js, as in tickets.
  5. // Choosing an identity fires `ident-login` (one-time code); /login.js hands it to the hidden input
  6. // #identcode, whose `change` goes to the face gitoriaLogin (server-side exchange, no reload). The element's
  7. // class is `in` / `out`; /login.js mirrors it into the selector's `loggedIn`. ident only answers a REGISTERED
  8. // origin (the main address), so on a repo address (the request's host, `host = null`, hybriel#74) the selector
  9. // carries the class `onrepo` and styles.hl hides it — the button stays there.
  10. // LOGIN: a plain link to <ident>/login?key=&return=<public url>/login/callback (project.hl
  11. // loginCallback); /login.js adds `?next=<this page>` at the click so the login returns here.
  12. // FIRST LOGIN: no display name yet → the name prompt; creating a repo waits for it.
  13. import { siteName } from '../project.hl'
  14. import { identKey, selectorScript, loginHref, infoOfSession, exchangeCode, ensureUser, userInfo, setUserName, userOfSession, slugOfHost } from '../users.hl'
  15. import { randomBytes } from 'hl:crypto'
  16. slot = null
  17. session = null
  18. host = null
  19. me = infoOfSession(session)
  20. loggedIn = me != null
  21. loggedOut = me == null
  22. needsName = me != null && !me.named
  23. myName = me != null && me.named ? me.name : 'you'
  24. buttonHref = loginHref
  25. onRepoHost = slugOfHost(host) != ''
  26. selectorClass = (me != null ? 'in' : 'out') + (onRepoHost ? ' onrepo' : '')
  27. selectorKey = identKey
  28. identScript = selectorScript
  29. loginError = ''
  30. hasLoginError = false
  31. nameDraft = ''
  32. nameError = ''
  33. // OFFLINE (mission 046, as tracker.worldapi.org#10): the service worker opens `/` from its cache without a network; the
  34. // shell then says so. hl:web gives a page no connection state and no "mounted" hook, so a tick asks the browser:
  35. // `netProbe` runs an endless 1 s CSS animation (styles.hl), and each `animationiteration` reads navigator.onLine — a
  36. // write only when it changed.
  37. offline = false
  38. View {
  39. body {
  40. applicationHeader {
  41. a { class = "brand" href = "/" siteName }
  42. userBox { id = "userbox"
  43. identSelector { id = "selector" key = selectorKey class = selectorClass }
  44. if (loggedIn) {
  45. span { id = "whoami" myName }
  46. button { id = "logout" type = "button" class = "quiet" "Log out" on click(e) { emit doLogout(e) } }
  47. }
  48. if (loggedOut) {
  49. a { id = "loginbutton" class = "button" href = buttonHref "Log in with ident" }
  50. }
  51. input { id = "identcode" type = "hidden" on change(e) { emit gotCode(e) } }
  52. }
  53. }
  54. if (hasLoginError) { p { id = "loginerror" class = "message banner" loginError } }
  55. if (offline) { offlineNote { id = "offline" "You are offline. Repositories and code need the network." } }
  56. netProbe { "aria-hidden" = "true" on animationiteration(e) { emit netTick(e) } }
  57. if (needsName) {
  58. namePrompt {
  59. form { id = "nameform"
  60. on submit(e) {
  61. e.preventDefault()
  62. emit saveName(e)
  63. }
  64. label { "Welcome! Choose the display name others see as the owner of your repositories"
  65. input { id = "displayname" name = "displayname" required = "required" maxlength = "60" value = nameDraft on input(e) { emit setNameDraft(e.target.value) } }
  66. }
  67. button { id = "namesave" type = "submit" "Save" }
  68. p { id = "nameerror" class = "message" nameError }
  69. }
  70. }
  71. }
  72. main { slot }
  73. script { src = identScript }
  74. script { src = "/login.js" }
  75. }
  76. }
  77. showMe = (info) => {
  78. loggedIn = true
  79. loggedOut = false
  80. needsName = !info.named
  81. myName = info.named ? info.name : 'you'
  82. selectorClass = onRepoHost ? 'in onrepo' : 'in'
  83. loginError = ''
  84. hasLoginError = false
  85. }
  86. showOut = () => {
  87. loggedIn = false
  88. loggedOut = true
  89. needsName = false
  90. myName = 'you'
  91. selectorClass = onRepoHost ? 'out onrepo' : 'out'
  92. }
  93. // the selector's code (via /login.js and the hidden input) → the server exchanges it
  94. on gotCode(e) {
  95. let code = e.target.value
  96. e.target.value = ''
  97. if (code == null || code == '') { return null }
  98. let r = emit server gitoriaLogin(code)
  99. if (r == null || r.error != null) {
  100. loginError = r != null ? r.error : 'the login failed'
  101. hasLoginError = true
  102. return null
  103. }
  104. showMe(r)
  105. }
  106. on client gitoriaSignedIn(tag, info) { showMe(info) }
  107. on client gitoriaSignedOut(tag) { showOut() }
  108. on setNameDraft(v) { nameDraft = v }
  109. on netTick(e) {
  110. let down = window.navigator.onLine == false
  111. if (down != offline) { offline = down }
  112. }
  113. on saveName(e) {
  114. let r = emit server gitoriaSaveName(nameDraft)
  115. if (r == null || r.error != null) {
  116. nameError = r != null ? r.error : 'could not save the name'
  117. return null
  118. }
  119. nameError = ''
  120. showMe(r)
  121. }
  122. on doLogout(e) {
  123. emit server gitoriaLogOut()
  124. showOut()
  125. }
  126. // ---- the faces (the trailing `session` is always the server's: hybriel #16) ----
  127. on server gitoriaLogin(code, session) {
  128. if (session == null) { return { error = 'no session — reload the page' } }
  129. let x = exchangeCode(code)
  130. if (x.error != null) { return { error = x.error } }
  131. let u = ensureUser(x.identity)
  132. if (u == null) { return { error = 'could not store the user' } }
  133. let tag = randomBytes(16)
  134. session.user = { id = u.id }
  135. session.data.tag = tag
  136. let info = userInfo(u)
  137. emit client gitoriaSignedIn(tag, info)
  138. return info
  139. }
  140. on server gitoriaSaveName(name, session) {
  141. let u = userOfSession(session)
  142. if (u == null) { return { error = 'log in with ident first' } }
  143. let r = setUserName(u.id, name)
  144. if (r.error != null) { return { error = r.error } }
  145. let info = userInfo(r.user)
  146. emit client gitoriaSignedIn(session.data.tag, info)
  147. return info
  148. }
  149. on server gitoriaLogOut(session) {
  150. if (session == null) { return { error = 'no session' } }
  151. emit client gitoriaSignedOut(session.data.tag)
  152. session.user = null
  153. return { ok = true }
  154. }

Branches

Latest commits

  • 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
  • 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
  • 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
  • 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
  • fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
  • 4a2d7125initial commitmre