Backend: Update user-delete

This commit is contained in:
ADAMJR 2022-12-19 21:43:28 +00:00
parent b67ae3166d
commit 931b7e0b66
22 changed files with 1778 additions and 1377 deletions

View File

@ -8,8 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
### Changed
- Moved advanced user settings to 'Advanced' tab
- Moved advanced guild settings to 'Advanced' tab
### Fixed
- Emails, and passwords salts/hashes are now forgotten when deleting user is deleted
## [Winter 0.4.0-alpha] - 2022/12/17

View File

@ -22,17 +22,6 @@ router.get('/count', async (req, res) => {
res.json(count);
});
router.delete('/:id', updateUser, validateUser, async (req, res) => {
const user = res.locals.user;
user.username = `deleted-user-${generateInvite(6)}`;
user.discriminator = 0;
delete user.salt;
delete user.hash;
await user.save();
res.status(201).json({ message: 'Modified' });
});
router.get('/check-username', async (req, res) => {
const username = req.query.value?.toString().toLowerCase();
const exists = await User.exists({

View File

@ -2,7 +2,7 @@ import { WSEvent } from './ws-event';
import { Socket } from 'socket.io';
import { WebSocket } from '../websocket';
import generateInvite from '../../data/utils/generate-invite';
import { WS } from '@acrd/types';
import { Entity, UserTypes, WS } from '@acrd/types';
export default class implements WSEvent<'USER_DELETE'> {
public on = 'USER_DELETE' as const;
@ -15,6 +15,9 @@ export default class implements WSEvent<'USER_DELETE'> {
discriminator: 0,
locked: true,
username: `Deleted User ${generateInvite(6)}`,
email: generateInvite(16),
salt: null,
hash: null,
};
await user.updateOne(partialUser);

View File

@ -21,9 +21,9 @@ const CreateInvite: React.FunctionComponent = () => {
}, [isOpen]);
const copyCode = () => window.navigator.clipboard.writeText(activeInvite!.id);
return (activeInvite) ? (
<Modal typeName={'CreateInvite'} className="p-5">
<Modal typeName={'CreateInvite'} className="p-5" >
<header className="mb-3">
<h1 className="font-bold inline uppercase">Invite Friends to {activeGuild?.name}</h1>
</header>
@ -43,5 +43,5 @@ const CreateInvite: React.FunctionComponent = () => {
</Modal>
) : null;
}
export default CreateInvite;

View File

@ -1,3 +1,12 @@
.invite:nth-child(even) {
th,
td {
padding-left: 7.5px;
}
th {
text-align: left;
}
tr:nth-child(even) {
background-color: var(--bg-secondary);
}

View File

@ -3,7 +3,7 @@ import { useParams } from 'react-router';
import { fetchGuildInvites, getGuild, getGuildInvites, getGuildUsers } from '../../../store/guilds';
import { deleteInvite } from '../../../store/invites';
import { openSaveChanges } from '../../../store/ui';
import Username from '../../user/username';
import FoundUsername from '../../user/username';
import CircleButton from '../../utils/buttons/circle-button';
import './guild-settings-invites.scoped.css';
@ -17,29 +17,33 @@ const GuildSettingsInvites: React.FunctionComponent = () => {
dispatch(fetchGuildInvites(guildId));
const Invites = () => (
<div className="mt-2">
{invites.filter(x => x).map(i => (
<div className="flex align-center justify-between invite w-full p-2">
<code className='font-bold pt-2'>{i.id}</code>
<span className="ml-4 secondary">
<span className='ml-4'>Used <code>{i.uses}</code> times</span>
</span>
<span className='ml-4'>Created by
<Username
<table className="mt-2">
<tr>
<th>Code</th>
<th>Used</th>
<th>Creator</th>
</tr>
{invites.map(i => (
<tr key={i.code} className="invite">
<td><code className='font-bold primary'>{i.id}</code></td>
<td><code className='tertiary'>{i.uses}</code> times</td>
<td>
<FoundUsername
size='sm'
className='h-full'
user={guildUsers.find(gu => gu.id == i.inviterId)}
guild={guild} />
</span>
<span className="justify-end">
</td>
<td className='w-0'>
<CircleButton
type="button"
style={{ borderColor: 'var(--danger)', color: 'var(--danger)' }}
onClick={() => dispatch(deleteInvite(i.id))}>X</CircleButton>
</span>
</div>
</td>
</tr>
))}
{!invites.length && <span>No invites created.</span>}
</div>
</table>
);
return (

View File

@ -7,7 +7,8 @@ import Category from '../../utils/category';
import SaveChanges from '../../utils/save-changes';
import Input from '../../inputs/input';
import ChannelSelect from '../../inputs/channel-select';
import CircleButton from '../../utils/buttons/circle-button';
const GuildSettingsOverview: React.FunctionComponent = () => {
const dispatch = useDispatch();
const guild = useSelector((s: Store.AppState) => s.ui.activeGuild)!;
@ -22,7 +23,7 @@ const GuildSettingsOverview: React.FunctionComponent = () => {
const confirmation = window.confirm('Are you sure you want to delete this guild?');
if (confirmation) dispatch(deleteGuild(guild.id));
}
return (
<form
onChange={() => dispatch(openSaveChanges(true))}
@ -30,7 +31,7 @@ const GuildSettingsOverview: React.FunctionComponent = () => {
<header>
<h1 className="text-xl font-bold inline">Guild Overview</h1>
</header>
<section className="w-1/2">
<Input
label="Name"
@ -64,18 +65,18 @@ const GuildSettingsOverview: React.FunctionComponent = () => {
title="Advanced Settings" />
<section>
<NormalButton
type="button"
<CircleButton
onClick={onDelete}
className="bg-danger">Delete</NormalButton>
style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}
className="border-danger red m-2">Delete</CircleButton>
</section>
<SaveChanges
setValue={setValue}
onSave={onSave}
obj={guild} />
</form>
</form>
);
}
export default GuildSettingsOverview;

View File

@ -0,0 +1,5 @@
.modal {
position: relative;
top: 50%;
transform: translateY(-50%);
}

View File

@ -3,6 +3,7 @@ import { useSnackbar } from 'notistack';
import ReactModal from 'react-modal'
import { useDispatch, useSelector } from 'react-redux';
import { closeModal } from '../../store/ui';
import './modal.scoped.css';
export interface ModalProps {
typeName: string;
@ -18,7 +19,7 @@ const sizeClass = {
'xl': 'rounded-lg 2xl:w-1/2 2xl:inset-x-1/4 2xl:top-1/4 md:w-1/3 md:inset-x-1/3 md:top-20',
'full': 'h-full w-full',
};
const Modal: React.FunctionComponent<ModalProps> = ({ className, typeName, size, children }) => {
const dispatch = useDispatch();
const openModal = useSelector((s: Store.AppState) => s.ui.openModal);
@ -27,7 +28,7 @@ const Modal: React.FunctionComponent<ModalProps> = ({ className, typeName, size,
return (
<ReactModal
className={classNames(
`bg-bg-primary overflow-auto fixed outline-none`,
`modal bg-bg-primary overflow-auto fixed outline-none`,
className,
sizeClass[size ?? 'sm'],
)}
@ -39,5 +40,5 @@ const Modal: React.FunctionComponent<ModalProps> = ({ className, typeName, size,
}}>{children}</ReactModal>
);
}
export default Modal;

View File

@ -7,7 +7,7 @@ import { FunctionComponent, useState } from 'react';
import { useSelector, useStore } from 'react-redux';
import { getGuild } from '../../store/guilds';
import SidebarIcon from '../navigation/sidebar/sidebar-icon';
import Username from '../user/username';
import FoundUsername from '../user/username';
import Category from '../utils/category';
import NavTabs from '../utils/nav-tabs';
import Modal from './modal';
@ -128,7 +128,7 @@ const UserProfile: FunctionComponent = () => {
size="md">
<header className="bg-bg-tertiary">
<div className="p-5">
<Username size="lg" user={user} />
<FoundUsername size="lg" user={user} />
<UserBadges />
</div>
<hr className="border-bg-primary" />

View File

@ -0,0 +1,57 @@
import { useDispatch, useSelector } from 'react-redux';
import Category from '../../utils/category';
import CircleButton from '../../utils/buttons/circle-button';
import Toggle from '../../inputs/toggle';
import { deleteSelf } from '../../../store/users';
import { toggleDevMode } from '../../../store/config';
const UserSettingsAdvanced: React.FunctionComponent = () => {
const dispatch = useDispatch();
const selfUser = useSelector((s: Store.AppState) => s.auth.user);
const devMode = useSelector((s: Store.AppState) => s.config.devMode);
const requestDelete = () => {
const confirmation = window.prompt(
`WARNING: You are about to delete account '${selfUser.username}'\n` +
`Type \'accord\' to confirm deletion.`
);
if (confirmation == 'accord')
dispatch(deleteSelf());
};
return (
<div className="flex flex-col pt-14 px-10 pb-20 h-full mt-1">
<header>
<h1 className="text-xl font-bold inline">Advanced Settings</h1>
</header>
<Category
className="py-2 mt-5"
title="Advanced Settings" />
<section>
<div className="w-1/2 pb-5">
<label htmlFor="devMode">Dev Mode</label>
<Toggle
onChange={(e) => e.stopPropagation()}
onClick={() => dispatch(toggleDevMode())}
checked={devMode}
className="float-right"
id="devMode" />
</div>
<Category
className="py-2 mt-5"
title="Delete Account" />
<CircleButton
id="deleteUserButton"
onClick={requestDelete}
style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}
className="border-danger red m-2">Delete</CircleButton>
</section>
</div>
);
}
export default UserSettingsAdvanced;

View File

@ -1,12 +1,8 @@
import { useForm } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { toggleDevMode } from '../../../store/config';
import { openSaveChanges } from '../../../store/ui';
import { updateSelf, deleteSelf, uploadUserAvatar } from '../../../store/users';
import NormalButton from '../../utils/buttons/normal-button';
import Category from '../../utils/category';
import { updateSelf, uploadUserAvatar } from '../../../store/users';
import Input from '../../inputs/input';
import Toggle from '../../inputs/toggle';
import SaveChanges from '../../utils/save-changes';
import FileInput from '../../inputs/file-input';
@ -14,16 +10,11 @@ const UserSettingsOverview: React.FunctionComponent = () => {
const dispatch = useDispatch();
const user = useSelector((s: Store.AppState) => s.auth.user)!;
const { register, handleSubmit, setValue } = useForm();
const devMode = useSelector((s: Store.AppState) => s.config.devMode);
const onSave = (e) => {
const onUpdate = (payload) => dispatch(updateSelf(payload));
handleSubmit(onUpdate)(e);
};
const onDelete = () => {
const confirmation = window.confirm('Are you sure you want to delete your user?');
if (confirmation) dispatch(deleteSelf());
}
return (
<div className="flex flex-col pt-14 px-10 pb-20 h-full mt-1">
@ -59,28 +50,6 @@ const UserSettingsOverview: React.FunctionComponent = () => {
onSave={onSave}
obj={user} />
</form>
<Category
className="py-2 mt-5"
title="Advanced Settings" />
<section>
<div className="w-1/2 pb-5">
<label htmlFor="devMode">Dev Mode</label>
<Toggle
onChange={(e) => e.stopPropagation()}
onClick={() => dispatch(toggleDevMode())}
checked={devMode}
className="float-right"
id="devMode" />
</div>
<NormalButton
id="deleteUserButton"
role="button"
onClick={handleSubmit(onDelete)}
className="bg-danger">Delete</NormalButton>
</section>
</div>
);
}

View File

@ -5,6 +5,7 @@ import EscButton from '../../utils/buttons/esc-button';
import Category from '../../utils/category';
import NavTabs from '../../utils/nav-tabs';
import Modal from '../modal';
import UserSettingsAdvanced from './user-settings-advanced';
import UserSettingsOverview from './user-settings-overview';
import UserSettingsSecurity from './user-settings-security';
import UserSettingsThemes from './user-settings-themes';
@ -32,6 +33,7 @@ const UserSettings: React.FunctionComponent = () => {
{ name: 'Overview', id: 'overview' },
{ name: 'Security', id: 'security' },
{ name: <>Themes <NewBadge /></>, id: 'themes' },
{ name: 'Advanced', id: 'advanced' },
]} />
<div className="rounded-sm bg-bg-modifier-accent h-px w-42 my-2 mx-2.5 " />
@ -45,6 +47,7 @@ const UserSettings: React.FunctionComponent = () => {
{tab === 'overview' && <UserSettingsOverview />}
{tab === 'themes' && <UserSettingsThemes />}
{tab === 'security' && <UserSettingsSecurity />}
{tab === 'advanced' && <UserSettingsAdvanced />}
</div>
<div className="col-span-2 h-full">

View File

@ -7,10 +7,10 @@ import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import usePerms from '../../../hooks/use-perms';
import { getChannelUsers, joinVoiceChannel } from '../../../store/channels';
import { getGuild, getGuildChannels } from '../../../store/guilds';
import { getGuildChannels } from '../../../store/guilds';
import { actions as ui } from '../../../store/ui';
import ChannelMenu from '../../ctx-menus/channel-menu';
import Username from '../../user/username';
import FoundUsername from '../../user/username';
import React from 'react';
import { Entity } from '@acrd/types';
@ -41,7 +41,7 @@ const ChannelTabs: React.FunctionComponent = () => {
return <div className="p-2 pl-3">{users.map(u =>
<ContextMenuTrigger key={u.id} id={u.id}>
<div className="mb-1">
<Username user={u} size="sm" guild={activeGuild} />
<FoundUsername user={u} size="sm" guild={activeGuild} />
</div>
</ContextMenuTrigger>
)}</div>;

View File

@ -1,4 +1,4 @@
import Username from '../../user/username';
import FoundUsername from '../../user/username';
import { useDispatch, useSelector } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCog, faPhoneSlash, faSignal } from '@fortawesome/free-solid-svg-icons';
@ -51,7 +51,7 @@ const SidebarFooter: React.FunctionComponent = () => {
<VoiceFooter />
<div className="relative flex items-center py-2">
<div className="select-all">
<Username user={user} />
<FoundUsername user={user} />
</div>
<FontAwesomeIcon
id="userSettingsButton"

View File

@ -10,7 +10,7 @@ import { fetchInvite, getInvite } from '../../store/invites';
import { joinGuild } from '../../store/members';
import { getTag, getUser } from '../../store/users';
import SidebarIcon from '../navigation/sidebar/sidebar-icon';
import Username from '../user/username';
import FoundUsername from '../user/username';
import NormalButton from '../utils/buttons/normal-button';
import FullParticles from '../utils/full-particles';
import PageWrapper from './page-wrapper';
@ -76,7 +76,7 @@ const InvitePage: React.FunctionComponent<InvitePageProps> = () => {
</span>
<span className='text-center'>
<div className="heading font-bold">Owned By</div>
<Username user={ownerUser} />
<FoundUsername user={ownerUser} />
</span>
</div>
</div>

View File

@ -1,5 +1,5 @@
import Category from '../utils/category';
import Username from './username';
import FoundUsername from './username';
import { useSelector } from 'react-redux';
import { ContextMenuTrigger } from 'react-contextmenu';
import GuildMemberMenu from '../ctx-menus/guild-member/guild-member-menu';
@ -38,7 +38,7 @@ const MemberList: React.FunctionComponent = () => {
id={u.id}
key={u.id}>
<div className="m-2">
<Username guild={guild} user={u} />
<FoundUsername guild={guild} user={u} />
</div>
<GuildMemberMenu user={u} />
</ContextMenuTrigger>

View File

@ -13,13 +13,23 @@ export interface UsernameProps {
className?: string;
}
const Username: React.FunctionComponent<UsernameProps> = ({ guild, user, className, size = 'md' }) => {
const highestRole = useSelector(getMemberHighestRole(guild?.id, user?.id));
export const Username: React.FunctionComponent<UsernameProps> = (props) =>
(props.user)
? FoundUsername(props)
: FoundUsername({
...props,
user: {
id: '0',
username: 'Unknown User',
discriminator: 0,
} as Partial<Entity.User>,
} as any);
const FoundUsername: React.FunctionComponent<UsernameProps> = ({ guild, user, className, size = 'md' }) => {
const highestRole = useSelector(getMemberHighestRole(guild?.id, user.id));
const userOwnsGuild = (guild?.ownerId === user.id);
const discrim = user.discriminator
.toString()
.padStart(4, '0');
const discrim = user.discriminator.toString().padStart(4, '0');
const isOnline = (user.status === 'ONLINE');
const UserPresence = () => {

View File

@ -9,5 +9,5 @@ const CircleButton: React.FunctionComponent<any> = (props) => {
props.className)}>{props.children}</button>
);
}
export default CircleButton;

View File

@ -29,17 +29,16 @@ const WSListener: React.FunctionComponent = () => {
const state = () => store.getState() as Store.AppState;
// TODO: make alphabetical order
useEffect(() => {
if (hasListened) return;
if (hasListened) return;
const handleDialog = (dialog: Dialog) =>
const handleDialog = (dialog: Dialog) =>
enqueueSnackbar(`${dialog.content}.`, {
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
variant: dialog.variant,
autoHideDuration: 5000,
});
ws.on('error', (error: any) => handleDialog({
variant: 'error',
content: error.data?.message ?? error.message,
@ -56,9 +55,9 @@ const WSListener: React.FunctionComponent = () => {
ws.on('CHANNEL_CREATE', (args) => {
// if we created it, we want to navigate there
// we'd expect the user to exist, as they should be logged in to receive ws events
const { auth, ui } = state();
const { auth, ui } = state();
const selfCreated = args.creatorId === auth.user!.id;
// we cannot go to the channel if not in store
dispatch(channels.created(args));
@ -105,7 +104,7 @@ const WSListener: React.FunctionComponent = () => {
ws.on('GUILD_MEMBER_ADD', (args) => {
// we not getting other users when joining guild
dispatch(users.fetched([args.user]));
dispatch(members.added(args));
dispatch(members.added(args));
});
ws.on('GUILD_MEMBER_UPDATE', (args) => dispatch(members.updated(args)));
// user may be in mutual guilds, and therefore not removed from global user cache
@ -125,7 +124,7 @@ const WSListener: React.FunctionComponent = () => {
if (isBlocked) return;
dispatch(messages.created(args));
const { channelId } = args.message;
const { activeChannel } = state().ui;
if (activeChannel && activeChannel.id !== channelId)
@ -151,7 +150,7 @@ const WSListener: React.FunctionComponent = () => {
history.push('/');
dispatch(logoutUser());
});
ws.on('USER_UPDATE', (args) => {
ws.on('USER_UPDATE', (args) => {
dispatch(auth.updatedUser(args));
dispatch(users.updated(args));
});
@ -170,8 +169,8 @@ const WSListener: React.FunctionComponent = () => {
dispatch(meta.listenedToWS());
}, [hasListened]);
return null;
}
export default WSListener;

View File

@ -74,4 +74,13 @@ nav[role='menu'] {
width: 100%;
height: 100%;
z-index: -1;
}
.text-center-v {
line-height: 225%;
height: 100%;
}
.w-0 {
width: 0;
}

File diff suppressed because it is too large Load Diff