Skip to content

Commit 450b46a

Browse files
Add optional tag edit role (#932)
1 parent 0ce5170 commit 450b46a

File tree

16 files changed

+114
-16
lines changed

16 files changed

+114
-16
lines changed

.golangci.yml

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
run:
22
timeout: 3m
3-
go: 1.23
4-
5-
issues:
6-
skip-files:
7-
- "pkg/models/generated_.*.go$"
3+
go: '1.23'
84

95
linters:
106
enable:
@@ -35,8 +31,6 @@ linters-settings:
3531
ignore-generated-header: true
3632
severity: error
3733
confidence: 0.8
38-
error-code: 1
39-
warning-code: 1
4034
rules:
4135
- name: blank-imports
4236
disabled: true
@@ -48,7 +42,7 @@ linters-settings:
4842
- name: error-naming
4943
- name: exported
5044
- name: if-return
51-
enabled: true
45+
disabled: false
5246
- name: increment-decrement
5347
- name: var-naming
5448
arguments:

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ There are two ways to authenticate a user in Stash-box: a session or an API key.
105105
| `postgres.max_idle_conns` | (0) | Maximum number of concurrent idle database connections. |
106106
| `postgres.conn_max_lifetime` | (0) | Maximum lifetime in minutes before a connection is released. |
107107
| `require_scene_draft` | false | Whether to allow scene creation outside of draft submissions. |
108+
| `require_tag_role` | false | Whether to require the EditTag role to edit tags. |
108109

109110
## SSL (HTTPS)
110111

frontend/src/graphql/queries/Config.gql

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ query Config {
1111
vote_cron_interval
1212
guidelines_url
1313
require_scene_draft
14+
require_tag_role
1415
}
1516
}

frontend/src/graphql/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,7 @@ export enum RoleEnum {
14951495
ADMIN = "ADMIN",
14961496
BOT = "BOT",
14971497
EDIT = "EDIT",
1498+
EDIT_TAGS = "EDIT_TAGS",
14981499
/** May generate invites without tokens */
14991500
INVITE = "INVITE",
15001501
/** May grant and rescind invite tokens and resind invite keys */
@@ -1751,6 +1752,7 @@ export type StashBoxConfig = {
17511752
require_activation: Scalars["Boolean"]["output"];
17521753
require_invite: Scalars["Boolean"]["output"];
17531754
require_scene_draft: Scalars["Boolean"]["output"];
1755+
require_tag_role: Scalars["Boolean"]["output"];
17541756
vote_application_threshold: Scalars["Int"]["output"];
17551757
vote_cron_interval: Scalars["String"]["output"];
17561758
vote_promotion_threshold?: Maybe<Scalars["Int"]["output"]>;
@@ -14568,6 +14570,7 @@ export type ConfigQuery = {
1456814570
vote_cron_interval: string;
1456914571
guidelines_url: string;
1457014572
require_scene_draft: boolean;
14573+
require_tag_role: boolean;
1457114574
};
1457214575
};
1457314576

@@ -54566,6 +54569,10 @@ export const ConfigDocument = {
5456654569
kind: "Field",
5456754570
name: { kind: "Name", value: "require_scene_draft" },
5456854571
},
54572+
{
54573+
kind: "Field",
54574+
name: { kind: "Name", value: "require_tag_role" },
54575+
},
5456954576
],
5457054577
},
5457154578
},

frontend/src/hooks/useCurrentUser.tsx

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import { useContext, useMemo, useCallback } from "react";
2+
import { useConfig } from "src/graphql/queries";
23

34
import AuthContext from "src/context";
4-
import { isAdmin as userIsAdmin, canEdit, canVote } from "src/utils";
5+
import {
6+
isAdmin as userIsAdmin,
7+
canEdit,
8+
canTagEdit,
9+
canVote,
10+
} from "src/utils";
511

