mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 09:28:11 +00:00
Moderation: user lists, BackLink, etc.
This commit is contained in:
1 parent
17792a670f
commit
bd2dd90a98
9 files changed
+240
-2
No files matched your search
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
{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 & Preferences</h2>
|
||||
|
||||
<Link to={viewLinkUrl}>< Back to profile</Link>
|
||||
<BackLink link={viewLinkUrl} text="Back to profile" />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Reference in new issue
Block a user