feat(auth): make upload review a separate grant instead of an admin privilege
Reviewing the upload queue is now a per-user flag (reviews_uploads), not part of the admin role: several people can hold it, a moderator can have it, and an admin may not. Only admins grant or revoke it; existing admins keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
066ba24834
commit
74da457362
27 files changed
+503
-319
No files matched your search
+14
-2
@@ -2176,7 +2176,7 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
Me:
|
Me:
|
||||||
type: object
|
type: object
|
||||||
required: [id, username, display_name, role, locale, ui_prefs, must_change_password, must_change_credentials, created_at]
|
required: [id, username, display_name, role, reviews_uploads, locale, ui_prefs, must_change_password, must_change_credentials, created_at]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
@@ -2188,6 +2188,9 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
role:
|
role:
|
||||||
$ref: "#/components/schemas/Role"
|
$ref: "#/components/schemas/Role"
|
||||||
|
reviews_uploads:
|
||||||
|
type: boolean
|
||||||
|
description: Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу
|
||||||
locale:
|
locale:
|
||||||
type: string
|
type: string
|
||||||
ui_prefs:
|
ui_prefs:
|
||||||
@@ -2228,7 +2231,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
AdminUser:
|
AdminUser:
|
||||||
type: object
|
type: object
|
||||||
required: [id, username, display_name, role, locale, is_blocked, must_change_password, must_change_credentials, created_at, last_login_at]
|
required: [id, username, display_name, role, reviews_uploads, locale, is_blocked, must_change_password, must_change_credentials, created_at, last_login_at]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
@@ -2240,6 +2243,9 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
role:
|
role:
|
||||||
$ref: "#/components/schemas/Role"
|
$ref: "#/components/schemas/Role"
|
||||||
|
reviews_uploads:
|
||||||
|
type: boolean
|
||||||
|
description: Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу
|
||||||
locale:
|
locale:
|
||||||
type: string
|
type: string
|
||||||
is_blocked:
|
is_blocked:
|
||||||
@@ -2277,6 +2283,9 @@ components:
|
|||||||
maxLength: 64
|
maxLength: 64
|
||||||
role:
|
role:
|
||||||
$ref: "#/components/schemas/Role"
|
$ref: "#/components/schemas/Role"
|
||||||
|
reviews_uploads:
|
||||||
|
type: boolean
|
||||||
|
description: Сразу выдать аудит загрузок. Может задать только админ
|
||||||
CreatedUser:
|
CreatedUser:
|
||||||
type: object
|
type: object
|
||||||
required: [user, temporary_password]
|
required: [user, temporary_password]
|
||||||
@@ -2297,6 +2306,9 @@ components:
|
|||||||
$ref: "#/components/schemas/Role"
|
$ref: "#/components/schemas/Role"
|
||||||
is_blocked:
|
is_blocked:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
reviews_uploads:
|
||||||
|
type: boolean
|
||||||
|
description: Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя
|
||||||
TemporaryPassword:
|
TemporaryPassword:
|
||||||
type: object
|
type: object
|
||||||
required: [temporary_password]
|
required: [temporary_password]
|
||||||
|
|||||||
@@ -226,7 +226,9 @@ const myUploadsRoute = createRoute({ getParentRoute: () => appRoute, path: '/upl
|
|||||||
const uploadQueueRoute = createRoute({
|
const uploadQueueRoute = createRoute({
|
||||||
getParentRoute: () => appRoute,
|
getParentRoute: () => appRoute,
|
||||||
path: '/admin/uploads',
|
path: '/admin/uploads',
|
||||||
beforeLoad: adminOnly,
|
beforeLoad: ({ context }) => {
|
||||||
|
if (!context.user.reviews_uploads) throw redirect({ to: '/' });
|
||||||
|
},
|
||||||
component: UploadQueuePage,
|
component: UploadQueuePage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,11 @@ export function UsersPage() {
|
|||||||
label: t('users.make_role', { role: t(`common:roles.${r}`) }),
|
label: t('users.make_role', { role: t(`common:roles.${r}`) }),
|
||||||
onSelect: () => patch.mutate({ id: u.id, body: { role: r } }),
|
onSelect: () => patch.mutate({ id: u.id, body: { role: r } }),
|
||||||
});
|
});
|
||||||
|
if (me.role === 'admin')
|
||||||
|
items.push({
|
||||||
|
label: t(u.reviews_uploads ? 'users.revoke_review' : 'users.grant_review'),
|
||||||
|
onSelect: () => patch.mutate({ id: u.id, body: { reviews_uploads: !u.reviews_uploads } }),
|
||||||
|
});
|
||||||
if (can('delete'))
|
if (can('delete'))
|
||||||
items.push({ label: t('users.delete'), danger: true, onSelect: () => setConfirm({ kind: 'delete', user: u }) });
|
items.push({ label: t('users.delete'), danger: true, onSelect: () => setConfirm({ kind: 'delete', user: u }) });
|
||||||
return items;
|
return items;
|
||||||
@@ -206,6 +211,7 @@ export function UsersPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={cx(s.badge, s[`role_${u.role}`])}>{t(`common:roles.${u.role}`)}</span>
|
<span className={cx(s.badge, s[`role_${u.role}`])}>{t(`common:roles.${u.role}`)}</span>
|
||||||
|
{u.reviews_uploads && <span className={cx(s.badge, s.role_moderator)}>{t('users.review_uploads')}</span>}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={cx(s.status, s[`status_${st}`])}>{t(`users.status_${st}`)}</span>
|
<span className={cx(s.status, s[`status_${st}`])}>{t(`users.status_${st}`)}</span>
|
||||||
@@ -285,12 +291,18 @@ function CreateUserDialog({
|
|||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [role, setRole] = useState<Role>('user');
|
const [role, setRole] = useState<Role>('user');
|
||||||
|
const [reviews, setReviews] = useState(false);
|
||||||
|
|
||||||
const create = useMutation({
|
const create = useMutation({
|
||||||
mutationFn: async () =>
|
mutationFn: async () =>
|
||||||
unwrap(
|
unwrap(
|
||||||
await session.client.POST('/api/v1/admin/users', {
|
await session.client.POST('/api/v1/admin/users', {
|
||||||
body: { username: username.trim(), display_name: name.trim() || undefined, role },
|
body: {
|
||||||
|
username: username.trim(),
|
||||||
|
display_name: name.trim() || undefined,
|
||||||
|
role,
|
||||||
|
reviews_uploads: actorRole === 'admin' && reviews ? true : undefined,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
onSuccess: (r) => onCreated(r.user.username, r.temporary_password),
|
onSuccess: (r) => onCreated(r.user.username, r.temporary_password),
|
||||||
@@ -342,6 +354,13 @@ function CreateUserDialog({
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
{actorRole === 'admin' && (
|
||||||
|
<label className={s.sessions} style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||||
|
<input type="checkbox" checked={reviews} onChange={(e) => setReviews(e.target.checked)} />
|
||||||
|
<span>{t('create.review_uploads')}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{actorRole === 'admin' && <p className={s.note}>{t('create.review_uploads_hint')}</p>}
|
||||||
<p className={s.note}>{t('create.password_note')}</p>
|
<p className={s.note}>{t('create.password_note')}</p>
|
||||||
<FormError>{create.error && !usernameError && !nameError ? errorText(create.error) : null}</FormError>
|
<FormError>{create.error && !usernameError && !nameError ? errorText(create.error) : null}</FormError>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export function makeUser(username: string, role: Role, extra: Partial<FakeUser>
|
|||||||
is_blocked: false,
|
is_blocked: false,
|
||||||
must_change_password: false,
|
must_change_password: false,
|
||||||
must_change_credentials: false,
|
must_change_credentials: false,
|
||||||
|
reviews_uploads: false,
|
||||||
created_at: '2026-01-01T00:00:00Z',
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
last_login_at: null,
|
last_login_at: null,
|
||||||
password: `${username}-password`,
|
password: `${username}-password`,
|
||||||
@@ -80,6 +81,7 @@ export function fakeApi(initial: FakeUser[]) {
|
|||||||
username: u.username,
|
username: u.username,
|
||||||
display_name: u.display_name,
|
display_name: u.display_name,
|
||||||
role: u.role,
|
role: u.role,
|
||||||
|
reviews_uploads: u.reviews_uploads,
|
||||||
locale: u.locale,
|
locale: u.locale,
|
||||||
ui_prefs: prefs.get(u.id) ?? {},
|
ui_prefs: prefs.get(u.id) ?? {},
|
||||||
must_change_password: u.must_change_password,
|
must_change_password: u.must_change_password,
|
||||||
@@ -179,6 +181,7 @@ export function fakeApi(initial: FakeUser[]) {
|
|||||||
display_name: (body?.display_name as string) ?? null,
|
display_name: (body?.display_name as string) ?? null,
|
||||||
must_change_password: true,
|
must_change_password: true,
|
||||||
password: 'Temp-Pass-123',
|
password: 'Temp-Pass-123',
|
||||||
|
reviews_uploads: body?.reviews_uploads === true && user.role === 'admin',
|
||||||
});
|
});
|
||||||
users.set(u.id, u);
|
users.set(u.id, u);
|
||||||
return json({ user: admin(u), temporary_password: u.password }, 201);
|
return json({ user: admin(u), temporary_password: u.password }, 201);
|
||||||
@@ -235,6 +238,20 @@ export function fakeApi(initial: FakeUser[]) {
|
|||||||
}
|
}
|
||||||
if (req.method === 'POST' && /^\/api\/v1\/admin\/users\/[^/]+\/reset-recommendations$/.test(path))
|
if (req.method === 'POST' && /^\/api\/v1\/admin\/users\/[^/]+\/reset-recommendations$/.test(path))
|
||||||
return new Response(null, { status: 204 });
|
return new Response(null, { status: 204 });
|
||||||
|
const userPath = /^\/api\/v1\/admin\/users\/([^/]+)$/.exec(path);
|
||||||
|
if (userPath && req.method === 'PATCH') {
|
||||||
|
if (!user || user.role === 'user') return problem(403, 'auth.forbidden');
|
||||||
|
const target = users.get(userPath[1]!);
|
||||||
|
if (!target) return problem(404, 'common.not_found');
|
||||||
|
if (typeof body?.reviews_uploads === 'boolean') {
|
||||||
|
if (user.role !== 'admin') return problem(403, 'auth.forbidden');
|
||||||
|
target.reviews_uploads = body.reviews_uploads;
|
||||||
|
}
|
||||||
|
if (typeof body?.role === 'string' && user.role === 'admin') target.role = body.role as Role;
|
||||||
|
if (typeof body?.is_blocked === 'boolean') target.is_blocked = body.is_blocked;
|
||||||
|
users.set(target.id, target);
|
||||||
|
return json(admin(target));
|
||||||
|
}
|
||||||
const reactionPath = /^\/api\/v1\/me\/reactions\/tracks\/([^/]+)$/.exec(path);
|
const reactionPath = /^\/api\/v1\/me\/reactions\/tracks\/([^/]+)$/.exec(path);
|
||||||
if (reactionPath) {
|
if (reactionPath) {
|
||||||
if (req.method === 'PUT') reactions.set(reactionPath[1]!, body?.reaction as 'like' | 'dislike');
|
if (req.method === 'PUT') reactions.set(reactionPath[1]!, body?.reaction as 'like' | 'dislike');
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ function Shell() {
|
|||||||
const searchAt = user.ui_prefs.search_position ?? 'center';
|
const searchAt = user.ui_prefs.search_position ?? 'center';
|
||||||
const search = <SearchBox align={searchAt} collapsible={user.ui_prefs.search_collapse === true} />;
|
const search = <SearchBox align={searchAt} collapsible={user.ui_prefs.search_collapse === true} />;
|
||||||
const counts = useCollectionCounts();
|
const counts = useCollectionCounts();
|
||||||
const pending = usePendingUploads(user.role === 'admin');
|
const reviews = user.reviews_uploads;
|
||||||
|
const pending = usePendingUploads(reviews);
|
||||||
const unseen = useUnseenUploads(staff);
|
const unseen = useUnseenUploads(staff);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -207,8 +208,17 @@ function Shell() {
|
|||||||
count={counts.dislikes}
|
count={counts.dislikes}
|
||||||
/>
|
/>
|
||||||
</NavSection>
|
</NavSection>
|
||||||
|
{!staff && reviews && (
|
||||||
|
<NavLink
|
||||||
|
to="/admin/uploads"
|
||||||
|
label={t('nav.upload_queue')}
|
||||||
|
icon={inboxIcon}
|
||||||
|
activeIcon={inboxFillIcon}
|
||||||
|
badge={pending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{staff && (
|
{staff && (
|
||||||
<NavSection id="admin" title={t('nav.admin')} badge={pending} defaultFolded>
|
<NavSection id="admin" title={t('nav.admin')} badge={reviews ? pending : undefined} defaultFolded>
|
||||||
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
|
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
|
||||||
{user.role === 'admin' && (
|
{user.role === 'admin' && (
|
||||||
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
||||||
@@ -216,15 +226,17 @@ function Shell() {
|
|||||||
<NavLink to="/admin/library" label={t('nav.library')} icon={libraryIcon} activeIcon={libraryFillIcon} />
|
<NavLink to="/admin/library" label={t('nav.library')} icon={libraryIcon} activeIcon={libraryFillIcon} />
|
||||||
<NavLink to="/uploads/new" label={t('nav.suggest')} icon={uploadIcon} activeIcon={uploadFillIcon} />
|
<NavLink to="/uploads/new" label={t('nav.suggest')} icon={uploadIcon} activeIcon={uploadFillIcon} />
|
||||||
<NavLink to="/uploads/mine" label={t('nav.my_uploads')} icon={myUploadsIcon} activeIcon={myUploadsFillIcon} badge={unseen} />
|
<NavLink to="/uploads/mine" label={t('nav.my_uploads')} icon={myUploadsIcon} activeIcon={myUploadsFillIcon} badge={unseen} />
|
||||||
|
{reviews && (
|
||||||
|
<NavLink
|
||||||
|
to="/admin/uploads"
|
||||||
|
label={t('nav.upload_queue')}
|
||||||
|
icon={inboxIcon}
|
||||||
|
activeIcon={inboxFillIcon}
|
||||||
|
badge={pending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{user.role === 'admin' && (
|
{user.role === 'admin' && (
|
||||||
<>
|
<>
|
||||||
<NavLink
|
|
||||||
to="/admin/uploads"
|
|
||||||
label={t('nav.upload_queue')}
|
|
||||||
icon={inboxIcon}
|
|
||||||
activeIcon={inboxFillIcon}
|
|
||||||
badge={pending}
|
|
||||||
/>
|
|
||||||
<NavLink to="/admin/settings" label={t('nav.admin_settings')} icon={tuneIcon} activeIcon={tuneFillIcon} />
|
<NavLink to="/admin/settings" label={t('nav.admin_settings')} icon={tuneIcon} activeIcon={tuneFillIcon} />
|
||||||
<NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} />
|
<NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
+9
-3
@@ -20,7 +20,13 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Пользователь | Слушать, искать, собирать коллекцию, «Моя волна» |
|
| Пользователь | Слушать, искать, собирать коллекцию, «Моя волна» |
|
||||||
| Модератор | Плюс: заводить пользователей и сбрасывать им пароли, смотреть статус библиотеки, предлагать треки |
|
| Модератор | Плюс: заводить пользователей и сбрасывать им пароли, смотреть статус библиотеки, предлагать треки |
|
||||||
| Администратор | Всё: хранилища, сканы, очередь на добавление, настройки сервера, роли, аудит |
|
| Администратор | Всё: хранилища, сканы, настройки сервера, роли, аудит |
|
||||||
|
|
||||||
|
**Аудит загрузок** — отдельное право, не роль: рассматривать очередь на добавление (одобрять, отклонять, править
|
||||||
|
метаданные). Выдаёт и забирает только администратор — при создании пользователя или в меню строки
|
||||||
|
(«Дать / Забрать аудит загрузок»), в том числе себе. Право может быть у нескольких людей сразу и у любой роли;
|
||||||
|
у администратора его может не быть. Первый (bootstrap) администратор и админы, существовавшие до появления права,
|
||||||
|
получают его автоматически.
|
||||||
|
|
||||||
Полная матрица прав — [auth-and-roles.md](auth-and-roles.md#матрица-прав).
|
Полная матрица прав — [auth-and-roles.md](auth-and-roles.md#матрица-прав).
|
||||||
|
|
||||||
@@ -57,8 +63,8 @@
|
|||||||
|
|
||||||
## 4. Очередь на добавление
|
## 4. Очередь на добавление
|
||||||
|
|
||||||
Модераторы и администраторы предлагают треки («Предложить треки»), администратор решает, что попадёт
|
Модераторы и администраторы предлагают треки («Предложить треки»), что попадёт в библиотеку, решают люди
|
||||||
в библиотеку. Подробно — [uploads.md](uploads.md).
|
с правом **аудита загрузок**. Подробно — [uploads.md](uploads.md).
|
||||||
|
|
||||||
- **Куда пишутся файлы:** у хранилища включите «Принимать загрузки» и задайте **шаблон раскладки** — «Подобрать»
|
- **Куда пишутся файлы:** у хранилища включите «Принимать загрузки» и задайте **шаблон раскладки** — «Подобрать»
|
||||||
предложит шаблон по уже лежащим файлам, «Проверить» убедится, что из пути, построенного шаблоном, сканер
|
предложит шаблон по уже лежащим файлам, «Проверить» убедится, что из пути, построенного шаблоном, сканер
|
||||||
|
|||||||
@@ -24,9 +24,13 @@
|
|||||||
| Хранилища, сканирование, переиндексация | — | — | ✅ |
|
| Хранилища, сканирование, переиндексация | — | — | ✅ |
|
||||||
| Статус библиотеки (только просмотр) | — | ✅ | ✅ |
|
| Статус библиотеки (только просмотр) | — | ✅ | ✅ |
|
||||||
| Предлагать треки (загрузка в очередь), видеть свои предложения | — | ✅ | ✅ |
|
| Предлагать треки (загрузка в очередь), видеть свои предложения | — | ✅ | ✅ |
|
||||||
| Рассматривать очередь, одобрять / отклонять, править метаданные | — | — | ✅ |
|
|
||||||
| Лимит очереди, целевое хранилище, шаблон раскладки | — | — | ✅ |
|
| Лимит очереди, целевое хранилище, шаблон раскладки | — | — | ✅ |
|
||||||
| Статус рекомендательной системы, аудит | — | — | ✅ |
|
| Статус рекомендательной системы, аудит | — | — | ✅ |
|
||||||
|
| Выдать / забрать аудит загрузок | — | — | ✅ |
|
||||||
|
|
||||||
|
**Аудит загрузок** (рассматривать очередь, одобрять / отклонять, править метаданные) — не строка матрицы, а
|
||||||
|
отдельный флаг пользователя `reviews_uploads`, не зависящий от роли: его может иметь кто угодно, сразу несколько
|
||||||
|
человек, а у админа его может не быть. Проверка — `Principal.Allowed(PermReviewUploads)`.
|
||||||
|
|
||||||
Реализация: в Go — политики в `internal/auth/policy.go` вида `CanManageUser(actor, target, action) bool`,
|
Реализация: в Go — политики в `internal/auth/policy.go` вида `CanManageUser(actor, target, action) bool`,
|
||||||
проверяются в сервисном слое (не только в middleware), покрываются табличными тестами по этой матрице.
|
проверяются в сервисном слое (не только в middleware), покрываются табличными тестами по этой матрице.
|
||||||
|
|||||||
+8
-5
@@ -1,7 +1,7 @@
|
|||||||
# Предложение треков (загрузка с модерацией)
|
# Предложение треков (загрузка с модерацией)
|
||||||
|
|
||||||
Модераторы (и админы) предлагают новые треки. Файлы попадают в **очередь на добавление** с общим лимитом размера,
|
Модераторы (и админы) предлагают новые треки. Файлы попадают в **очередь на добавление** с общим лимитом размера,
|
||||||
дубли отбрасываются сразу, админ одобряет или отклоняет, одобренные файлы раскладываются в хранилище
|
дубли отбрасываются сразу, аудитор загрузок одобряет или отклоняет, одобренные файлы раскладываются в хранилище
|
||||||
**по той же структуре папок, что принята в этом хранилище**.
|
**по той же структуре папок, что принята в этом хранилище**.
|
||||||
|
|
||||||
## 1. Роли
|
## 1. Роли
|
||||||
@@ -10,10 +10,13 @@
|
|||||||
|---|:-:|:-:|:-:|
|
|---|:-:|:-:|:-:|
|
||||||
| Загрузить треки в очередь | — | ✅ | ✅ |
|
| Загрузить треки в очередь | — | ✅ | ✅ |
|
||||||
| Видеть свои предложения и их статус, отозвать до рассмотрения | — | ✅ | ✅ |
|
| Видеть свои предложения и их статус, отозвать до рассмотрения | — | ✅ | ✅ |
|
||||||
| Видеть всю очередь | — | — | ✅ |
|
|
||||||
| Править метаданные, слушать, одобрять / отклонять | — | — | ✅ |
|
|
||||||
| Настраивать лимит очереди, целевое хранилище и шаблон раскладки | — | — | ✅ |
|
| Настраивать лимит очереди, целевое хранилище и шаблон раскладки | — | — | ✅ |
|
||||||
|
|
||||||
|
Видеть всю очередь, слушать, править метаданные, одобрять и отклонять может только тот, у кого есть право
|
||||||
|
**аудита загрузок** (`users.reviews_uploads`, в API — `reviews_uploads` в `Me` / `AdminUser`). Это не роль: право
|
||||||
|
выдаёт админ, оно может быть у нескольких людей сразу, у модератора или даже у обычного пользователя, а у админа
|
||||||
|
его может не быть. Без него админ по-прежнему настраивает лимиты и хранилища, но очередь не видит.
|
||||||
|
|
||||||
Загрузки админа проходят ту же проверку на дубли; в очередь они тоже попадают, но админ может одобрить их сразу.
|
Загрузки админа проходят ту же проверку на дубли; в очередь они тоже попадают, но админ может одобрить их сразу.
|
||||||
|
|
||||||
## 2. Жизненный цикл загрузки
|
## 2. Жизненный цикл загрузки
|
||||||
@@ -142,7 +145,7 @@
|
|||||||
- Если файл по целевому пути уже существует — добавляется суффикс ` (2)`, а в задаче — предупреждение.
|
- Если файл по целевому пути уже существует — добавляется суффикс ` (2)`, а в задаче — предупреждение.
|
||||||
- Путь проверяется на выход за корень хранилища (`..`, абсолютные пути) — ошибка.
|
- Путь проверяется на выход за корень хранилища (`..`, абсолютные пути) — ошибка.
|
||||||
|
|
||||||
## 8. Рассмотрение (админ)
|
## 8. Рассмотрение (аудит загрузок)
|
||||||
|
|
||||||
Экран «Очередь на добавление»:
|
Экран «Очередь на добавление»:
|
||||||
- Группировка по пачке загрузки / по альбому; фильтры: статус, автор, `possible_duplicate`.
|
- Группировка по пачке загрузки / по альбому; фильтры: статус, автор, `possible_duplicate`.
|
||||||
@@ -173,7 +176,7 @@
|
|||||||
|
|
||||||
## 10. Уведомления
|
## 10. Уведомления
|
||||||
- Автор видит статусы в «Мои предложения» (раздел админки модератора), бейдж с числом решений с последнего визита.
|
- Автор видит статусы в «Мои предложения» (раздел админки модератора), бейдж с числом решений с последнего визита.
|
||||||
- Админ видит бейдж «N на рассмотрении» в сайдбаре.
|
- Аудитор загрузок видит бейдж «N на рассмотрении» в сайдбаре.
|
||||||
|
|
||||||
## Реализация (`services/api/internal/uploads`)
|
## Реализация (`services/api/internal/uploads`)
|
||||||
|
|
||||||
|
|||||||
@@ -1648,6 +1648,8 @@ export interface components {
|
|||||||
username: string;
|
username: string;
|
||||||
display_name: string | null;
|
display_name: string | null;
|
||||||
role: components["schemas"]["Role"];
|
role: components["schemas"]["Role"];
|
||||||
|
/** @description Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу */
|
||||||
|
reviews_uploads: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
ui_prefs: components["schemas"]["UiPrefs"];
|
ui_prefs: components["schemas"]["UiPrefs"];
|
||||||
must_change_password: boolean;
|
must_change_password: boolean;
|
||||||
@@ -1675,6 +1677,8 @@ export interface components {
|
|||||||
username: string;
|
username: string;
|
||||||
display_name: string | null;
|
display_name: string | null;
|
||||||
role: components["schemas"]["Role"];
|
role: components["schemas"]["Role"];
|
||||||
|
/** @description Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу */
|
||||||
|
reviews_uploads: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
is_blocked: boolean;
|
is_blocked: boolean;
|
||||||
must_change_password: boolean;
|
must_change_password: boolean;
|
||||||
@@ -1692,6 +1696,8 @@ export interface components {
|
|||||||
username: string;
|
username: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
role: components["schemas"]["Role"];
|
role: components["schemas"]["Role"];
|
||||||
|
/** @description Сразу выдать аудит загрузок. Может задать только админ */
|
||||||
|
reviews_uploads?: boolean;
|
||||||
};
|
};
|
||||||
CreatedUser: {
|
CreatedUser: {
|
||||||
user: components["schemas"]["AdminUser"];
|
user: components["schemas"]["AdminUser"];
|
||||||
@@ -1703,6 +1709,8 @@ export interface components {
|
|||||||
display_name?: string;
|
display_name?: string;
|
||||||
role?: components["schemas"]["Role"];
|
role?: components["schemas"]["Role"];
|
||||||
is_blocked?: boolean;
|
is_blocked?: boolean;
|
||||||
|
/** @description Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя */
|
||||||
|
reviews_uploads?: boolean;
|
||||||
};
|
};
|
||||||
TemporaryPassword: {
|
TemporaryPassword: {
|
||||||
temporary_password: string;
|
temporary_password: string;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const user = {
|
|||||||
ui_prefs: {},
|
ui_prefs: {},
|
||||||
must_change_password: false,
|
must_change_password: false,
|
||||||
must_change_credentials: false,
|
must_change_credentials: false,
|
||||||
|
reviews_uploads: false,
|
||||||
created_at: '2026-01-01T00:00:00Z',
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"users": {
|
"users": {
|
||||||
"title": "Users",
|
"title": "Users",
|
||||||
"subtitle": "Create users and moderators, manage roles, reset passwords, block accounts",
|
"subtitle": "Create users and moderators, manage roles, upload review, reset passwords, block accounts",
|
||||||
"create": "Create user",
|
"create": "Create user",
|
||||||
"search": "Name or username",
|
"search": "Name or username",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"block": "Block",
|
"block": "Block",
|
||||||
"unblock": "Unblock",
|
"unblock": "Unblock",
|
||||||
"make_role": "Make {{role}}",
|
"make_role": "Make {{role}}",
|
||||||
|
"review_uploads": "Upload review",
|
||||||
|
"grant_review": "Grant upload review",
|
||||||
|
"revoke_review": "Revoke upload review",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_confirm": "Delete {{username}}? Their history and likes will be lost.",
|
"delete_confirm": "Delete {{username}}? Their history and likes will be lost.",
|
||||||
"block_confirm": "Block {{username}}? All their sessions will end.",
|
"block_confirm": "Block {{username}}? All their sessions will end.",
|
||||||
@@ -36,6 +39,8 @@
|
|||||||
"username_hint": "Latin letters, digits, \".\", \"_\", \"-\"",
|
"username_hint": "Latin letters, digits, \".\", \"_\", \"-\"",
|
||||||
"display_name": "Name",
|
"display_name": "Name",
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
|
"review_uploads": "Upload review",
|
||||||
|
"review_uploads_hint": "They can review the upload queue. This is not a role: several people can have it, and an admin might not.",
|
||||||
"password_note": "The server will generate a password and show it once.",
|
"password_note": "The server will generate a password and show it once.",
|
||||||
"submit": "Create"
|
"submit": "Create"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"users": {
|
"users": {
|
||||||
"title": "Пользователи",
|
"title": "Пользователи",
|
||||||
"subtitle": "Создание пользователей и модераторов, роли, сброс пароля, блокировка",
|
"subtitle": "Создание пользователей и модераторов, роли, аудит загрузок, сброс пароля, блокировка",
|
||||||
"create": "Создать пользователя",
|
"create": "Создать пользователя",
|
||||||
"search": "Имя или логин",
|
"search": "Имя или логин",
|
||||||
"all": "Все",
|
"all": "Все",
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"block": "Заблокировать",
|
"block": "Заблокировать",
|
||||||
"unblock": "Разблокировать",
|
"unblock": "Разблокировать",
|
||||||
"make_role": "Сделать: {{role}}",
|
"make_role": "Сделать: {{role}}",
|
||||||
|
"review_uploads": "Аудит загрузок",
|
||||||
|
"grant_review": "Дать аудит загрузок",
|
||||||
|
"revoke_review": "Забрать аудит загрузок",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"delete_confirm": "Удалить {{username}}? Его история и лайки пропадут.",
|
"delete_confirm": "Удалить {{username}}? Его история и лайки пропадут.",
|
||||||
"block_confirm": "Заблокировать {{username}}? Все его сессии завершатся.",
|
"block_confirm": "Заблокировать {{username}}? Все его сессии завершатся.",
|
||||||
@@ -36,6 +39,8 @@
|
|||||||
"username_hint": "Латинские буквы, цифры, «.», «_», «-»",
|
"username_hint": "Латинские буквы, цифры, «.», «_», «-»",
|
||||||
"display_name": "Имя",
|
"display_name": "Имя",
|
||||||
"role": "Роль",
|
"role": "Роль",
|
||||||
|
"review_uploads": "Аудит загрузок",
|
||||||
|
"review_uploads_hint": "Сможет рассматривать очередь. Это не роль: можно выдать нескольким людям, у админа её может не быть.",
|
||||||
"password_note": "Пароль сгенерирует сервер и покажет один раз.",
|
"password_note": "Пароль сгенерирует сервер и покажет один раз.",
|
||||||
"submit": "Создать"
|
"submit": "Создать"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -241,8 +241,11 @@ type AdminUser struct {
|
|||||||
Locale string `json:"locale"`
|
Locale string `json:"locale"`
|
||||||
MustChangeCredentials bool `json:"must_change_credentials"`
|
MustChangeCredentials bool `json:"must_change_credentials"`
|
||||||
MustChangePassword bool `json:"must_change_password"`
|
MustChangePassword bool `json:"must_change_password"`
|
||||||
Role Role `json:"role"`
|
|
||||||
Username string `json:"username"`
|
// ReviewsUploads Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу
|
||||||
|
ReviewsUploads bool `json:"reviews_uploads"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminUserPage defines model for AdminUserPage.
|
// AdminUserPage defines model for AdminUserPage.
|
||||||
@@ -388,8 +391,11 @@ type ClientType string
|
|||||||
// CreateUserRequest defines model for CreateUserRequest.
|
// CreateUserRequest defines model for CreateUserRequest.
|
||||||
type CreateUserRequest struct {
|
type CreateUserRequest struct {
|
||||||
DisplayName *string `json:"display_name,omitempty"`
|
DisplayName *string `json:"display_name,omitempty"`
|
||||||
Role Role `json:"role"`
|
|
||||||
Username string `json:"username"`
|
// ReviewsUploads Сразу выдать аудит загрузок. Может задать только админ
|
||||||
|
ReviewsUploads *bool `json:"reviews_uploads,omitempty"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatedUser defines model for CreatedUser.
|
// CreatedUser defines model for CreatedUser.
|
||||||
@@ -493,9 +499,12 @@ type Me struct {
|
|||||||
Locale string `json:"locale"`
|
Locale string `json:"locale"`
|
||||||
MustChangeCredentials bool `json:"must_change_credentials"`
|
MustChangeCredentials bool `json:"must_change_credentials"`
|
||||||
MustChangePassword bool `json:"must_change_password"`
|
MustChangePassword bool `json:"must_change_password"`
|
||||||
Role Role `json:"role"`
|
|
||||||
UiPrefs UiPrefs `json:"ui_prefs"`
|
// ReviewsUploads Может рассматривать очередь на добавление. Не следует из роли и может быть у нескольких людей сразу
|
||||||
Username string `json:"username"`
|
ReviewsUploads bool `json:"reviews_uploads"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
UiPrefs UiPrefs `json:"ui_prefs"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Onboarding defines model for Onboarding.
|
// Onboarding defines model for Onboarding.
|
||||||
@@ -962,7 +971,10 @@ type UpdateUserRequest struct {
|
|||||||
// DisplayName Пустая строка — сбросить имя
|
// DisplayName Пустая строка — сбросить имя
|
||||||
DisplayName *string `json:"display_name,omitempty"`
|
DisplayName *string `json:"display_name,omitempty"`
|
||||||
IsBlocked *bool `json:"is_blocked,omitempty"`
|
IsBlocked *bool `json:"is_blocked,omitempty"`
|
||||||
Role *Role `json:"role,omitempty"`
|
|
||||||
|
// ReviewsUploads Выдать или забрать аудит загрузок. Может менять только админ, в том числе у себя
|
||||||
|
ReviewsUploads *bool `json:"reviews_uploads,omitempty"`
|
||||||
|
Role *Role `json:"role,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadBatchCreated defines model for UploadBatchCreated.
|
// UploadBatchCreated defines model for UploadBatchCreated.
|
||||||
@@ -9789,265 +9801,268 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"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",
|
||||||
|
"AQA=",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ func (s *Service) ListUsers(ctx context.Context, p Principal, query string, role
|
|||||||
|
|
||||||
// CreateUserInput — форма создания пользователя.
|
// CreateUserInput — форма создания пользователя.
|
||||||
type CreateUserInput struct {
|
type CreateUserInput struct {
|
||||||
Username string
|
Username string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
Role Role
|
Role Role
|
||||||
|
ReviewsUploads bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser создаёт пользователя с временным паролем, который возвращается один раз.
|
// CreateUser создаёт пользователя с временным паролем, который возвращается один раз.
|
||||||
@@ -88,6 +89,9 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu
|
|||||||
if !CanAssignRole(p.Role, in.Role) || !CanManageUser(actorOf(p), Actor{Role: in.Role}, UserCreate) {
|
if !CanAssignRole(p.Role, in.Role) || !CanManageUser(actorOf(p), Actor{Role: in.Role}, UserCreate) {
|
||||||
return User{}, "", forbidden()
|
return User{}, "", forbidden()
|
||||||
}
|
}
|
||||||
|
if in.ReviewsUploads && p.Role != RoleAdmin {
|
||||||
|
return User{}, "", forbidden()
|
||||||
|
}
|
||||||
username := NormalizeUsername(in.Username)
|
username := NormalizeUsername(in.Username)
|
||||||
if err := ValidateUsername("username", username); err != nil {
|
if err := ValidateUsername("username", username); err != nil {
|
||||||
return User{}, "", err
|
return User{}, "", err
|
||||||
@@ -109,7 +113,8 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu
|
|||||||
locale = s.cfg.SupportedLocales[0]
|
locale = s.cfg.SupportedLocales[0]
|
||||||
}
|
}
|
||||||
u := User{
|
u := User{
|
||||||
ID: s.ids.New(), Username: username, DisplayName: name, PasswordHash: hash, Role: in.Role, Locale: locale,
|
ID: s.ids.New(), Username: username, DisplayName: name, PasswordHash: hash, Role: in.Role,
|
||||||
|
ReviewsUploads: in.ReviewsUploads, Locale: locale,
|
||||||
MustChangePassword: true, CreatedAt: s.clock.Now(),
|
MustChangePassword: true, CreatedAt: s.clock.Now(),
|
||||||
}
|
}
|
||||||
err = s.store.WithTx(ctx, func(tx Store) error {
|
err = s.store.WithTx(ctx, func(tx Store) error {
|
||||||
@@ -118,7 +123,7 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu
|
|||||||
}
|
}
|
||||||
return tx.InsertAudit(ctx, s.ids.New(), AuditEntry{
|
return tx.InsertAudit(ctx, s.ids.New(), AuditEntry{
|
||||||
ActorID: &p.UserID, Action: "user.create", TargetType: "user", TargetID: &u.ID,
|
ActorID: &p.UserID, Action: "user.create", TargetType: "user", TargetID: &u.ID,
|
||||||
Payload: map[string]any{"username": username, "role": string(in.Role)},
|
Payload: map[string]any{"username": username, "role": string(in.Role), "reviews_uploads": in.ReviewsUploads},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if errors.Is(err, ErrUsernameTaken) {
|
if errors.Is(err, ErrUsernameTaken) {
|
||||||
@@ -145,9 +150,10 @@ func displayName(s string) (*string, error) {
|
|||||||
|
|
||||||
// UserPatch — изменения пользователя из админки. nil — не менять.
|
// UserPatch — изменения пользователя из админки. nil — не менять.
|
||||||
type UserPatch struct {
|
type UserPatch struct {
|
||||||
DisplayName *string
|
DisplayName *string
|
||||||
Role *Role
|
Role *Role
|
||||||
IsBlocked *bool
|
IsBlocked *bool
|
||||||
|
ReviewsUploads *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser меняет имя, роль и блокировку с проверкой прав по каждому изменению.
|
// UpdateUser меняет имя, роль и блокировку с проверкой прав по каждому изменению.
|
||||||
@@ -191,6 +197,13 @@ func (s *Service) UpdateUser(ctx context.Context, p Principal, id uuid.UUID, pat
|
|||||||
upd.Role, upd.RevokeTokens = patch.Role, true
|
upd.Role, upd.RevokeTokens = patch.Role, true
|
||||||
audit("user.role_change", map[string]any{"from": string(target.Role), "to": string(*patch.Role)})
|
audit("user.role_change", map[string]any{"from": string(target.Role), "to": string(*patch.Role)})
|
||||||
}
|
}
|
||||||
|
if patch.ReviewsUploads != nil && *patch.ReviewsUploads != target.ReviewsUploads {
|
||||||
|
if p.Role != RoleAdmin {
|
||||||
|
return forbidden()
|
||||||
|
}
|
||||||
|
upd.ReviewsUploads = patch.ReviewsUploads
|
||||||
|
audit("user.reviews_uploads", map[string]any{"reviews_uploads": *patch.ReviewsUploads})
|
||||||
|
}
|
||||||
blockChange := patch.IsBlocked != nil && *patch.IsBlocked != target.IsBlocked
|
blockChange := patch.IsBlocked != nil && *patch.IsBlocked != target.IsBlocked
|
||||||
if blockChange {
|
if blockChange {
|
||||||
if !CanManageUser(actor, tgt, UserBlock) {
|
if !CanManageUser(actor, tgt, UserBlock) {
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ func (h *Handler) CreateUser(ctx context.Context, req gen.CreateUserRequestObjec
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
in := CreateUserInput{Username: req.Body.Username, Role: Role(req.Body.Role)}
|
in := CreateUserInput{Username: req.Body.Username, Role: Role(req.Body.Role)}
|
||||||
|
if req.Body.ReviewsUploads != nil {
|
||||||
|
in.ReviewsUploads = *req.Body.ReviewsUploads
|
||||||
|
}
|
||||||
if req.Body.DisplayName != nil {
|
if req.Body.DisplayName != nil {
|
||||||
in.DisplayName = *req.Body.DisplayName
|
in.DisplayName = *req.Body.DisplayName
|
||||||
}
|
}
|
||||||
@@ -60,7 +63,7 @@ func (h *Handler) UpdateUser(ctx context.Context, req gen.UpdateUserRequestObjec
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
patch := UserPatch{DisplayName: req.Body.DisplayName, IsBlocked: req.Body.IsBlocked}
|
patch := UserPatch{DisplayName: req.Body.DisplayName, IsBlocked: req.Body.IsBlocked, ReviewsUploads: req.Body.ReviewsUploads}
|
||||||
if req.Body.Role != nil {
|
if req.Body.Role != nil {
|
||||||
r := Role(*req.Body.Role)
|
r := Role(*req.Body.Role)
|
||||||
patch.Role = &r
|
patch.Role = &r
|
||||||
@@ -147,6 +150,7 @@ func AdminUserDTO(u User) gen.AdminUser {
|
|||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
Role: gen.Role(u.Role),
|
Role: gen.Role(u.Role),
|
||||||
|
ReviewsUploads: u.ReviewsUploads,
|
||||||
Locale: u.Locale,
|
Locale: u.Locale,
|
||||||
IsBlocked: u.IsBlocked,
|
IsBlocked: u.IsBlocked,
|
||||||
MustChangePassword: u.MustChangePassword,
|
MustChangePassword: u.MustChangePassword,
|
||||||
|
|||||||
@@ -395,3 +395,28 @@ func TestAudit(t *testing.T) {
|
|||||||
mod := e.signIn("mod", "moderator-pass-1")
|
mod := e.signIn("mod", "moderator-pass-1")
|
||||||
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodGet, "/api/v1/admin/audit", nil, bearer(mod)).code())
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodGet, "/api/v1/admin/audit", nil, bearer(mod)).code())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Аудит загрузок выдаёт только админ, и он не следует из роли.
|
||||||
|
func TestReviewUploadsGrant(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
admin := e.signIn("root", "root-password-1")
|
||||||
|
target := e.addUser("mod", "mod-password-1", auth.RoleModerator)
|
||||||
|
mod := e.signIn("mod", "mod-password-1")
|
||||||
|
path := "/api/v1/admin/users/" + target.ID.String()
|
||||||
|
grant := map[string]any{"reviews_uploads": true}
|
||||||
|
|
||||||
|
me := e.do(http.MethodGet, "/api/v1/me", nil, bearer(admin))
|
||||||
|
assert.Equal(t, false, me.body["reviews_uploads"])
|
||||||
|
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, path, grant, bearer(mod)).code())
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPost, "/api/v1/admin/users",
|
||||||
|
map[string]any{"username": "rev", "role": "user", "reviews_uploads": true}, bearer(mod)).code())
|
||||||
|
|
||||||
|
r := e.do(http.MethodPatch, path, grant, bearer(admin))
|
||||||
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
||||||
|
assert.Equal(t, true, r.body["reviews_uploads"])
|
||||||
|
assert.Equal(t, "moderator", r.body["role"])
|
||||||
|
me = e.do(http.MethodGet, "/api/v1/me", nil, bearer(mod))
|
||||||
|
assert.Equal(t, true, me.body["reviews_uploads"])
|
||||||
|
}
|
||||||
@@ -303,6 +303,9 @@ func (s *MemStore) UpdateUser(ctx context.Context, id uuid.UUID, upd auth.UserUp
|
|||||||
if upd.IsBlocked != nil {
|
if upd.IsBlocked != nil {
|
||||||
u.IsBlocked = *upd.IsBlocked
|
u.IsBlocked = *upd.IsBlocked
|
||||||
}
|
}
|
||||||
|
if upd.ReviewsUploads != nil {
|
||||||
|
u.ReviewsUploads = *upd.ReviewsUploads
|
||||||
|
}
|
||||||
if upd.RevokeTokens {
|
if upd.RevokeTokens {
|
||||||
u.TokenVersion++
|
u.TokenVersion++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ func MeDTO(u User) gen.Me {
|
|||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
Role: gen.Role(u.Role),
|
Role: gen.Role(u.Role),
|
||||||
|
ReviewsUploads: u.ReviewsUploads,
|
||||||
Locale: u.Locale,
|
Locale: u.Locale,
|
||||||
UiPrefs: uiPrefsDTO(u.UIPrefs),
|
UiPrefs: uiPrefsDTO(u.UIPrefs),
|
||||||
MustChangePassword: u.MustChangePassword,
|
MustChangePassword: u.MustChangePassword,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type User struct {
|
|||||||
DisplayName *string
|
DisplayName *string
|
||||||
PasswordHash string
|
PasswordHash string
|
||||||
Role Role
|
Role Role
|
||||||
|
ReviewsUploads bool // аудит очереди на добавление; не следует из роли
|
||||||
Locale string
|
Locale string
|
||||||
UIPrefs UIPrefs
|
UIPrefs UIPrefs
|
||||||
MustChangePassword bool
|
MustChangePassword bool
|
||||||
@@ -94,6 +95,7 @@ type Session struct {
|
|||||||
type Principal struct {
|
type Principal struct {
|
||||||
UserID uuid.UUID
|
UserID uuid.UUID
|
||||||
Role Role
|
Role Role
|
||||||
|
ReviewsUploads bool
|
||||||
SessionID uuid.UUID
|
SessionID uuid.UUID
|
||||||
MustChangePassword bool
|
MustChangePassword bool
|
||||||
MustChangeCredentials bool
|
MustChangeCredentials bool
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ func (s *PgStore) WithTx(ctx context.Context, fn func(Store) error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userColumns = `id, username, display_name, password_hash, role, locale, ui_prefs, must_change_password,
|
const userColumns = `id, username, display_name, password_hash, role, locale, ui_prefs, must_change_password,
|
||||||
must_change_credentials, is_blocked, token_version, failed_login_count, locked_until, created_at, last_login_at`
|
must_change_credentials, is_blocked, token_version, failed_login_count, locked_until, created_at, last_login_at,
|
||||||
|
reviews_uploads`
|
||||||
|
|
||||||
func scanUser(row pgx.Row) (User, error) {
|
func scanUser(row pgx.Row) (User, error) {
|
||||||
var (
|
var (
|
||||||
@@ -49,7 +50,7 @@ func scanUser(row pgx.Row) (User, error) {
|
|||||||
)
|
)
|
||||||
err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &u.PasswordHash, &role, &u.Locale, &prefs,
|
err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &u.PasswordHash, &role, &u.Locale, &prefs,
|
||||||
&u.MustChangePassword, &u.MustChangeCredentials, &u.IsBlocked, &u.TokenVersion, &u.FailedLoginCount,
|
&u.MustChangePassword, &u.MustChangeCredentials, &u.IsBlocked, &u.TokenVersion, &u.FailedLoginCount,
|
||||||
&u.LockedUntil, &u.CreatedAt, &u.LastLoginAt)
|
&u.LockedUntil, &u.CreatedAt, &u.LastLoginAt, &u.ReviewsUploads)
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return User{}, ErrNotFound
|
return User{}, ErrNotFound
|
||||||
}
|
}
|
||||||
@@ -81,10 +82,10 @@ func (s *PgStore) CreateUser(ctx context.Context, u User, createdBy *uuid.UUID)
|
|||||||
}
|
}
|
||||||
_, err = s.q.Exec(ctx, `
|
_, err = s.q.Exec(ctx, `
|
||||||
INSERT INTO users (id, username, display_name, password_hash, role, locale, ui_prefs,
|
INSERT INTO users (id, username, display_name, password_hash, role, locale, ui_prefs,
|
||||||
must_change_password, must_change_credentials, created_by, created_at, updated_at)
|
must_change_password, must_change_credentials, created_by, created_at, updated_at, reviews_uploads)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11)`,
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11, $12)`,
|
||||||
u.ID, u.Username, u.DisplayName, u.PasswordHash, string(u.Role), u.Locale, prefs,
|
u.ID, u.Username, u.DisplayName, u.PasswordHash, string(u.Role), u.Locale, prefs,
|
||||||
u.MustChangePassword, u.MustChangeCredentials, createdBy, u.CreatedAt)
|
u.MustChangePassword, u.MustChangeCredentials, createdBy, u.CreatedAt, u.ReviewsUploads)
|
||||||
if isUniqueViolation(err) {
|
if isUniqueViolation(err) {
|
||||||
return ErrUsernameTaken
|
return ErrUsernameTaken
|
||||||
}
|
}
|
||||||
@@ -344,10 +345,11 @@ func (s *PgStore) UpdateUser(ctx context.Context, id uuid.UUID, upd UserUpdate)
|
|||||||
display_name = CASE WHEN $2 THEN $3 ELSE display_name END,
|
display_name = CASE WHEN $2 THEN $3 ELSE display_name END,
|
||||||
role = COALESCE($4::user_role, role),
|
role = COALESCE($4::user_role, role),
|
||||||
is_blocked = COALESCE($5, is_blocked),
|
is_blocked = COALESCE($5, is_blocked),
|
||||||
|
reviews_uploads = COALESCE($7, reviews_uploads),
|
||||||
token_version = token_version + CASE WHEN $6 THEN 1 ELSE 0 END,
|
token_version = token_version + CASE WHEN $6 THEN 1 ELSE 0 END,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING `+userColumns, id, setName, name, role, upd.IsBlocked, upd.RevokeTokens))
|
RETURNING `+userColumns, id, setName, name, role, upd.IsBlocked, upd.RevokeTokens, upd.ReviewsUploads))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetPassword реализует Store.
|
// ResetPassword реализует Store.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const (
|
|||||||
PermManageLibrary // хранилища, сканирование, переиндексация
|
PermManageLibrary // хранилища, сканирование, переиндексация
|
||||||
PermViewLibraryStatus // статус библиотеки (просмотр)
|
PermViewLibraryStatus // статус библиотеки (просмотр)
|
||||||
PermSuggestTracks // предлагать треки, видеть свои предложения
|
PermSuggestTracks // предлагать треки, видеть свои предложения
|
||||||
PermReviewUploads // очередь, одобрение, правка метаданных, лимиты
|
PermReviewUploads // очередь, одобрение, правка метаданных; даёт флаг, не роль
|
||||||
PermSystemStatus // статус рекомендательной системы, аудит
|
PermSystemStatus // статус рекомендательной системы, аудит
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ var permissions = map[Permission][]Role{
|
|||||||
PermManageLibrary: {RoleAdmin},
|
PermManageLibrary: {RoleAdmin},
|
||||||
PermViewLibraryStatus: {RoleModerator, RoleAdmin},
|
PermViewLibraryStatus: {RoleModerator, RoleAdmin},
|
||||||
PermSuggestTracks: {RoleModerator, RoleAdmin},
|
PermSuggestTracks: {RoleModerator, RoleAdmin},
|
||||||
PermReviewUploads: {RoleAdmin},
|
|
||||||
PermSystemStatus: {RoleAdmin},
|
PermSystemStatus: {RoleAdmin},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can сообщает, есть ли у роли глобальное право.
|
// Can сообщает, есть ли у роли глобальное право.
|
||||||
|
// Аудит загрузок сюда не входит: его проверяет Principal.Allowed.
|
||||||
func Can(r Role, p Permission) bool {
|
func Can(r Role, p Permission) bool {
|
||||||
for _, allowed := range permissions[p] {
|
for _, allowed := range permissions[p] {
|
||||||
if allowed == r {
|
if allowed == r {
|
||||||
@@ -38,6 +38,14 @@ func Can(r Role, p Permission) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allowed — право конкретного пользователя. Аудит загрузок не вытекает из роли admin.
|
||||||
|
func (p Principal) Allowed(perm Permission) bool {
|
||||||
|
if perm == PermReviewUploads {
|
||||||
|
return p.ReviewsUploads
|
||||||
|
}
|
||||||
|
return Can(p.Role, perm)
|
||||||
|
}
|
||||||
|
|
||||||
// UserAction — действие над учётной записью другого пользователя.
|
// UserAction — действие над учётной записью другого пользователя.
|
||||||
type UserAction int
|
type UserAction int
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ func TestCan(t *testing.T) {
|
|||||||
{PermManageLibrary, "manage library", [3]bool{false, false, true}},
|
{PermManageLibrary, "manage library", [3]bool{false, false, true}},
|
||||||
{PermViewLibraryStatus, "library status", [3]bool{false, true, true}},
|
{PermViewLibraryStatus, "library status", [3]bool{false, true, true}},
|
||||||
{PermSuggestTracks, "suggest tracks", [3]bool{false, true, true}},
|
{PermSuggestTracks, "suggest tracks", [3]bool{false, true, true}},
|
||||||
{PermReviewUploads, "review uploads", [3]bool{false, false, true}},
|
|
||||||
{PermSystemStatus, "system status", [3]bool{false, false, true}},
|
{PermSystemStatus, "system status", [3]bool{false, false, true}},
|
||||||
}
|
}
|
||||||
roles := [3]Role{RoleUser, RoleModerator, RoleAdmin}
|
roles := [3]Role{RoleUser, RoleModerator, RoleAdmin}
|
||||||
@@ -72,6 +71,14 @@ func TestCanManageUserSelf(t *testing.T) {
|
|||||||
assert.True(t, CanManageUser(admin, admin, UserResetPassword))
|
assert.True(t, CanManageUser(admin, admin, UserResetPassword))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReviewUploadsIsSeparateFromRole(t *testing.T) {
|
||||||
|
assert.False(t, Principal{Role: RoleAdmin}.Allowed(PermReviewUploads))
|
||||||
|
assert.True(t, Principal{Role: RoleModerator, ReviewsUploads: true}.Allowed(PermReviewUploads))
|
||||||
|
assert.True(t, Principal{Role: RoleUser, ReviewsUploads: true}.Allowed(PermReviewUploads))
|
||||||
|
assert.False(t, Principal{Role: RoleUser}.Allowed(PermReviewUploads))
|
||||||
|
assert.True(t, Principal{Role: RoleUser, ReviewsUploads: true}.Allowed(PermSuggestTracks) == false)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCanAssignRole(t *testing.T) {
|
func TestCanAssignRole(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
actor, role Role
|
actor, role Role
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ func (s *Service) Authenticate(ctx context.Context, access string) (Principal, e
|
|||||||
return Principal{
|
return Principal{
|
||||||
UserID: u.ID,
|
UserID: u.ID,
|
||||||
Role: u.Role,
|
Role: u.Role,
|
||||||
|
ReviewsUploads: u.ReviewsUploads,
|
||||||
SessionID: claims.SessionID,
|
SessionID: claims.SessionID,
|
||||||
MustChangePassword: u.MustChangePassword,
|
MustChangePassword: u.MustChangePassword,
|
||||||
MustChangeCredentials: u.MustChangeCredentials,
|
MustChangeCredentials: u.MustChangeCredentials,
|
||||||
@@ -621,7 +622,7 @@ func (s *Service) Bootstrap(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
u := User{
|
u := User{
|
||||||
ID: s.ids.New(), Username: username, PasswordHash: hash, Role: RoleAdmin, Locale: "ru",
|
ID: s.ids.New(), Username: username, PasswordHash: hash, Role: RoleAdmin, Locale: "ru",
|
||||||
MustChangeCredentials: true, CreatedAt: s.clock.Now(),
|
ReviewsUploads: true, MustChangeCredentials: true, CreatedAt: s.clock.Now(),
|
||||||
}
|
}
|
||||||
err = s.store.WithTx(ctx, func(tx Store) error {
|
err = s.store.WithTx(ctx, func(tx Store) error {
|
||||||
if err := tx.CreateUser(ctx, u, nil); err != nil {
|
if err := tx.CreateUser(ctx, u, nil); err != nil {
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ type UserFilter struct {
|
|||||||
|
|
||||||
// UserUpdate — изменения из админки. nil — не менять.
|
// UserUpdate — изменения из админки. nil — не менять.
|
||||||
type UserUpdate struct {
|
type UserUpdate struct {
|
||||||
DisplayName **string
|
DisplayName **string
|
||||||
Role *Role
|
Role *Role
|
||||||
IsBlocked *bool
|
IsBlocked *bool
|
||||||
RevokeTokens bool
|
ReviewsUploads *bool
|
||||||
|
RevokeTokens bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store — доступ к users, refresh_tokens, audit_log и app_settings.
|
// Store — доступ к users, refresh_tokens, audit_log и app_settings.
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (h *Handler) fileDTO(ctx context.Context, f File) gen.UploadFile {
|
|||||||
out.TusUrl = &u
|
out.TusUrl = &u
|
||||||
}
|
}
|
||||||
if h.signer != nil && f.StagingPath != "" && f.Status.KeepsStaging() {
|
if h.signer != nil && f.StagingPath != "" && f.Status.KeepsStaging() {
|
||||||
if p, ok := auth.PrincipalFrom(ctx); ok && auth.Can(p.Role, auth.PermReviewUploads) {
|
if p, ok := auth.PrincipalFrom(ctx); ok && p.ReviewsUploads {
|
||||||
u := h.streamURL(f.ID)
|
u := h.streamURL(f.ID)
|
||||||
out.StreamUrl = &u
|
out.StreamUrl = &u
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func principal(ctx context.Context, perm auth.Permission) (auth.Principal, error
|
|||||||
if !ok {
|
if !ok {
|
||||||
return auth.Principal{}, apperr.Unauthorized(auth.CodeUnauthorized, "not authenticated")
|
return auth.Principal{}, apperr.Unauthorized(auth.CodeUnauthorized, "not authenticated")
|
||||||
}
|
}
|
||||||
if !auth.Can(p.Role, perm) {
|
if !p.Allowed(perm) {
|
||||||
return auth.Principal{}, apperr.Forbidden(auth.CodeForbidden, "not allowed")
|
return auth.Principal{}, apperr.Forbidden(auth.CodeForbidden, "not allowed")
|
||||||
}
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
@@ -425,9 +425,9 @@ func (s *Service) Retry(ctx context.Context, id uuid.UUID) (File, error) {
|
|||||||
return s.store.File(ctx, id)
|
return s.store.File(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSettings — настройки очереди (админ).
|
// GetSettings — лимиты очереди (админ, не аудитор загрузок).
|
||||||
func (s *Service) GetSettings(ctx context.Context) (Settings, error) {
|
func (s *Service) GetSettings(ctx context.Context) (Settings, error) {
|
||||||
if _, err := principal(ctx, auth.PermReviewUploads); err != nil {
|
if _, err := principal(ctx, auth.PermManageLibrary); err != nil {
|
||||||
return Settings{}, err
|
return Settings{}, err
|
||||||
}
|
}
|
||||||
return s.store.Settings(ctx)
|
return s.store.Settings(ctx)
|
||||||
@@ -446,7 +446,7 @@ type SettingsInput struct {
|
|||||||
|
|
||||||
// UpdateSettings меняет лимиты; уменьшение ниже занятого ничего не удаляет (docs/uploads.md#3).
|
// UpdateSettings меняет лимиты; уменьшение ниже занятого ничего не удаляет (docs/uploads.md#3).
|
||||||
func (s *Service) UpdateSettings(ctx context.Context, in SettingsInput) (Settings, error) {
|
func (s *Service) UpdateSettings(ctx context.Context, in SettingsInput) (Settings, error) {
|
||||||
p, err := principal(ctx, auth.PermReviewUploads)
|
p, err := principal(ctx, auth.PermManageLibrary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Settings{}, err
|
return Settings{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- Аудит загрузок — отдельное право, не роль. Уже существующие админы его сохраняют,
|
||||||
|
-- чтобы очередь не осталась без того, кто её рассматривает. Новым админам его выдают явно.
|
||||||
|
ALTER TABLE users ADD COLUMN reviews_uploads boolean NOT NULL DEFAULT false;
|
||||||
|
UPDATE users SET reviews_uploads = true WHERE role = 'admin';
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE users DROP COLUMN reviews_uploads;
|
||||||
Reference in new issue
Block a user