diff --git a/vocodes/src/common/ApiConfig.ts b/vocodes/src/common/ApiConfig.ts index 8c3abddcee..ed3afad8f5 100644 --- a/vocodes/src/common/ApiConfig.ts +++ b/vocodes/src/common/ApiConfig.ts @@ -246,6 +246,10 @@ class ApiConfig { return `${this.getScheme()}://${this.getNewApiHost()}/moderation/ip_bans/${ipAddress}/delete`; } + banUser() : string { + return `${this.getScheme()}://${this.getNewApiHost()}/moderation/user_bans/manage_ban`; + } + private getScheme() : string { return this.isLocalDev ? "http" : "https"; } diff --git a/vocodes/src/common/FrontendUrlConfig.ts b/vocodes/src/common/FrontendUrlConfig.ts index 2927838fcd..c682979020 100644 --- a/vocodes/src/common/FrontendUrlConfig.ts +++ b/vocodes/src/common/FrontendUrlConfig.ts @@ -71,6 +71,14 @@ class FrontendUrlConfig { static userProfilePage(userDisplayName: string) : string { return `/profile/${userDisplayName}`; } + + static userProfileEditPage(userDisplayName: string) : string { + return `/profile/${userDisplayName}/edit`; + } + + static userProfileBanPage(userDisplayName: string) : string { + return `/profile/${userDisplayName}/ban`; + } } export { FrontendUrlConfig } diff --git a/vocodes/src/v2/api/user/GetUserByUsername.ts b/vocodes/src/v2/api/user/GetUserByUsername.ts index caf4a0b428..f778551c95 100644 --- a/vocodes/src/v2/api/user/GetUserByUsername.ts +++ b/vocodes/src/v2/api/user/GetUserByUsername.ts @@ -14,7 +14,6 @@ export interface User { profile_markdown: string, profile_rendered_html: string, user_role_slug: string, - banned: boolean, dark_mode: string, avatar_public_bucket_hash: string, disable_gravatar: boolean, @@ -28,6 +27,7 @@ export interface User { cashapp_username: string | undefined | null, created_at: string, badges: ProfileBadge[], + maybe_moderator_fields: UserProfileModeratorFields | null | undefined, } export interface ProfileBadge { @@ -38,6 +38,13 @@ export interface ProfileBadge { granted_at: string, } +export interface UserProfileModeratorFields { + is_banned: boolean, + maybe_mod_comments: string | null | undefined, + maybe_mod_user_token: string | null | undefined, +} + + export async function GetUserByUsername(username: string) : Promise { const usernameLower = username.toLowerCase(); // NB: Until I standardize on display name vs username lookup. const endpoint = new ApiConfig().getProfile(usernameLower); diff --git a/vocodes/src/v2/view/NewVocodesContainer.tsx b/vocodes/src/v2/view/NewVocodesContainer.tsx index 4cbc91bde5..7ee30af208 100644 --- a/vocodes/src/v2/view/NewVocodesContainer.tsx +++ b/vocodes/src/v2/view/NewVocodesContainer.tsx @@ -36,6 +36,7 @@ import { W2lResultDeleteFc } from './w2l/w2l_result_delete/W2lResultDeleteFc'; import { W2lTemplateApproveFc } from './w2l/w2l_template_approve/W2lTemplateApproveFc'; import { TtsModelListFc } from './tts/tts_model_list/TtsModelListFc'; import { TtsModelListItem } from '../api/tts/ListTtsModels'; +import { ProfileBanFc } from './profile/profile_ban/ProfileBanFc'; interface Props { sessionWrapper: SessionWrapper, @@ -109,6 +110,12 @@ class NewVocodesContainer extends React.Component { /> + + + + (undefined); + + // Form values + const [modComments, setModComments] = useState(""); + const [isBanned, setIsBanned] = useState(false); + + const getUserProfile = useCallback(async (username) => { + const user = await GetUserByUsername(username); + if (user) { + setUserData(user); + setIsBanned(user?.maybe_moderator_fields?.is_banned || false); + setModComments(user?.maybe_moderator_fields?.maybe_mod_comments || ""); + } + }, []); + + useEffect(() => { + getUserProfile(username); + }, [username, getUserProfile]); + + const handleFormSubmit = (ev: React.FormEvent) : boolean => { + ev.preventDefault(); + + const api = new ApiConfig(); + const endpointUrl = api.banUser(); + + const request = { + username: userData?.username, + is_banned: isBanned, + mod_notes: modComments, + } + + fetch(endpointUrl, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(request), + }) + .then(res => res.json()) + .then(res => { + if (res.success) { + history.push(userProfilePage); + } + }) + .catch(e => { + }); + + return false; + } + + + if (!userData) { + // Waiting for load. + return ; + } + + if (!!userData && !props.sessionWrapper.canEditUserProfile(username)) { + // Loading and we don't have access. + history.push(userProfilePage); + } + + + const handleModCommentsChange = (ev: React.FormEvent) => { + ev.preventDefault(); + const textValue = (ev.target as HTMLInputElement).value; + setModComments(textValue); + return false; + }; + + const handleIsBannedChange = (ev: React.FormEvent) => { + let bannedState = false; + switch ((ev.target as HTMLSelectElement).value) { + case "true": + bannedState = true; + break; + case "TRUE": + bannedState = true; + break; + } + setIsBanned(bannedState) + }; + + let viewLinkUrl = `/profile/${userData?.username}`; + + let isDisabled = userData === undefined; + + + return ( +
+

Profile & Preferences

+ + < Back to profile + +
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ {/*

{invalidReason}

*/} +
+ + +
+ + + +
+
+ +
+ +
+

Notes on banned users:

+
    +
  • +
  • +
  • +
+
+ +
+ ) +} + +export { ProfileBanFc }; diff --git a/vocodes/src/v2/view/profile/profile_view/ProfileFc.tsx b/vocodes/src/v2/view/profile/profile_view/ProfileFc.tsx index 46ce2a61bd..dc782c7759 100644 --- a/vocodes/src/v2/view/profile/profile_view/ProfileFc.tsx +++ b/vocodes/src/v2/view/profile/profile_view/ProfileFc.tsx @@ -12,6 +12,7 @@ import { faDiscord, faFirefox, faGithub, faTwitch, faTwitter } from '@fortawesom import { faClock, faDollarSign } from '@fortawesome/free-solid-svg-icons'; import { GetUserByUsername, User } from '../../../api/user/GetUserByUsername'; import { format } from 'date-fns'; +import { FrontendUrlConfig } from '../../../../common/FrontendUrlConfig'; interface Props { sessionWrapper: SessionWrapper, @@ -43,7 +44,7 @@ function ProfileFc(props: Props) { let editProfileButton = if (props.sessionWrapper.canEditUserProfile(userData.username)) { - const editLinkUrl = `/profile/${userData.username}/edit`; + const editLinkUrl = FrontendUrlConfig.userProfileEditPage(userData.username); // Mods shouldn't edit preferences. const buttonLabel = props.sessionWrapper.userTokenMatches(userData.user_token) ? @@ -59,6 +60,26 @@ function ProfileFc(props: Props) { ); } + let banUserButton = + + if (props.sessionWrapper.canBanUsers()) { + const currentlyBanned = userData.maybe_moderator_fields?.is_banned; + const banLinkUrl = FrontendUrlConfig.userProfileBanPage(userData.username); + const buttonLabel = currentlyBanned ? "Unban User" : "Ban User"; + const banButtonCss = currentlyBanned ? + "button is-warning is-large is-fullwidth" : + "button is-danger is-large is-fullwidth"; + + banUserButton = ( + <> + {buttonLabel} +
+ + ); + } + let profileRows : Array = []; if (userData.website_url !== undefined && userData.website_url !== null) { @@ -209,6 +230,7 @@ function ProfileFc(props: Props) { {editProfileButton} + {banUserButton}