carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

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 &lt;app&gt;</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&rsquo;s Errors log level could not be read just now, so it is not
168shown 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&mdash; the level for every app without its own setting, which an app&rsquo;s own Settings tab
182overrides. Widening one look to warn is always available on an app&rsquo;s Errors tab and changes
183nothing here.</p>
184{{end}}
185
186<h2>Analytics</h2>
187{{if .AnalyticsUnread}}
188<p class="small">This account&rsquo;s analytics setting could not be read just now. Reload before
189concluding 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&mdash; for every app without its own setting, which an app&rsquo;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
205JavaScript, no cookie, no stored IP. A visitor is a salted hash that changes every day and cannot be
206joined across days. Whether that needs consent where your visitors are is your call, which is why it
207is 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