612
export const useCurrentUser = () => {
713
const auth = useContext(AuthContext);
14+
const { data: config } = useConfig();
15+
const requireTagRole = config?.getConfig.require_tag_role ?? false;
816

917
const isAdmin = useMemo(() => userIsAdmin(auth.user), [auth.user]);
1018
const isEditor = useMemo(() => canEdit(auth.user), [auth.user]);
1119
const isVoter = useMemo(() => canVote(auth.user), [auth.user]);
20+
const isTagEditor = useMemo(
21+
() => (requireTagRole ? canTagEdit(auth.user) : canEdit(auth.user)),
22+
[auth.user, requireTagRole],
23+
);
1224
const isSelf = useCallback(
1325
(user?: (typeof auth)["user"] | string | null) => {
1426
if (!auth.user?.id || !user) return false;
@@ -22,6 +34,7 @@ export const useCurrentUser = () => {
2234
isAdmin,
2335
isSelf,
2436
isEditor,
37+
isTagEditor,
2538
isVoter,
2639
isAuthenticated: auth.authenticated,
2740
user: auth.user,

frontend/src/pages/tags/Tag.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface Props {
2727
}
2828

2929
const TagComponent: FC<Props> = ({ tag }) => {
30-
const { isEditor } = useCurrentUser();
30+
const { isTagEditor } = useCurrentUser();
3131
const navigate = useNavigate();
3232
const location = useLocation();
3333
const activeTab = location.hash?.slice(1) || DEFAULT_TAB;
@@ -48,7 +48,7 @@ const TagComponent: FC<Props> = ({ tag }) => {
4848
<span className="me-2">Tag:</span>
4949
{tag.deleted ? <del>{tag.name}</del> : <em>{tag.name}</em>}
5050
</h3>
51-
{isEditor && !tag.deleted && (
51+
{isTagEditor && !tag.deleted && (
5252
<div className="ms-auto">
5353
<Link to={tagHref(tag, ROUTE_TAG_EDIT)} className="ms-2">
5454
<Button>Edit</Button>

frontend/src/pages/tags/Tags.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import { ROUTE_TAG_ADD } from "src/constants/route";
88
import { useCurrentUser } from "src/hooks";
99

1010
const Tags: FC = () => {
11-
const { isEditor } = useCurrentUser();
11+
const { isTagEditor } = useCurrentUser();
1212
return (
1313
<>
1414
<div className="d-flex">
1515
<h3>Tags</h3>
16-
{isEditor && (
16+
{isTagEditor && (
1717
<Link to={createHref(ROUTE_TAG_ADD)} className="ms-auto">
1818
<Button className="ms-auto">Create</Button>
1919
</Link>

frontend/src/utils/user.ts

+3
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ export const isAdmin = (user?: User) =>
2424
export const canEdit = (user?: User) =>
2525
(user?.roles ?? []).includes(RoleEnum.EDIT) || isAdmin(user);
2626

27+
export const canTagEdit = (user?: User) =>
28+
(user?.roles ?? []).includes(RoleEnum.EDIT_TAGS) || isAdmin(user);
29+
2730
export const canVote = (user?: User) =>
2831
(user?.roles ?? []).includes(RoleEnum.VOTE) || isAdmin(user);

graphql/schema/types/config.graphql

+1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ type StashBoxConfig {
1010
guidelines_url: String!
1111
require_scene_draft: Boolean!
1212
edit_update_limit: Int!
13+
require_tag_role: Boolean!
1314
}

graphql/schema/types/user.graphql

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ enum RoleEnum {
1212
"""May grant and rescind invite tokens and resind invite keys"""
1313
MANAGE_INVITES
1414
BOT
15+
EDIT_TAGS
1516
}
1617

1718
type InviteKey {

pkg/api/resolver.go

+1
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,6 @@ func (r *queryResolver) GetConfig(ctx context.Context) (*models.StashBoxConfig,
130130
GuidelinesURL: config.GetGuidelinesURL(),
131131
RequireSceneDraft: config.GetRequireSceneDraft(),
132132
EditUpdateLimit: config.GetEditUpdateLimit(),
133+
RequireTagRole: config.GetRequireTagRole(),
133134
}, nil
134135
}

pkg/api/resolver_model_notification.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@ func (r *notificationResolver) Data(ctx context.Context, obj *models.Notificatio
4949

5050
if obj.Type == models.NotificationEnumFavoritePerformerScene {
5151
return &models.FavoritePerformerScene{Scene: scene}, nil
52-
} else {
53-
return &models.FavoriteStudioScene{Scene: scene}, nil
5452
}
53+
return &models.FavoriteStudioScene{Scene: scene}, nil
5554

5655
case models.NotificationEnumFavoritePerformerEdit:
5756
fallthrough

pkg/api/resolver_mutation_edit.go

+6
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ func (r *mutationResolver) StudioEditUpdate(ctx context.Context, id uuid.UUID, i
192192
}
193193

194194
func (r *mutationResolver) TagEdit(ctx context.Context, input models.TagEditInput) (*models.Edit, error) {
195+
if config.GetRequireTagRole() {
196+
if err := user.ValidateRole(ctx, models.RoleEnumEditTags); err != nil {
197+
return nil, err
198+
}
199+
}
200+
195201
UUID, err := uuid.NewV4()
196202
if err != nil {
197203
return nil, err

pkg/manager/config/config.go

+7
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ type config struct {
6767
EditUpdateLimit int `mapstructure:"edit_update_limit"`
6868
// Require all scene create edits to be submitted via drafts
6969
RequireSceneDraft bool `mapstructure:"require_scene_draft"`
70+
// Require the TagRole or Admin to edit tags
71+
RequireTagRole bool `mapstructure:"require_tag_role"`
7072

7173
// Email settings
7274
EmailHost string `mapstructure:"email_host"`
@@ -136,6 +138,7 @@ var C = &config{
136138
DraftTimeLimit: 86400,
137139
EditUpdateLimit: 1,
138140
RequireSceneDraft: false,
141+
RequireTagRole: false,
139142
}
140143

141144
func GetDatabasePath() string {
@@ -406,6 +409,10 @@ func GetRequireSceneDraft() bool {
406409
return C.RequireSceneDraft
407410
}
408411

412+
func GetRequireTagRole() bool {
413+
return C.RequireTagRole
414+
}
415+
409416
func GetTitle() string {
410417
if C.Title == "" {
411418
return "Stash-Box"

pkg/models/generated_exec.go

+61
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/models/generated_models.go

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)