mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Create/update/remove domain permission subscriptions (#3623)
* [feature] Create/update/remove domain permission subscriptions * lint * envparsing * remove errant fmt.Println * create drafts, subs, exclude, from snapshot models * name etag column correctly * remove count column * lint
This commit is contained in:
@@ -41,6 +41,7 @@ import type {
|
||||
ChecklistInputHook,
|
||||
FieldArrayInputHook,
|
||||
ArrayInputHook,
|
||||
NumberFormInputHook,
|
||||
} from "./types";
|
||||
|
||||
function capitalizeFirst(str: string) {
|
||||
@@ -102,11 +103,11 @@ function value<T>(name: string, initialValue: T) {
|
||||
name,
|
||||
Name: "",
|
||||
value: initialValue,
|
||||
hasChanged: () => true, // always included
|
||||
};
|
||||
}
|
||||
|
||||
export const useTextInput = inputHook(text) as (_name: string, _opts?: HookOpts<string>) => TextFormInputHook;
|
||||
export const useNumberInput = inputHook(text) as (_name: string, _opts?: HookOpts<number>) => NumberFormInputHook;
|
||||
export const useFileInput = inputHook(file) as (_name: string, _opts?: HookOpts<File>) => FileFormInputHook;
|
||||
export const useBoolInput = inputHook(bool) as (_name: string, _opts?: HookOpts<boolean>) => BoolFormInputHook;
|
||||
export const useRadioInput = inputHook(radio) as (_name: string, _opts?: HookOpts<string>) => RadioFormInputHook;
|
||||
|
104
web/source/settings/lib/form/number.tsx
Normal file
104
web/source/settings/lib/form/number.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useTransition,
|
||||
useEffect,
|
||||
} from "react";
|
||||
|
||||
import type {
|
||||
CreateHookNames,
|
||||
HookOpts,
|
||||
NumberFormInputHook,
|
||||
} from "./types";
|
||||
|
||||
const _default = 0;
|
||||
|
||||
export default function useNumberInput(
|
||||
{ name, Name }: CreateHookNames,
|
||||
{
|
||||
initialValue = _default,
|
||||
dontReset = false,
|
||||
validator,
|
||||
showValidation = true,
|
||||
initValidation,
|
||||
nosubmit = false,
|
||||
}: HookOpts<number>
|
||||
): NumberFormInputHook {
|
||||
const [number, setNumber] = useState(initialValue);
|
||||
const numberRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [validation, setValidation] = useState(initValidation ?? "");
|
||||
const [_isValidating, startValidation] = useTransition();
|
||||
const valid = validation == "";
|
||||
|
||||
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const input = e.target.valueAsNumber;
|
||||
setNumber(input);
|
||||
|
||||
if (validator) {
|
||||
startValidation(() => {
|
||||
setValidation(validator(input));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (!dontReset) {
|
||||
setNumber(initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (validator && numberRef.current) {
|
||||
if (showValidation) {
|
||||
numberRef.current.setCustomValidity(validation);
|
||||
} else {
|
||||
numberRef.current.setCustomValidity("");
|
||||
}
|
||||
}
|
||||
}, [validation, validator, showValidation]);
|
||||
|
||||
// Array / Object hybrid, for easier access in different contexts
|
||||
return Object.assign([
|
||||
onChange,
|
||||
reset,
|
||||
{
|
||||
[name]: number,
|
||||
[`${name}Ref`]: numberRef,
|
||||
[`set${Name}`]: setNumber,
|
||||
[`${name}Valid`]: valid,
|
||||
}
|
||||
], {
|
||||
onChange,
|
||||
reset,
|
||||
name,
|
||||
Name: "", // Will be set by inputHook function.
|
||||
nosubmit,
|
||||
value: number,
|
||||
ref: numberRef,
|
||||
setter: setNumber,
|
||||
valid,
|
||||
validate: () => setValidation(validator ? validator(number): ""),
|
||||
hasChanged: () => number != initialValue,
|
||||
_default
|
||||
});
|
||||
}
|
@@ -34,8 +34,24 @@ import type {
|
||||
} from "./types";
|
||||
|
||||
interface UseFormSubmitOptions {
|
||||
/**
|
||||
* Include only changed fields when submitting the form.
|
||||
* If no fields have been changed, submit will be a noop.
|
||||
*/
|
||||
changedOnly: boolean;
|
||||
/**
|
||||
* Optional function to run when the form has been sent
|
||||
* and a response has been returned from the server.
|
||||
*/
|
||||
onFinish?: ((_res: any) => void);
|
||||
/**
|
||||
* Can be optionally used to modify the final mutation argument from the
|
||||
* gathered mutation data before it's passed into the trigger function.
|
||||
*
|
||||
* Useful if the mutation trigger function takes not just a simple key/value
|
||||
* object but a more complicated object.
|
||||
*/
|
||||
customizeMutationArgs?: (_mutationData: { [k: string]: any }) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +121,7 @@ export default function useFormSubmit(
|
||||
usedAction.current = action;
|
||||
|
||||
// Transform the hooked form into an object.
|
||||
const {
|
||||
let {
|
||||
mutationData,
|
||||
updatedFields,
|
||||
} = getFormMutations(form, { changedOnly });
|
||||
@@ -117,7 +133,12 @@ export default function useFormSubmit(
|
||||
return;
|
||||
}
|
||||
|
||||
// Final tweaks on the mutation
|
||||
// argument before triggering it.
|
||||
mutationData.action = action;
|
||||
if (opts.customizeMutationArgs) {
|
||||
mutationData = opts.customizeMutationArgs(mutationData);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await runMutation(mutationData);
|
||||
|
@@ -181,6 +181,13 @@ export interface TextFormInputHook extends FormInputHook<string>,
|
||||
_withValidate,
|
||||
_withRef {}
|
||||
|
||||
export interface NumberFormInputHook extends FormInputHook<number>,
|
||||
_withSetter<number>,
|
||||
_withOnChange,
|
||||
_withReset,
|
||||
_withValidate,
|
||||
_withRef {}
|
||||
|
||||
export interface RadioFormInputHook extends FormInputHook<string>,
|
||||
_withSetter<string>,
|
||||
_withOnChange,
|
||||
|
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { gtsApi } from "../../gts-api";
|
||||
|
||||
import type {
|
||||
DomainPermSub,
|
||||
DomainPermSubCreateUpdateParams,
|
||||
DomainPermSubSearchParams,
|
||||
DomainPermSubSearchResp,
|
||||
} from "../../../types/domain-permission";
|
||||
import parse from "parse-link-header";
|
||||
import { PermType } from "../../../types/perm";
|
||||
|
||||
const extended = gtsApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
searchDomainPermissionSubscriptions: build.query<DomainPermSubSearchResp, DomainPermSubSearchParams>({
|
||||
query: (form) => {
|
||||
const params = new(URLSearchParams);
|
||||
Object.entries(form).forEach(([k, v]) => {
|
||||
if (v !== undefined) {
|
||||
params.append(k, v);
|
||||
}
|
||||
});
|
||||
|
||||
let query = "";
|
||||
if (params.size !== 0) {
|
||||
query = `?${params.toString()}`;
|
||||
}
|
||||
|
||||
return {
|
||||
url: `/api/v1/admin/domain_permission_subscriptions${query}`
|
||||
};
|
||||
},
|
||||
// Headers required for paging.
|
||||
transformResponse: (apiResp: DomainPermSub[], meta) => {
|
||||
const subs = apiResp;
|
||||
const linksStr = meta?.response?.headers.get("Link");
|
||||
const links = parse(linksStr);
|
||||
return { subs, links };
|
||||
},
|
||||
// Only provide TRANSFORMED tag id since this model is not the same
|
||||
// as getDomainPermissionSubscription model (due to transformResponse).
|
||||
providesTags: [{ type: "DomainPermissionSubscription", id: "TRANSFORMED" }]
|
||||
}),
|
||||
|
||||
getDomainPermissionSubscriptionsPreview: build.query<DomainPermSub[], PermType>({
|
||||
query: (permType) => ({
|
||||
url: `/api/v1/admin/domain_permission_subscriptions/preview?permission_type=${permType}`
|
||||
}),
|
||||
providesTags: (_result, _error, permType) =>
|
||||
// Cache by permission type.
|
||||
[{ type: "DomainPermissionSubscription", id: `${permType}sByPriority` }]
|
||||
}),
|
||||
|
||||
getDomainPermissionSubscription: build.query<DomainPermSub, string>({
|
||||
query: (id) => ({
|
||||
url: `/api/v1/admin/domain_permission_subscriptions/${id}`
|
||||
}),
|
||||
providesTags: (_result, _error, id) => [
|
||||
{ type: 'DomainPermissionSubscription', id }
|
||||
],
|
||||
}),
|
||||
|
||||
createDomainPermissionSubscription: build.mutation<DomainPermSub, DomainPermSubCreateUpdateParams>({
|
||||
query: (formData) => ({
|
||||
method: "POST",
|
||||
url: `/api/v1/admin/domain_permission_subscriptions`,
|
||||
asForm: true,
|
||||
body: formData,
|
||||
discardEmpty: true
|
||||
}),
|
||||
invalidatesTags: (_res, _error, formData) =>
|
||||
[
|
||||
// Invalidate transformed list of all perm subs.
|
||||
{ type: "DomainPermissionSubscription", id: "TRANSFORMED" },
|
||||
// Invalidate perm subs of this type sorted by priority.
|
||||
{ type: "DomainPermissionSubscription", id: `${formData.permission_type}sByPriority` }
|
||||
]
|
||||
}),
|
||||
|
||||
updateDomainPermissionSubscription: build.mutation<DomainPermSub, { id: string, permType: PermType, formData: DomainPermSubCreateUpdateParams }>({
|
||||
query: ({ id, formData }) => ({
|
||||
method: "PATCH",
|
||||
url: `/api/v1/admin/domain_permission_subscriptions/${id}`,
|
||||
asForm: true,
|
||||
body: formData,
|
||||
}),
|
||||
invalidatesTags: (_res, _error, { id, permType }) =>
|
||||
[
|
||||
// Invalidate this perm sub.
|
||||
{ type: "DomainPermissionSubscription", id: id },
|
||||
// Invalidate transformed list of all perms subs.
|
||||
{ type: "DomainPermissionSubscription", id: "TRANSFORMED" },
|
||||
// Invalidate perm subs of this type sorted by priority.
|
||||
{ type: "DomainPermissionSubscription", id: `${permType}sByPriority` }
|
||||
],
|
||||
}),
|
||||
|
||||
removeDomainPermissionSubscription: build.mutation<DomainPermSub, { id: string, remove_children: boolean }>({
|
||||
query: ({ id, remove_children }) => ({
|
||||
method: "POST",
|
||||
url: `/api/v1/admin/domain_permission_subscriptions/${id}/remove`,
|
||||
asForm: true,
|
||||
body: { remove_children: remove_children },
|
||||
}),
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* View domain permission subscriptions.
|
||||
*/
|
||||
const useLazySearchDomainPermissionSubscriptionsQuery = extended.useLazySearchDomainPermissionSubscriptionsQuery;
|
||||
|
||||
/**
|
||||
* Get domain permission subscription with the given ID.
|
||||
*/
|
||||
const useGetDomainPermissionSubscriptionQuery = extended.useGetDomainPermissionSubscriptionQuery;
|
||||
|
||||
/**
|
||||
* Create a domain permission subscription with the given parameters.
|
||||
*/
|
||||
const useCreateDomainPermissionSubscriptionMutation = extended.useCreateDomainPermissionSubscriptionMutation;
|
||||
|
||||
/**
|
||||
* View domain permission subscriptions of selected perm type, sorted by priority descending.
|
||||
*/
|
||||
const useGetDomainPermissionSubscriptionsPreviewQuery = extended.useGetDomainPermissionSubscriptionsPreviewQuery;
|
||||
|
||||
/**
|
||||
* Update domain permission subscription.
|
||||
*/
|
||||
const useUpdateDomainPermissionSubscriptionMutation = extended.useUpdateDomainPermissionSubscriptionMutation;
|
||||
|
||||
/**
|
||||
* Remove a domain permission subscription and optionally its children (harsh).
|
||||
*/
|
||||
const useRemoveDomainPermissionSubscriptionMutation = extended.useRemoveDomainPermissionSubscriptionMutation;
|
||||
|
||||
export {
|
||||
useLazySearchDomainPermissionSubscriptionsQuery,
|
||||
useGetDomainPermissionSubscriptionQuery,
|
||||
useCreateDomainPermissionSubscriptionMutation,
|
||||
useGetDomainPermissionSubscriptionsPreviewQuery,
|
||||
useUpdateDomainPermissionSubscriptionMutation,
|
||||
useRemoveDomainPermissionSubscriptionMutation,
|
||||
};
|
@@ -170,7 +170,8 @@ export const gtsApi = createApi({
|
||||
"DefaultInteractionPolicies",
|
||||
"InteractionRequest",
|
||||
"DomainPermissionDraft",
|
||||
"DomainPermissionExclude"
|
||||
"DomainPermissionExclude",
|
||||
"DomainPermissionSubscription"
|
||||
],
|
||||
endpoints: (build) => ({
|
||||
instanceV1: build.query<InstanceV1, void>({
|
||||
|
@@ -20,6 +20,7 @@
|
||||
import typia from "typia";
|
||||
import { PermType } from "./perm";
|
||||
import { Links } from "parse-link-header";
|
||||
import { PermSubContentType } from "./permsubcontenttype";
|
||||
|
||||
export const validateDomainPerms = typia.createValidate<DomainPerm[]>();
|
||||
|
||||
@@ -213,3 +214,156 @@ export interface DomainPermExcludeCreateParams {
|
||||
*/
|
||||
private_comment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API model of one domain permission susbcription.
|
||||
*/
|
||||
export interface DomainPermSub {
|
||||
/**
|
||||
* The ID of the domain permission subscription.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The priority of the domain permission subscription.
|
||||
*/
|
||||
priority: number;
|
||||
/**
|
||||
* Time at which the subscription was created (ISO 8601 Datetime).
|
||||
*/
|
||||
created_at: string;
|
||||
/**
|
||||
* Title of this subscription, as set by admin who created or updated it.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The type of domain permission subscription (allow, block).
|
||||
*/
|
||||
permission_type: PermType;
|
||||
/**
|
||||
* If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect.
|
||||
* If false, domain permissions from this subscription will come into force immediately.
|
||||
*/
|
||||
as_draft: boolean;
|
||||
/**
|
||||
* If true, this domain permission subscription will "adopt" domain permissions
|
||||
* which already exist on the instance, and which meet the following conditions:
|
||||
* 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
|
||||
* in the subscribed list. Such orphaned domain permissions will be given this
|
||||
* subscription's subscription ID value and be managed by this subscription.
|
||||
*/
|
||||
adopt_orphans: boolean;
|
||||
/**
|
||||
* ID of the account that created this subscription.
|
||||
*/
|
||||
created_by: string;
|
||||
/**
|
||||
* URI to call in order to fetch the permissions list.
|
||||
*/
|
||||
uri: string;
|
||||
/**
|
||||
* MIME content type to use when parsing the permissions list.
|
||||
*/
|
||||
content_type: PermSubContentType;
|
||||
/**
|
||||
* (Optional) username to set for basic auth when doing a fetch of URI.
|
||||
*/
|
||||
fetch_username?: string;
|
||||
/**
|
||||
* (Optional) password to set for basic auth when doing a fetch of URI.
|
||||
*/
|
||||
fetch_password?: string;
|
||||
/**
|
||||
* Time at which the most recent fetch was attempted (ISO 8601 Datetime).
|
||||
*/
|
||||
fetched_at?: string;
|
||||
/**
|
||||
* Time of the most recent successful fetch (ISO 8601 Datetime).
|
||||
*/
|
||||
successfully_fetched_at?: string;
|
||||
/**
|
||||
* If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt.
|
||||
*/
|
||||
error?: string;
|
||||
/**
|
||||
* Count of domain permission entries discovered at URI on last (successful) fetch.
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for GET to /api/v1/admin/domain_permission_subscriptions.
|
||||
*/
|
||||
export interface DomainPermSubSearchParams {
|
||||
/**
|
||||
* Return only block or allow subscriptions.
|
||||
*/
|
||||
permission_type?: PermType;
|
||||
/**
|
||||
* Return only items *OLDER* than the given max ID (for paging downwards).
|
||||
* The item with the specified ID will not be included in the response.
|
||||
*/
|
||||
max_id?: string;
|
||||
/**
|
||||
* Return only items *NEWER* than the given since ID.
|
||||
* The item with the specified ID will not be included in the response.
|
||||
*/
|
||||
since_id?: string;
|
||||
/**
|
||||
* Return only items immediately *NEWER* than the given min ID (for paging upwards).
|
||||
* The item with the specified ID will not be included in the response.
|
||||
*/
|
||||
min_id?: string;
|
||||
/**
|
||||
* Number of items to return.
|
||||
*/
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface DomainPermSubCreateUpdateParams {
|
||||
/**
|
||||
* The priority of the domain permission subscription.
|
||||
*/
|
||||
priority?: number;
|
||||
/**
|
||||
* Title of this subscription, as set by admin who created or updated it.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* URI to call in order to fetch the permissions list.
|
||||
*/
|
||||
uri: string;
|
||||
/**
|
||||
* MIME content type to use when parsing the permissions list.
|
||||
*/
|
||||
content_type: PermSubContentType;
|
||||
/**
|
||||
* If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect.
|
||||
* If false, domain permissions from this subscription will come into force immediately.
|
||||
*/
|
||||
as_draft?: boolean;
|
||||
/**
|
||||
* If true, this domain permission subscription will "adopt" domain permissions
|
||||
* which already exist on the instance, and which meet the following conditions:
|
||||
* 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
|
||||
* in the subscribed list. Such orphaned domain permissions will be given this
|
||||
* subscription's subscription ID value and be managed by this subscription.
|
||||
*/
|
||||
adopt_orphans?: boolean;
|
||||
/**
|
||||
* (Optional) username to set for basic auth when doing a fetch of URI.
|
||||
*/
|
||||
fetch_username?: string;
|
||||
/**
|
||||
* (Optional) password to set for basic auth when doing a fetch of URI.
|
||||
*/
|
||||
fetch_password?: string;
|
||||
/**
|
||||
* The type of domain permission subscription to create or update (allow, block).
|
||||
*/
|
||||
permission_type: PermType;
|
||||
}
|
||||
|
||||
export interface DomainPermSubSearchResp {
|
||||
subs: DomainPermSub[];
|
||||
links: Links | null;
|
||||
}
|
||||
|
20
web/source/settings/lib/types/permsubcontenttype.ts
Normal file
20
web/source/settings/lib/types/permsubcontenttype.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export type PermSubContentType = "text/plain" | "text/csv" | "application/json";
|
@@ -46,3 +46,22 @@ export function formDomainValidator(domain: string): string {
|
||||
|
||||
return "invalid domain";
|
||||
}
|
||||
|
||||
export function urlValidator(urlStr: string): string {
|
||||
if (urlStr.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(urlStr);
|
||||
} catch (e) {
|
||||
return e.message;
|
||||
}
|
||||
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return `invalid protocol, must be http or https`;
|
||||
}
|
||||
|
||||
return formDomainValidator(url.host);
|
||||
}
|
||||
|
Reference in New Issue
Block a user