From bd2dd90a980dc36f39ff013b6d0dd9bc5cf057ad Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 6 Sep 2021 03:57:11 -0400 Subject: [PATCH] Moderation: user lists, BackLink, etc. --- vocodes/src/common/ApiConfig.ts | 4 + vocodes/src/common/FrontendUrlConfig.ts | 6 + vocodes/src/v2/api/moderation/GetUserList.ts | 68 +++++++++++ vocodes/src/v2/view/NewVocodesContainer.tsx | 7 ++ vocodes/src/v2/view/_common/BackLink.tsx | 17 +++ vocodes/src/v2/view/_icons/BackButtonIcon.tsx | 13 ++ .../moderation_main/ModerationFc.tsx | 7 ++ .../ModerationUserList.tsx | 115 ++++++++++++++++++ .../profile/profile_edit/ProfileEditFc.tsx | 5 +- 9 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 vocodes/src/v2/api/moderation/GetUserList.ts create mode 100644 vocodes/src/v2/view/_common/BackLink.tsx create mode 100644 vocodes/src/v2/view/_icons/BackButtonIcon.tsx create mode 100644 vocodes/src/v2/view/moderation/moderation_user_list/ModerationUserList.tsx diff --git a/vocodes/src/common/ApiConfig.ts b/vocodes/src/common/ApiConfig.ts index ed3afad8f5..f50abb542c 100644 --- a/vocodes/src/common/ApiConfig.ts +++ b/vocodes/src/common/ApiConfig.ts @@ -250,6 +250,10 @@ class ApiConfig { return `${this.getScheme()}://${this.getNewApiHost()}/moderation/user_bans/manage_ban`; } + getModerationUserList() : string { + return `${this.getScheme()}://${this.getNewApiHost()}/moderation/user/list`; + } + private getScheme() : string { return this.isLocalDev ? "http" : "https"; } diff --git a/vocodes/src/common/FrontendUrlConfig.ts b/vocodes/src/common/FrontendUrlConfig.ts index c682979020..84dd473715 100644 --- a/vocodes/src/common/FrontendUrlConfig.ts +++ b/vocodes/src/common/FrontendUrlConfig.ts @@ -79,6 +79,12 @@ class FrontendUrlConfig { static userProfileBanPage(userDisplayName: string) : string { return `/profile/${userDisplayName}/ban`; } + + // Moderation links + + static moderationMain() : string { + return '/moderation'; + } } export { FrontendUrlConfig } diff --git a/vocodes/src/v2/api/moderation/GetUserList.ts b/vocodes/src/v2/api/moderation/GetUserList.ts new file mode 100644 index 0000000000..c426455689 --- /dev/null +++ b/vocodes/src/v2/api/moderation/GetUserList.ts @@ -0,0 +1,68 @@ +import { ApiConfig } from "../../../common/ApiConfig"; + +//export interface GetUserListRequest { +//} + +export interface GetUserListSuccessResponse { + success: boolean, + users: Array +} + +export interface UserForList { + user_id: number, + user_token: string, + + username: string, + display_name: string, + gravatar_hash: string, + + is_banned: boolean, + user_role_slug: string, + + created_at: Date, + updated_at: Date, +} + +export interface GetUserListErrorResponse { + success: boolean, + error_type: string, + error_message: string, +} + +type GetUserListResponse = GetUserListSuccessResponse | GetUserListErrorResponse; + +export function GetUserListIsOk(response: GetUserListResponse): response is GetUserListSuccessResponse { + return response?.success === true; +} + +export function GetUserListIsError(response: GetUserListResponse): response is GetUserListErrorResponse { + return response?.success === false; +} + +export async function GetUserList() : Promise +{ + const endpoint = new ApiConfig().getModerationUserList(); + + return await fetch(endpoint, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + credentials: 'include', + }) + .then(res => res.json()) + .then(res => { + if (!res) { + return { success : false }; + } + + if (res && 'success' in res) { + return res; + } else { + return { success : false }; + } + }) + .catch(e => { + return { success : false }; + }); +} diff --git a/vocodes/src/v2/view/NewVocodesContainer.tsx b/vocodes/src/v2/view/NewVocodesContainer.tsx index 7ee30af208..3658abb4a5 100644 --- a/vocodes/src/v2/view/NewVocodesContainer.tsx +++ b/vocodes/src/v2/view/NewVocodesContainer.tsx @@ -37,6 +37,7 @@ import { W2lTemplateApproveFc } from './w2l/w2l_template_approve/W2lTemplateAppr import { TtsModelListFc } from './tts/tts_model_list/TtsModelListFc'; import { TtsModelListItem } from '../api/tts/ListTtsModels'; import { ProfileBanFc } from './profile/profile_ban/ProfileBanFc'; +import { ModerationUserListFc } from './moderation/moderation_user_list/ModerationUserList'; interface Props { sessionWrapper: SessionWrapper, @@ -252,6 +253,12 @@ class NewVocodesContainer extends React.Component { /> + + + + {linkText} + ) +} + +export { BackLink }; diff --git a/vocodes/src/v2/view/_icons/BackButtonIcon.tsx b/vocodes/src/v2/view/_icons/BackButtonIcon.tsx new file mode 100644 index 0000000000..1a6889b6c2 --- /dev/null +++ b/vocodes/src/v2/view/_icons/BackButtonIcon.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faAngleLeft } from '@fortawesome/free-solid-svg-icons' + +interface Props { + title?: string, +} + +export function BackButtonIcon(props: Props) { + return ( + + ); +} diff --git a/vocodes/src/v2/view/moderation/moderation_main/ModerationFc.tsx b/vocodes/src/v2/view/moderation/moderation_main/ModerationFc.tsx index f5dfc88c8f..15b3f2709a 100644 --- a/vocodes/src/v2/view/moderation/moderation_main/ModerationFc.tsx +++ b/vocodes/src/v2/view/moderation/moderation_main/ModerationFc.tsx @@ -20,6 +20,13 @@ function ModerationFc(props: Props) { to="/moderation/ip_bans" className="button is-link is-large is-fullwidth is-outlined" >IP Ban List + +
+ + User List

More mod controls will be added in the future: user roles, activity tracking, timed bans, account bans, etc.

diff --git a/vocodes/src/v2/view/moderation/moderation_user_list/ModerationUserList.tsx b/vocodes/src/v2/view/moderation/moderation_user_list/ModerationUserList.tsx new file mode 100644 index 0000000000..d286bbd40f --- /dev/null +++ b/vocodes/src/v2/view/moderation/moderation_user_list/ModerationUserList.tsx @@ -0,0 +1,115 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { SessionWrapper } from '../../../../session/SessionWrapper'; +import { GetUserList, GetUserListIsOk, UserForList } from '../../../api/moderation/GetUserList'; +import { formatDistance } from 'date-fns'; +import { FrontendUrlConfig } from '../../../../common/FrontendUrlConfig'; +import { GravatarFc } from '../../_common/GravatarFc'; +import { BackLink } from '../../_common/BackLink'; + +interface Props { + sessionWrapper: SessionWrapper, +} + +function ModerationUserListFc(props: Props) { + const [userList, setUserList] = useState>([]); + + const getUsers = useCallback(async () => { + const response = await GetUserList(); + + if (GetUserListIsOk(response)) { + setUserList(response.users); + } + + }, []); + + useEffect(() => { + getUsers(); + }, [getUsers]); + + + if (!userList) { + return
+ } + + if (!props.sessionWrapper.canBanUsers()) { + return

Unauthorized

; + } + + const now = new Date(); + let rows : Array = []; + + userList.forEach(user => { + //const modUserLink = `/profile/${ban.mod_username}`; + //const viewBanLink = `/moderation/ip_bans/${ban.ip_address}`; + + const createTime = new Date(user.created_at); + const relativeCreateTime = formatDistance(createTime, now, { addSuffix: true }); + + const updateTime = new Date(user.updated_at); + const relativeUpdateTime = formatDistance(updateTime, now, { addSuffix: true }); + + rows.push( + + {user.user_id} + + + +   + {user.display_name} + + + {relativeCreateTime} + {relativeUpdateTime} + {user.user_role_slug} + {user.is_banned ? "banned" : ""} + {/* + {ban.mod_username} + + {ban.mod_notes} + {relativeCreateTime} + + view / edit + */} + + ) + }); + + return ( +
+

User list

+ + + +
+
+ +

+ Brandon did the bare minimum to get this working. + It isn't paginated and will break once we get a lot of users. + It's also not sortable. + I'll need to cycle back and fix it after we launch. +

+ +
+ + + + + + + + + + + + + + {rows} + +
IdUserCreatedProfile UpdatedRoleIs Banned?
+
+ ) +} + +export { ModerationUserListFc }; diff --git a/vocodes/src/v2/view/profile/profile_edit/ProfileEditFc.tsx b/vocodes/src/v2/view/profile/profile_edit/ProfileEditFc.tsx index 1ac0354002..07648c91e0 100644 --- a/vocodes/src/v2/view/profile/profile_edit/ProfileEditFc.tsx +++ b/vocodes/src/v2/view/profile/profile_edit/ProfileEditFc.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { ApiConfig } from '../../../../common/ApiConfig'; -import { Link, useHistory } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { SessionWrapper } from '../../../../session/SessionWrapper'; import { useParams } from 'react-router-dom'; import { VisibleIconFc } from '../../_icons/VisibleIcon'; @@ -9,6 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faDiscord, faFirefox, faGithub, faTwitch, faTwitter } from '@fortawesome/free-brands-svg-icons'; import { faDollarSign, faUser } from '@fortawesome/free-solid-svg-icons'; import { GravatarFc } from '../../_common/GravatarFc'; +import { BackLink } from '../../_common/BackLink'; const DEFAULT_VISIBILITY = 'public'; @@ -205,7 +206,7 @@ function ProfileEditFc(props: Props) {

Profile & Preferences

- < Back to profile +