merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -64,7 +64,7 @@ func run() error {
|
|||
}
|
||||
defer pool.Close()
|
||||
|
||||
hs := hoststats.NewPoller(cfg.BlobDir)
|
||||
hs := hoststats.NewPoller(cfg.DiskStatsPath)
|
||||
go hs.Run(ctx, hostStatsPollInterval)
|
||||
|
||||
srv, err := newServer(cfg, newReadStore(pool), hs)
|
||||
|
|
@ -97,10 +97,10 @@ type uiConfig struct {
|
|||
Password string
|
||||
Token string
|
||||
SessionKey []byte
|
||||
// BlobDir is the local blob-storage root, reused only to pick which
|
||||
// filesystem the dashboard's disk-free reading statfs's -- irrelevant when
|
||||
// TELESRV_BLOB_BACKEND=s3, where disk space isn't the storage constraint.
|
||||
BlobDir string
|
||||
// DiskStatsPath points the dashboard host-disk sampler at the local path
|
||||
// that matters for the selected blob backend: permanent localfs storage or
|
||||
// the S3 upload spool.
|
||||
DiskStatsPath string
|
||||
// Permissions is the right set a panel session is issued with, from
|
||||
// TELESRV_ADMIN_UI_PERMISSIONS. The shipped default is the single wildcard
|
||||
// entry, so introducing the permission model never locks an operator out of a
|
||||
|
|
@ -163,14 +163,21 @@ func loadConfig() (uiConfig, error) {
|
|||
Password: appCfg.AdminUIPassword,
|
||||
Token: appCfg.AdminUIToken,
|
||||
SessionKey: sum[:],
|
||||
DiskStatsPath: dashboardDiskPath(appCfg),
|
||||
Permissions: appCfg.AdminUIPermissions,
|
||||
HideThirdPartyVerification: appCfg.HideThirdPartyVerification,
|
||||
BlobDir: appCfg.BlobDir,
|
||||
IdentityDir: appCfg.IdentityDir,
|
||||
RepoRoot: repoRoot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func dashboardDiskPath(cfg config.Config) string {
|
||||
if strings.EqualFold(strings.TrimSpace(cfg.BlobBackendKind), "s3") && strings.TrimSpace(cfg.BlobStagingDir) != "" {
|
||||
return cfg.BlobStagingDir
|
||||
}
|
||||
return cfg.BlobDir
|
||||
}
|
||||
|
||||
func adminAPIURL(addr string) string {
|
||||
addr = strings.TrimSpace(addr)
|
||||
if addr == "" {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ import (
|
|||
// TELESRV_ADMIN_UI_PERMISSIONS and the ones the admin API enforces.
|
||||
const (
|
||||
permissionAll = "*"
|
||||
permissionPremiumManage = "premium.manage"
|
||||
permissionBotTokenRead = "bots.token.read"
|
||||
permissionVerificationReview = "verification.review"
|
||||
permissionVerificationRevoke = "verification.revoke"
|
||||
// Third-party bot verification. Deliberately not implied by the official
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ func (s *server) routes() http.Handler {
|
|||
mux.Handle("POST /api/actions/create-bot", s.requireAuthAPI(http.HandlerFunc(s.handleCreateBotAPI)))
|
||||
mux.Handle("POST /api/actions/create-broadcast", s.requireAuthAPI(http.HandlerFunc(s.handleCreateBroadcastAPI)))
|
||||
mux.Handle("POST /api/actions/delete-bot", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteBotAPI)))
|
||||
mux.Handle("POST /api/actions/export-bot-token", s.requireAuthAPI(http.HandlerFunc(s.handleExportBotTokenAPI)))
|
||||
mux.Handle("POST /api/actions/export-bot-token", s.requireAuthAPI(s.requirePermission(permissionBotTokenRead, http.HandlerFunc(s.handleExportBotTokenAPI))))
|
||||
mux.Handle("POST /api/actions/set-channel-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelVerifiedAPI)))
|
||||
mux.Handle("POST /api/actions/revoke-sessions", s.requireAuthAPI(http.HandlerFunc(s.handleRevokeSessionsAPI)))
|
||||
mux.Handle("POST /api/actions/delete-messages", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteMessagesAPI)))
|
||||
|
|
|
|||
|
|
@ -144,6 +144,53 @@ func TestModerationReadAPIDisablesBrowserCaching(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuthorizationRowJSONPreservesInt64AsDecimalStrings(t *testing.T) {
|
||||
const maxInt64 = int64(9223372036854775807)
|
||||
raw, err := json.Marshal(AuthorizationRow{AuthKeyID: maxInt64, Hash: maxInt64})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal authorization row: %v", err)
|
||||
}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(raw, &got); err != nil {
|
||||
t.Fatalf("unmarshal authorization row: %v", err)
|
||||
}
|
||||
for _, field := range []string{"AuthKeyID", "Hash"} {
|
||||
if got[field] != "9223372036854775807" {
|
||||
t.Fatalf("authorization %s = %#v, want exact decimal string", field, got[field])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeSessionsBFFForwardsExactAuthorizationHash(t *testing.T) {
|
||||
const authorizationHash = int64(2361577175213625973)
|
||||
var got admin.RevokeSessionsRequest
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/accounts/revoke-sessions" || r.Header.Get("Authorization") != "Bearer secret" {
|
||||
t.Fatalf("upstream request path=%q authorization=%q", r.URL.Path, r.Header.Get("Authorization"))
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(admin.CommandResult{CommandID: got.CommandID, Status: "completed", DryRun: got.DryRun})
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
srv := &server{cfg: uiConfig{AdminAPIURL: upstream.URL, AdminAPIToken: "secret"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/actions/revoke-sessions", strings.NewReader(`{
|
||||
"reason":"precision regression","confirm":false,"user_id":1001,
|
||||
"hash":"2361577175213625973"
|
||||
}`))
|
||||
req = req.WithContext(context.WithValue(req.Context(), actorKey{}, "operator"))
|
||||
rec := httptest.NewRecorder()
|
||||
srv.handleRevokeSessionsAPI(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got.Hash != authorizationHash || got.Actor != "operator" || !got.DryRun {
|
||||
t.Fatalf("forwarded revoke request = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMintCollectibleUsernameBFFForwardsActorAndTolerantScalars(t *testing.T) {
|
||||
const maxInt64 = int64(9223372036854775807)
|
||||
var got admin.MintCollectibleUsernameRequest
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
cmd/telesrv-admin/web/dist/index.html
vendored
4
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -23,8 +23,8 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin src="/assets/index-D8u51wND.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-hA2EpjuH.css">
|
||||
<script type="module" crossorigin src="/assets/index-CwTwvGWj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-0MvM-hpw.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
6
cmd/telesrv-admin/web/package-lock.json
generated
6
cmd/telesrv-admin/web/package-lock.json
generated
|
|
@ -758,9 +758,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -193,3 +193,20 @@ export function parseIDs(value: string, invalidMessage = "msg ids invalid"): num
|
|||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
// toUnixSeconds reads a datetime-local input. Such an input carries no zone, so
|
||||
// the value parses as the operator's local time — which is the time they picked.
|
||||
// 0 means "empty or unparseable", which every caller treats as "not scheduled".
|
||||
export function toUnixSeconds(value: string): number {
|
||||
if (!value.trim()) return 0;
|
||||
const ms = new Date(value).getTime();
|
||||
return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
|
||||
}
|
||||
|
||||
// localInputValue formats a datetime-local default some seconds out, so a
|
||||
// scheduling form never opens on a value the server would reject as past.
|
||||
export function localInputValue(offsetSeconds: number): string {
|
||||
const at = new Date(Date.now() + offsetSeconds * 1000);
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}:${pad(at.getMinutes())}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ export function ownerLabel(row: CollectibleUsernameRow, vaultLabel: string): str
|
|||
export function priceLabel(row: CollectibleUsernameRow): string {
|
||||
const base = formatCurrency(row.Amount, row.Currency);
|
||||
if (row.CryptoCurrency && row.CryptoAmount && row.CryptoAmount !== "0") {
|
||||
return `${base} (${formatCurrency(row.CryptoAmount, row.CryptoCurrency)})`;
|
||||
return `${formatCurrency(row.CryptoAmount, row.CryptoCurrency)} (${base})`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { Alert, PageFrame } from "./components/ui";
|
|||
// (cmd/telesrv-admin/security.go). "*" is the wildcard an operator configures for
|
||||
// a full-access session.
|
||||
export const permissionAll = "*";
|
||||
export const permissionPremiumManage = "premium.manage";
|
||||
export const permissionBotTokenRead = "bots.token.read";
|
||||
export const permissionVerificationReview = "verification.review";
|
||||
export const permissionVerificationRevoke = "verification.revoke";
|
||||
// Third-party verification is a separate mechanism and therefore a separate pair of
|
||||
|
|
|
|||
|
|
@ -425,3 +425,34 @@
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.secret-reveal {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
background: var(--warn-tint);
|
||||
border: 1px solid var(--warn-border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.secret-reveal-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--warn);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.secret-reveal-row { display: flex; align-items: center; gap: 10px; }
|
||||
.secret-reveal-value {
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
padding: 6px 10px;
|
||||
color: var(--text-soft);
|
||||
letter-spacing: .12em;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue