Skip to content

Commit

Permalink
Admin panel improvements (#394)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinejaussoin authored Apr 23, 2022
1 parent 790b57f commit b12be53
Show file tree
Hide file tree
Showing 34 changed files with 518 additions and 58 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ configuration.json
.docusaurus

output.txt
vulnerabilities.md
vulnerabilities.md
/esm
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ This will run a demo version, which you can turn into a fully licenced version b

## Versions History

### Version 4.15.0 (not released)

- Improve Admin dashboard for Self-Hosted, allowing the admin to add and delete users
- Fix GDPR account deletion, which did not work when the user had written messages (chat)

### Version 4.14.1 (hotfix)

- Remove CSRF code, causing random issues
Expand Down
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@retrospected/backend",
"version": "4.14.1",
"version": "4.15.0",
"license": "GNU GPLv3",
"private": true,
"scripts": {
Expand Down
5 changes: 3 additions & 2 deletions backend/src/auth/register/register-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { getIdentityByUsername, registerUser } from '../../db/actions/users';
import { canSendEmails } from '../../email/utils';

export default async function registerPasswordUser(
details: RegisterPayload
details: RegisterPayload,
skipValidation = false
): Promise<UserIdentityEntity | null> {
const existingIdentity = await getIdentityByUsername(
'password',
Expand All @@ -24,7 +25,7 @@ export default async function registerPasswordUser(
type: 'password',
username: details.username,
password: hashedPassword,
emailVerification: canSendEmails() ? v4() : undefined,
emailVerification: !skipValidation && canSendEmails() ? v4() : undefined,
language: details.language,
});

Expand Down
42 changes: 27 additions & 15 deletions backend/src/db/actions/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,33 @@ export async function deleteAccount(
}

return await transaction(async (manager) => {
await deleteVisits(manager, options.deleteSessions, user, anonymousAccount);
await deleteVotes(manager, options.deleteVotes, user, anonymousAccount);
await deletePosts(manager, options.deletePosts, user, anonymousAccount);
await deleteSessions(
manager,
options.deleteSessions,
user,
anonymousAccount
);
await deleteUserAccount(manager, user);
await delMessages(manager, options.deleteSessions, user, anonymousAccount);
await delVisits(manager, options.deleteSessions, user, anonymousAccount);
await delVotes(manager, options.deleteVotes, user, anonymousAccount);
await delPosts(manager, options.deletePosts, user, anonymousAccount);
await delSessions(manager, options.deleteSessions, user, anonymousAccount);
await delUserAccount(manager, user);
return true;
});
}

async function deleteVisits(
async function delMessages(
manager: EntityManager,
hardDelete: boolean,
user: UserView,
anon: UserIdentityEntity
) {
if (hardDelete) {
await manager.query('delete from messages where user_id = $1', [user.id]);
} else {
await manager.query('update messages set user_id = $1 where user_id = $2', [
anon.user.id,
user.id,
]);
}
}

async function delVisits(
manager: EntityManager,
hardDelete: boolean,
user: UserView,
Expand All @@ -51,7 +63,7 @@ async function deleteVisits(
}
}

async function deleteVotes(
async function delVotes(
manager: EntityManager,
hardDelete: boolean,
user: UserView,
Expand All @@ -67,7 +79,7 @@ async function deleteVotes(
}
}

async function deletePosts(
async function delPosts(
manager: EntityManager,
hardDelete: boolean,
user: UserView,
Expand Down Expand Up @@ -98,7 +110,7 @@ async function deletePosts(
}
}

async function deleteSessions(
async function delSessions(
manager: EntityManager,
hardDelete: boolean,
user: UserView,
Expand Down Expand Up @@ -138,7 +150,7 @@ async function deleteSessions(
}
}

async function deleteUserAccount(manager: EntityManager, user: UserView) {
async function delUserAccount(manager: EntityManager, user: UserView) {
await manager.query(
`
update users set default_template_id = null where default_template_id in (select id from templates where created_by_id = $1)
Expand Down
57 changes: 57 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,63 @@ db().then(() => {
}
});

app.delete('/api/user/:identityId', heavyLoadLimiter, async (req, res) => {
const user = await getUserViewFromRequest(req);
if (!user || user.email !== config.SELF_HOSTED_ADMIN) {
res
.status(403)
.send('Deleting a user is only allowed for the self-hosted admin.');
return;
}
const userToDelete = await getUserView(req.params.identityId);
if (userToDelete) {
const result = await deleteAccount(
userToDelete,
req.body as DeleteAccountPayload
);
res.status(200).send(result);
} else {
res.status(404).send('User not found');
}
});

app.post('/api/user', heavyLoadLimiter, async (req, res) => {
const user = await getUserViewFromRequest(req);
if (!user || user.email !== config.SELF_HOSTED_ADMIN) {
res
.status(403)
.send('Adding a user is only allowed for the self-hosted admin.');
return;
}
if (config.DISABLE_PASSWORD_REGISTRATION) {
res.status(403).send('Password accounts registration is disabled.');
return;
}

const registerPayload = req.body as RegisterPayload;
if (
(await getIdentityByUsername('password', registerPayload.username)) !==
null
) {
res.status(403).send('User already exists');
return;
}
const identity = await registerPasswordUser(registerPayload, true);
if (!identity) {
res.status(500).send();
} else {
const userView = await getUserView(identity.id);
if (userView) {
res.status(200).send({
loggedIn: false,
user: userView.toJson(),
});
} else {
res.status(500).send();
}
}
});

app.post('/api/validate', heavyLoadLimiter, async (req, res) => {
const validatePayload = req.body as ValidateEmailPayload;
const identity = await getPasswordIdentity(validatePayload.email);
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "docs",
"version": "4.14.1",
"version": "4.15.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@retrospected/frontend",
"version": "4.14.1",
"version": "4.15.0",
"license": "GNU GPLv3",
"private": true,
"dependencies": {
Expand Down
32 changes: 31 additions & 1 deletion frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,30 @@ interface RegisterResponse {
loggedIn: boolean;
}

export async function addUser(
name: string,
email: string,
password: string,
language: string
) {
return registerBase(name, email, password, language, `/api/user`);
}

export async function register(
name: string,
email: string,
password: string,
language: string
) {
return registerBase(name, email, password, language, `/api/register`);
}

async function registerBase(
name: string,
email: string,
password: string,
language: string,
endpoint: string
): Promise<RegisterResponse> {
const payload: RegisterPayload = {
username: email,
Expand All @@ -112,7 +131,7 @@ export async function register(
language,
};
try {
const response = await fetch(`/api/register`, {
const response = await fetch(endpoint, {
method: 'POST',
...requestConfig(),
body: JSON.stringify(payload),
Expand Down Expand Up @@ -217,6 +236,17 @@ export async function deleteAccount(
}
}

export async function deleteUser(
user: FullUser,
options: DeleteAccountPayload
): Promise<boolean> {
try {
return await fetchDelete(`/api/user/${user.identityId}`, options);
} catch (err) {
return false;
}
}

export async function getGiphyUrl(giphyId: string): Promise<string | null> {
try {
const response = await fetch(
Expand Down
24 changes: 20 additions & 4 deletions frontend/src/auth/AccountMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { logout } from '../api';
import UserContext from './Context';
import Avatar from '../components/Avatar';
import { useNavigate } from 'react-router-dom';
import { Logout, Star } from '@mui/icons-material';
import { Key, Logout, Star } from '@mui/icons-material';
import { colors, Divider, ListItemIcon, ListItemText } from '@mui/material';
import AccountCircle from '@mui/icons-material/AccountCircle';
import useIsAdmin from './useIsAdmin';

const AccountMenu = () => {
const translations = useTranslation();
Expand All @@ -24,6 +25,9 @@ const AccountMenu = () => {
const navigate = useNavigate();
const closeMenu = useCallback(() => setMenuOpen(false), []);
const openMenu = useCallback(() => setMenuOpen(true), []);
const user = useUser();
const isAdmin = useIsAdmin();
const isNotAnon = user && user.accountType !== 'anonymous';

const handleModalOpen = useCallback(
(evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
Expand All @@ -48,12 +52,16 @@ const AccountMenu = () => {
setMenuOpen(false);
}, [navigate]);

const handleAdmin = useCallback(() => {
navigate('/admin');
setMenuOpen(false);
}, [navigate]);

const handleSubscribe = useCallback(() => {
navigate('/subscribe');
setMenuOpen(false);
}, [navigate]);

const user = useUser();
if (user) {
return (
<div style={{ position: 'relative' }}>
Expand Down Expand Up @@ -81,15 +89,23 @@ const AccountMenu = () => {
<ListItemText>Go Pro!</ListItemText>
</MenuItem>
) : null}
{user && user.accountType !== 'anonymous' ? (
{isNotAnon ? (
<MenuItem onClick={handleAccount}>
<ListItemIcon>
<AccountCircle />
</ListItemIcon>
<ListItemText>{translations.Header.account}</ListItemText>
</MenuItem>
) : null}
{user && user.accountType !== 'anonymous' ? <Divider /> : null}
{isAdmin ? (
<MenuItem onClick={handleAdmin}>
<ListItemIcon>
<Key />
</ListItemIcon>
<ListItemText>{translations.Header.adminPanel}</ListItemText>
</MenuItem>
) : null}
{isAdmin || isNotAnon ? <Divider /> : null}
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<Logout />
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/auth/useIsAdmin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import useBackendCapabilities from '../global/useBackendCapabilities';
import useUser from './useUser';

export default function useIsAdmin() {
const user = useUser();
const backend = useBackendCapabilities();
return user?.email === backend.adminEmail;
}
1 change: 1 addition & 0 deletions frontend/src/translations/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
leave: 'غادر',
summaryMode: 'النّمط الملخّص',
account: undefined,
adminPanel: undefined,
},
LanguagePicker: {
header: 'إختيار اللُّغة',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
leave: 'Verlassen',
summaryMode: 'Zusammenfassungsmodus',
account: undefined,
adminPanel: undefined,
},
LanguagePicker: {
header: 'Sprache auswählen',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
leave: 'Leave',
summaryMode: 'Summary Mode',
account: 'My Account',
adminPanel: 'Administration Panel',
},
LanguagePicker: {
header: 'Choose a language',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
leave: 'Salir',
summaryMode: 'Modo resumido',
account: undefined,
adminPanel: undefined,
},
LanguagePicker: {
header: 'Escoje un idioma',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/translations/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
leave: 'Sortir',
summaryMode: 'Mode Résumé',
account: 'Mon compte',
adminPanel: 'Gestion des utilisateurs',
},
LanguagePicker: {
header: 'Changez de langue',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/translations/hu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
leave: 'Távozás',
summaryMode: 'Összesített mód',
account: undefined,
adminPanel: undefined,
},
LanguagePicker: {
header: 'Válassz nyelvet',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/translations/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default {
leave: 'Abbandona',
summaryMode: 'Modalità sommario',
account: undefined,
adminPanel: undefined,
},
LanguagePicker: {
header: 'Scegli una lingua',
Expand Down
Loading

0 comments on commit b12be53

Please sign in to comment.