| 1 | {{define "account_settings"}}{{template "top" .}} |
| 2 | {{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}} |
| 3 | {{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}} |
| 4 | <div class="pagehead"> |
| 5 | <p class="eyebrow">Account · {{.Account.Name}}</p> |
| 6 | <h1>Account settings</h1> |
| 7 | <p class="lede">The account itself — what it's called, and how it ends.</p> |
| 8 | </div> |
| 9 | <h2>Identity</h2> |
| 10 | <div class="tablewrap"> |
| 11 | <table> |
| 12 | <tr><th>Name</th><th>ID</th><th>Your role</th></tr> |
| 13 | <tr><td>{{.Account.Name}}</td><td><code>{{.Account.Sqid}}</code></td><td>{{.Role}}</td></tr> |
| 14 | </table> |
| 15 | </div> |
| 16 | {{with .AccountStore}} |
| 17 | <h2>Object storage</h2> |
| 18 | {{if .Unavailable}}<p>Object storage applications are unavailable on this deployment.</p> |
| 19 | {{else}} |
| 20 | <p>{{.State}}</p> |
| 21 | {{if eq .State "Approved"}}<p>Run <code>carlos store create --app <app></code> to provision a store when this deployment has a backend. Usage is metered.</p>{{end}} |
| 22 | {{if eq .State "Pending"}}<p>Waiting for a deployment operator.</p>{{end}} |
| 23 | {{if eq .State "Declined"}}<p>An account owner may apply again.</p>{{end}} |
| 24 | {{if and $.IsOwner .CanApply}} |
| 25 | <form method="post" action="{{$.Base}}/store/apply"> |
| 26 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| 27 | <label>What will you use storage for? (optional) <input type="text" name="note"></label> |
| 28 | <button type="submit">Apply for object storage</button> |
| 29 | </form> |
| 30 | {{end}} |
| 31 | {{end}} |
| 32 | {{if .Events}} |
| 33 | <h3>Storage activity</h3> |
| 34 | <div class="tablewrap"><table> |
| 35 | <tr><th>When</th><th>What</th></tr> |
| 36 | {{range .Events}}<tr><td>{{fmtTime .CreatedAt}}</td><td>{{.Detail}}</td></tr>{{end}} |
| 37 | </table></div> |
| 38 | {{end}} |
| 39 | {{end}} |
| 40 | {{if .IsOwner}} |
| 41 | <h2>Service credentials</h2> |
| 42 | <p class="small">Credentials a SERVER holds — a CI runner, a router, a job — not a person. Each one |
| 43 | is bound to this account, carries exactly one role, and never expires: revoking is the control, |
| 44 | and it works from here.</p> |
| 45 | {{/* The once-only secret block. The two "Reloading this page" sentences below |
| 46 | are a CONTROL, not decoration (C47): this response is a 200 rather than a |
| 47 | redirect, because a redirect would have to carry the plaintext somewhere |
| 48 | the next request could read it — and the price of that choice is that a |
| 49 | reload re-posts. Saying so, next to the secret, is what makes the trade |
| 50 | honest. Both sentences are pinned by |
| 51 | TestTheFreshSecretAppearsOnceAndOnlyInItsOwnResponse in svcpage_test.go; |
| 52 | if you are deleting one as redundant copy, read svcpage.go's package |
| 53 | comment first. */}} |
| 54 | {{with .ServiceSecret}} |
| 55 | <div class="banner"> |
| 56 | {{if .Rotated}} |
| 57 | {{if .ScopeKnown}} |
| 58 | <p><strong>New secret for {{.Name}} — role {{.Role}}, over {{.Scope}}:</strong></p> |
| 59 | {{else}} |
| 60 | <p><strong>New secret for {{.Name}}:</strong></p> |
| 61 | {{end}} |
| 62 | {{else}} |
| 63 | {{if .Rungs}}<p><strong>Minted {{.Name}}: role {{.Role}}, over {{.Scope}}, rungs {{.Rungs}}:</strong></p>{{else if .Commands}}<p><strong>Minted {{.Name}}: role {{.Role}}, over {{.Scope}}, commands {{.Commands}}:</strong></p>{{else}}<p><strong>Minted {{.Name}} — role {{.Role}}, over {{.Scope}}:</strong></p>{{end}} |
| 64 | {{end}} |
| 65 | <p><code>CARLOS_SERVICE_TOKEN={{.Token}}</code></p> |
| 66 | <p class="small">Shown once. Only its hash is stored — a lost secret means rotate, not another |
| 67 | look at this page.</p> |
| 68 | {{if and .Rotated (not .ScopeKnown)}} |
| 69 | <p class="small">This credential's role and scope could not be read back just now, so they are |
| 70 | not shown. Only the secret changed — the credential is otherwise exactly what it was.</p> |
| 71 | {{end}} |
| 72 | {{if .Rotated}} |
| 73 | <p class="small"><strong>Reloading this page rotates again</strong> and retires the secret |
| 74 | above. Copy it before you reload — if you lose it, rotate once more and redistribute; nothing |
| 75 | is destroyed, but this secret will not come back.</p> |
| 76 | {{else}} |
| 77 | <p class="small"><strong>Reloading this page</strong> tries to mint {{.Name}} a second time and |
| 78 | is refused, because the name is taken. The credential above keeps working — but this screen is |
| 79 | the only place its secret is ever shown, and a reload loses it.</p> |
| 80 | {{end}} |
| 81 | <p class="small">If you were just asked for your passkey, this page's address is the |
| 82 | confirmation step rather than the form: reloading it re-runs a confirmation that has already |
| 83 | been used, so it reports that the action has expired and changes nothing either way.</p> |
| 84 | <p class="small">Deliver it: <code>{{.DeliverCommand}}</code></p> |
| 85 | {{if .Rotated}} |
| 86 | <p class="small">ROTATION IS NOT REVOCATION: this refuses the credential's NEXT call with the |
| 87 | OLD secret, but does not stop work already in flight — a credential that may be COMPROMISED |
| 88 | needs Revoke, not another rotate.</p> |
| 89 | {{end}} |
| 90 | {{if .Doors}} |
| 91 | <p class="small">{{if .App}}It may (bound to app {{.App}} ONLY, despite the wildcards below):{{else}}It may:{{end}}</p> |
| 92 | <ul class="small"> |
| 93 | {{range .Doors}}<li><code>{{.}}</code></li>{{end}} |
| 94 | </ul> |
| 95 | {{end}} |
| 96 | </div> |
| 97 | {{end}} |
| 98 | {{if .ServicesUnknown}} |
| 99 | <p class="small">This account's credentials could not be read just now, so none are listed. That is |
| 100 | not the same as there being none — reload before concluding anything.</p> |
| 101 | {{else if .Services}} |
| 102 | <div class="tablewrap"> |
| 103 | <table> |
| 104 | <tr> |
| 105 | <th>Name</th><th>Role</th><th>Scope</th><th>Rungs</th><th>Commands</th><th>Label</th> |
| 106 | <th>Secret since</th><th>Last used</th><th>Minted by</th><th></th> |
| 107 | </tr> |
| 108 | {{range .Services}} |
| 109 | <tr> |
| 110 | <td>{{if .Name}}{{.Name}}{{else}}<span class="small">(unnamed)</span>{{end}}</td> |
| 111 | <td>{{.Role}}</td> |
| 112 | <td>{{if .App}}app {{.App}}{{else}}account-wide{{end}}</td> |
| 113 | <td>{{emptyOrDash .RungList}}</td> |
| 114 | <td>{{emptyOrDash .CommandList}}</td> |
| 115 | <td>{{emptyOrDash .Label}}</td> |
| 116 | <td>{{if .RotatedAt.IsZero}}—{{else}}{{fmtTime .RotatedAt}}{{end}}</td> |
| 117 | <td>{{if .LastUsed.IsZero}}—{{else}}{{fmtTime .LastUsed}}{{end}}</td> |
| 118 | <td>{{emptyOrDash .Address}}</td> |
| 119 | <td> |
| 120 | {{if .Name}} |
| 121 | <form method="post" action="{{$.Base}}/services/rotate"> |
| 122 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| 123 | <input type="hidden" name="name" value="{{.Name}}"> |
| 124 | <button class="linkish" type="submit">Rotate</button> |
| 125 | </form> |
| 126 | {{end}} |
| 127 | <form method="post" action="{{$.Base}}/services/revoke"> |
| 128 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| 129 | <input type="hidden" name="handle" value="{{.Handle}}"> |
| 130 | <button class="linkish danger" type="submit">Revoke</button> |
| 131 | </form> |
| 132 | </td> |
| 133 | </tr> |
| 134 | {{end}} |
| 135 | </table> |
| 136 | </div> |
| 137 | <p class="small">Revoking is immediate and needs nothing but this page: the credential's next call |
| 138 | fails, whatever secret it holds. Rotating replaces the secret and keeps everything else — the |
| 139 | name, the role, the scope, the history — so a credential that may be compromised needs Revoke, |
| 140 | not Rotate. A row with no name is an older app token; its listing id is its handle, and its |
| 141 | label is what it was minted as.</p> |
| 142 | {{else}} |
| 143 | <p class="small">No service credentials in this account.</p> |
| 144 | {{end}} |
| 145 | <h3>Mint a credential</h3> |
| 146 | <form class="signin" method="post" action="{{.Base}}/services/new"> |
| 147 | <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| 148 | <input type="text" name="name" placeholder="name, e.g. ci-runner"> |
| 149 | <select name="role"> |
| 150 | {{range .ServiceRoles}}<option value="{{.}}">{{.}}</option>{{end}} |
| 151 | </select> |
| 152 | <select name="app"> |
| 153 | <option value="">the whole account</option> |
| 154 | {{range .ServiceApps}}<option value="{{.}}">app {{.}}</option>{{end}} |
| 155 | </select> |
| 156 | <span class="small">Rungs, for a deploy credential:</span> |
| 157 | {{range .ServiceRungs}}<label class="small"><input type="checkbox" name="rungs" value="{{.}}"> {{.}}</label>{{end}} |
| 158 | <label class="small">Commands, for a run credential: |
| 159 | <input type="text" name="commands" placeholder="import classic, import sync"></label> |
| 160 | <button type="submit">Mint credential</button> |
| 161 | </form> |
| 162 | <p class="small">The secret is shown once, on the next screen, and is never recoverable from here. |
| 163 | Minting and rotating both ask for your passkey; revoking does not, so it stays one step in an |
| 164 | emergency.</p> |
| 165 | <h2>Errors</h2> |
| 166 | {{if .ErrLevelUnread}} |
| 167 | <p class="small">This account’s Errors log level could not be read just now, so it is not |
| 168 | shown and cannot be changed from here. Reload before concluding anything.</p> |
| 169 | {{else}} |
| 170 | <form class="signin" method="post" action="{{.Base}}/errors/level"> |
| 171 | <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| 172 | <label>Log lines counted as errors: |
| 173 | <select name="level"> |
| 174 | <option value="error"{{if eq .ErrLevelSetting.LogLevel "error"}} selected{{end}}>error and above</option> |
| 175 | <option value="warn"{{if eq .ErrLevelSetting.LogLevel "warn"}} selected{{end}}>warn and above</option> |
| 176 | </select> |
| 177 | </label> |
| 178 | <button type="submit">Save</button> |
| 179 | </form> |
| 180 | <p class="small">Currently {{.ErrLevelSetting.LogLevel}}, from the {{.ErrLevelSetting.LogLevelFrom}} |
| 181 | — the level for every app without its own setting, which an app’s own Settings tab |
| 182 | overrides. Widening one look to warn is always available on an app’s Errors tab and changes |
| 183 | nothing here.</p> |
| 184 | {{end}} |
| 185 | |
| 186 | <h2>Analytics</h2> |
| 187 | {{if .AnalyticsUnread}} |
| 188 | <p class="small">This account’s analytics setting could not be read just now. Reload before |
| 189 | concluding anything.</p> |
| 190 | {{else}} |
| 191 | <form class="signin" method="post" action="{{.Base}}/analytics/setting"> |
| 192 | <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| 193 | <label>Count page views at the edge: |
| 194 | <select name="count"> |
| 195 | <option value="off"{{if eq .AnalyticsSetting.Count "off"}} selected{{end}}>off</option> |
| 196 | <option value="edge"{{if eq .AnalyticsSetting.Count "edge"}} selected{{end}}>on (edge)</option> |
| 197 | </select> |
| 198 | </label> |
| 199 | <button type="submit">Save</button> |
| 200 | </form> |
| 201 | <p class="small">Currently {{.AnalyticsSetting.Count}}, from the {{.AnalyticsSetting.CountFrom}} |
| 202 | — for every app without its own setting, which an app’s own Settings tab overrides.</p> |
| 203 | {{end}} |
| 204 | <p class="small">When on, the edge counts HTML page views and daily visitors as it serves them: no |
| 205 | JavaScript, no cookie, no stored IP. A visitor is a salted hash that changes every day and cannot be |
| 206 | joined across days. Whether that needs consent where your visitors are is your call, which is why it |
| 207 | is off until you turn it on.</p> |
| 208 | |
| 209 | <h2>Delete this account</h2> |
| 210 | {{if .HasApps}} |
| 211 | <p class="small">This account has apps, so it can't be deleted. Delete or move the apps first; |
| 212 | an account with nothing in it can be retired from this page.</p> |
| 213 | {{else}} |
| 214 | <p class="small">An account with no apps can be retired. Type <strong>{{.Account.Name}}</strong> to |
| 215 | confirm, then confirm with your passkey. Members and pending invitations go with it; nothing |
| 216 | outside this account is touched.</p> |
| 217 | <form class="signin" method="post" action="{{.Base}}/delete"> |
| 218 | <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| 219 | <input type="text" name="confirm" placeholder="{{.Account.Name}}"> |
| 220 | <button type="submit">Delete account</button> |
| 221 | </form> |
| 222 | {{end}} |
| 223 | {{end}} |
| 224 | {{template "bottom" .}}{{end}} |
| 225 | |