Moderation: user lists, BackLink, etc.

This commit is contained in:
Brandon Thomas committed 2021-09-06 03:57:11 -04:00
1 parent 17792a670f
commit bd2dd90a98
9 files changed
+240 -2

No files matched your search

+4
View File
@@ -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";
}
+6
View File
@@ -79,6 +79,12 @@ class FrontendUrlConfig {
static userProfileBanPage(userDisplayName: string) : string {
return `/profile/${userDisplayName}/ban`;
}
// Moderation links
static moderationMain() : string {
return '/moderation';
}
}
export { FrontendUrlConfig }
@@ -0,0 +1,68 @@
import { ApiConfig } from "../../../common/ApiConfig";
//export interface GetUserListRequest {
//}
export interface GetUserListSuccessResponse {
success: boolean,
users: Array<UserForList>
}
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<GetUserListResponse>
{
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 };
});
}
@@ -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<Props, State> {
/>
</Route>
<Route path="/moderation/user/list">
<ModerationUserListFc
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/ip_bans/:ipAddress">
<ModerationViewIpBanFc
sessionWrapper={this.props.sessionWrapper}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { BackButtonIcon } from '../_icons/BackButtonIcon';
interface Props {
link: string,
text?: string,
}
function BackLink(props: Props) {
const linkText = props.text === undefined ? 'Back' : props.text;
return (
<Link to={props.link}><BackButtonIcon title={linkText} /> {linkText}</Link>
)
}
export { BackLink };
@@ -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 (
<FontAwesomeIcon icon={faAngleLeft} title={props.title} />
);
}
@@ -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</Link>
<br />
<Link
to="/moderation/user/list"
className="button is-link is-large is-fullwidth is-outlined"
>User List</Link>
<br />
<p>More mod controls will be added in the future: user roles, activity tracking, timed bans, account bans, etc.</p>
@@ -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<Array<UserForList>>([]);
const getUsers = useCallback(async () => {
const response = await GetUserList();
if (GetUserListIsOk(response)) {
setUserList(response.users);
}
}, []);
useEffect(() => {
getUsers();
}, [getUsers]);
if (!userList) {
return <div />
}
if (!props.sessionWrapper.canBanUsers()) {
return <h1>Unauthorized</h1>;
}
const now = new Date();
let rows : Array<JSX.Element> = [];
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(
<tr key={user.user_token}>
<td>{user.user_id}</td>
<td>
<Link to={FrontendUrlConfig.userProfilePage(user.display_name)}>
<GravatarFc username={user.display_name} email_hash={user.gravatar_hash} size={12} />
&nbsp;
{user.display_name}
</Link>
</td>
<td>{relativeCreateTime}</td>
<td>{relativeUpdateTime}</td>
<td>{user.user_role_slug}</td>
<td>{user.is_banned ? "banned" : ""}</td>
{/*<td>
<Link to={modUserLink}>{ban.mod_username}</Link>
</td>
<td>{ban.mod_notes}</td>
<td>{relativeCreateTime}</td>
<td>
<Link to={viewBanLink}>view / edit</Link>
</td>*/}
</tr>
)
});
return (
<div>
<h1 className="title is-1"> User list </h1>
<BackLink link={FrontendUrlConfig.moderationMain()} text="Back to moderation" />
<br />
<br />
<p>
Brandon did the <strong>bare minimum</strong> 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.
</p>
<br />
<table className="table">
<thead>
<tr>
<th>Id</th>
<th>User</th>
<th>Created</th>
<th>Profile Updated</th>
<th>Role</th>
<th>Is Banned?</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
)
}
export { ModerationUserListFc };
@@ -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) {
<div>
<h2 className="subtitle is-2">Profile &amp; Preferences</h2>
<Link to={viewLinkUrl}>&lt; Back to profile</Link>
<BackLink link={viewLinkUrl} text="Back to profile" />
<br />
<br />