Files
anonymous_github/public/partials/admin/user.htm
T
tdurieux 8fc7ac5175 Add user ban/activate feature
Add admin endpoints to ban and activate users, block banned users
from all auth flows (OAuth, token login, bearer auth), and invalidate
existing sessions on next request. Includes frontend translation and
user detail page ban/activate buttons.
2026-05-07 05:41:12 +03:00

231 lines
12 KiB
HTML

<div class="container paper-page admin-page">
<div class="paper-crumbs"><a href="/admin/users">Users</a> &nbsp;/&nbsp; <span class="here">{{userInfo.username || 'Profile'}}</span></div>
<h1 class="paper-page-title">{{userInfo.username || 'User'}}</h1>
<nav class="admin-nav">
<a href="/admin/"><i class="fas fa-code-branch"></i> Repositories</a>
<a href="/admin/users" class="active"><i class="fas fa-users"></i> Users</a>
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav>
<div class="user-detail-card" ng-if="userInfo">
<div class="user-header">
<img ng-src="{{userInfo.photo}}" ng-if="userInfo.photo" width="56" height="56" />
<div>
<h1>
{{userInfo.username}}
<span class="status-dot-wrap">
<span class="status-dot" ng-class="{'status-ready': userInfo.status == 'active', 'status-removed': userInfo.status != 'active'}"></span>
<span ng-bind="userInfo.status | title"></span>
</span>
<span class="type-badge type-repo" ng-if="userInfo.isAdmin">Admin</span>
</h1>
<div class="user-actions" style="margin-top: 4px;">
<button class="btn btn-sm text-danger" ng-if="userInfo.status !== 'banned'" ng-click="banUser()"><i class="fas fa-ban"></i> Ban</button>
<button class="btn btn-sm" ng-if="userInfo.status === 'banned' || userInfo.status === 'removed'" ng-click="activateUser()"><i class="fas fa-check-circle"></i> Activate</button>
</div>
</div>
</div>
<div class="user-detail-grid">
<div class="detail-label">ID</div>
<div class="detail-value">{{userInfo._id}}</div>
<div class="detail-label">Email</div>
<div class="detail-value">{{userInfo.emails[0].email}}</div>
<div class="detail-label">Access token</div>
<div class="detail-value" style="font-family: var(--font-mono); font-size: 0.85rem;">{{userInfo.accessTokens.github}}</div>
<div class="detail-label">GitHub</div>
<div class="detail-value">
<a ng-href="https://github.com/{{userInfo.username}}" target="_blank">
<i class="fab fa-github"></i> {{userInfo.username}}
</a>
</div>
<div class="detail-label">GitHub repos</div>
<div class="detail-value">
{{userInfo.repositories.length}} repositories
<button class="btn btn-sm ml-2" ng-click="showRepos = !showRepos">
{{showRepos ? 'Hide' : 'Show'}}
</button>
<button class="btn btn-sm ml-1" ng-click="getGitHubRepositories()">
<i class="fas fa-sync"></i> Refresh
</button>
</div>
</div>
<div ng-if="showRepos" style="margin-top: 20px">
<div class="paper-section-eyebrow">GitHub repositories</div>
<div class="paper-table w-100" style="margin-top: 10px;">
<div class="paper-table-row" ng-repeat="repo in userInfo.repositories" style="grid-template-columns: 1fr 160px;">
<div class="cell-anon" role="cell">
<span class="type-badge type-repo">Repo</span>
<div class="anon-text">
<span class="repo-name" ng-bind="repo.name"></span>
</div>
</div>
<div class="cell-expires" role="cell">
<i class="fas fa-database"></i> {{::repo.size | humanFileSize}}
</div>
</div>
</div>
</div>
</div>
<div class="admin-section-header" ng-if="userInfo && userInfo.isAdmin && user && user.username == userInfo.username">
<h2><i class="fas fa-key"></i> API tokens</h2>
<span class="section-count">{{tokens.length}}</span>
</div>
<div ng-if="userInfo && userInfo.isAdmin && user && user.username == userInfo.username" class="user-detail-card">
<p class="paper-page-lede">Personal API tokens for this admin account. Send as <code>Authorization: Bearer &lt;token&gt;</code> to authenticate without GitHub OAuth (useful for development).</p>
<form ng-submit="createToken()" class="d-flex" style="gap: 8px; margin-bottom: 12px;">
<input type="text" class="form-control" ng-model="tokenForm.name" placeholder="Token name (e.g. dev-laptop)" required />
<button type="submit" class="btn btn-primary"><i class="fas fa-plus"></i> Generate</button>
</form>
<div ng-if="tokenForm.plaintext" class="alert alert-warning" role="alert">
<strong>Copy this token now — it will not be shown again:</strong>
<pre style="white-space: pre-wrap; word-break: break-all; margin: 8px 0 0; font-family: var(--font-mono); font-size: 0.85rem;">{{tokenForm.plaintext}}</pre>
<button class="btn btn-sm" ng-click="tokenForm.plaintext = null">Dismiss</button>
</div>
<div class="paper-table w-100" ng-if="tokens.length">
<div class="paper-table-head" role="row" style="grid-template-columns: 1fr 200px 200px 80px;">
<div role="columnheader">Name</div>
<div role="columnheader">Created</div>
<div role="columnheader">Last used</div>
<div role="columnheader" aria-label="Actions"></div>
</div>
<div class="paper-table-row" role="row" ng-repeat="t in tokens" style="grid-template-columns: 1fr 200px 200px 80px;">
<div role="cell" ng-bind="t.name"></div>
<div role="cell" ng-bind="t.createdAt | humanTime"></div>
<div role="cell"><span ng-if="t.lastUsedAt">{{t.lastUsedAt | humanTime}}</span><span ng-if="!t.lastUsedAt" class="text-muted">never</span></div>
<div role="cell">
<button class="btn btn-sm text-danger" ng-click="revokeToken(t)" title="Revoke"><i class="fas fa-trash-alt"></i></button>
</div>
</div>
</div>
<div class="paper-table-empty" ng-if="!tokens.length">
<i class="fas fa-inbox"></i>
<span>No tokens yet.</span>
</div>
</div>
<div class="admin-section-header">
<h2><i class="fas fa-code-branch"></i> Anonymized repositories</h2>
<span class="section-count">{{repositories.length}}</span>
</div>
<form class="w-100 dashboard-filter-row" aria-label="Repositories" accept-charset="UTF-8">
<div class="search-wrap">
<input
type="search"
class="form-control"
aria-label="Search repositories"
placeholder="Search repositories…"
autocomplete="off"
ng-model="search"
/>
</div>
<div class="d-flex flex-wrap" style="gap: 8px;">
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown">Sort</button>
<div class="dropdown-menu" aria-labelledby="dropdownSort">
<h6 class="dropdown-header">Sort by</h6>
<a class="dropdown-item" href="#" ng-click="orderBy = '-anonymizeDate'">
<i class="fas fa-check" ng-show="orderBy == '-anonymizeDate'"></i> Anonymize date
</a>
<a class="dropdown-item" href="#" ng-click="orderBy = 'repoId'">
<i class="fas fa-check" ng-show="orderBy == 'repoId'"></i> ID
</a>
<a class="dropdown-item" href="#" ng-click="orderBy = '-status'">
<i class="fas fa-check" ng-show="orderBy == '-status'"></i> Status
</a>
</div>
</div>
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown">Status</button>
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
<h6 class="dropdown-header">Filter by status</h6>
<div class="form-check dropdown-item">
<input class="form-check-input" type="checkbox" id="adminUserStatusReady" ng-model="filters.status.ready" />
<label class="form-check-label" for="adminUserStatusReady">Ready</label>
</div>
<div class="form-check dropdown-item">
<input class="form-check-input" type="checkbox" id="adminUserStatusExpired" ng-model="filters.status.expired" />
<label class="form-check-label" for="adminUserStatusExpired">Expired</label>
</div>
<div class="form-check dropdown-item">
<input class="form-check-input" type="checkbox" id="adminUserStatusRemoved" ng-model="filters.status.removed" />
<label class="form-check-label" for="adminUserStatusRemoved">Removed</label>
</div>
</div>
</div>
</div>
</form>
<div class="paper-table paper-table-repos w-100" role="table" aria-label="Repositories">
<div class="paper-table-head" role="row">
<div role="columnheader">Repository</div>
<div role="columnheader">Status</div>
<div role="columnheader" class="num">Views</div>
<div role="columnheader">Anonymized</div>
<div role="columnheader" aria-label="Actions"></div>
</div>
<div
class="paper-table-row"
role="row"
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'repo-error': repo.status == 'error'}"
ng-repeat="repo in repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories"
>
<div class="cell-anon" role="cell">
<span class="type-badge type-repo">Repo</span>
<div class="anon-text">
<a class="repo-name" ng-href="/r/{{repo.repoId}}" ng-bind="repo.repoId"></a>
<div class="anon-sub">
<a href="https://github.com/{{repo.source.fullName}}/" ng-bind="repo.source.fullName"></a><span ng-if="repo.options.update">&nbsp;&middot;&nbsp;<a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.branch}}" ng-bind="repo.source.branch"></a></span><span ng-if="!repo.options.update">&nbsp;&middot;&nbsp;@<a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.commit}}" ng-bind="repo.source.commit.substring(0, 8)"></a></span><span>&nbsp;&middot;&nbsp;{{::repo.size.storage | humanFileSize}}</span><span>&nbsp;&middot;&nbsp;{{::repo.options.terms.length | number}} terms</span>
</div>
</div>
</div>
<div class="cell-status" role="cell">
<span class="status-line">
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error'}"></span>
<span ng-bind="repo.status | title"></span>
</span>
<span class="status-sub status-sub-error" ng-if="repo.status == 'error' && repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage"></span>
</div>
<div class="cell-views num" role="cell" ng-bind="::repo.pageView | number"></div>
<div class="cell-expires" role="cell" ng-bind="repo.anonymizeDate | humanTime"></div>
<div class="cell-actions" role="cell">
<div class="dropdown">
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions">
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/anonymize/{{repo.repoId}}"><i class="far fa-edit"></i> Edit</a>
<a class="dropdown-item" href="/r/{{repo.repoId}}/"><i class="fa fa-eye"></i> View repo</a>
<a class="dropdown-item" href="/w/{{repo.repoId}}/" target="_self" ng-if="repo.options.page && repo.status == 'ready'"><i class="fas fa-globe"></i> View page</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" ng-show="repo.status == 'ready' || repo.status == 'error'" ng-click="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
<a class="dropdown-item" href="#" ng-show="repo.status == 'removed'" ng-click="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" ng-click="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a>
<a class="dropdown-item text-danger" href="#" ng-show="repo.status == 'ready'" ng-click="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
</div>
</div>
</div>
</div>
<div class="paper-table-empty" ng-if="filteredRepositories.length == 0">
<i class="fas fa-inbox"></i>
<span>No repositories to display.</span>
</div>
</div>
</div>