1
0
Fork 0

feat(org): Added restrict/remove account

This commit is contained in:
joostdecock 2023-08-20 18:04:26 +02:00
parent 5d1249c419
commit 2b4d5c240a
9 changed files with 338 additions and 8 deletions

View file

@ -141,6 +141,30 @@ UsersController.prototype.exportAccount = async (req, res, tools) => {
return User.sendResponse(res) return User.sendResponse(res)
} }
/*
* Restricts processing of account data
*
* See: https://freesewing.dev/reference/backend/api
*/
UsersController.prototype.restrictAccount = async (req, res, tools) => {
const User = new UserModel(tools)
await User.restrictAccount(req)
return User.sendResponse(res)
}
/*
* Remove account
*
* See: https://freesewing.dev/reference/backend/api
*/
UsersController.prototype.removeAccount = async (req, res, tools) => {
const User = new UserModel(tools)
await User.removeAccount(req)
return User.sendResponse(res)
}
/* /*
* Checks whether a submitted username is available * Checks whether a submitted username is available
* *

View file

@ -1,7 +1,7 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { log } from '../utils/log.mjs' import { log } from '../utils/log.mjs'
import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs' import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs'
import { replaceImage, importImage } from '../utils/cloudflare-images.mjs' import { replaceImage, importImage, removeImage } from '../utils/cloudflare-images.mjs'
import { clean, asJson, i18nUrl, writeExportedData } from '../utils/index.mjs' import { clean, asJson, i18nUrl, writeExportedData } from '../utils/index.mjs'
import { decorateModel } from '../utils/model-decorator.mjs' import { decorateModel } from '../utils/model-decorator.mjs'
@ -110,6 +110,76 @@ UserModel.prototype.exportAccount = async function ({ user }) {
}) })
} }
/*
* Restricts processing of account data
*
* @param {user} object - The user as loaded by the authentication middleware
* @returns {UserModel} object - The UserModel
*/
UserModel.prototype.restrictAccount = async function ({ user }) {
/*
* Read the record from the database
*/
await this.read({ id: user.uid }, { apikeys: true, bookmarks: true, patterns: true, sets: true })
/*
* If it does not exist, return 404
*/
if (!this.exists) return this.setResponse(404)
/*
* Update status to block the account
*/
await this.update({ status: -1 })
return this.setResponse200({
result: 'success',
data: {},
})
}
/*
* Remove account
*
* @param {user} object - The user as loaded by the authentication middleware
* @returns {UserModel} object - The UserModel
*/
UserModel.prototype.removeAccount = async function ({ user }) {
/*
* Read the record from the database
*/
await this.read({ id: user.uid }, { apikeys: true, bookmarks: true, patterns: true, sets: true })
/*
* If it does not exist, return 404
*/
if (!this.exists) return this.setResponse(404)
/*
* Remove user image
*/
await removeImage(`user-${this.record.ihash}`)
/*
* Remove account
*/
try {
await this.prisma.pattern.deleteMany({ where: { userId: user.uid } })
await this.prisma.set.deleteMany({ where: { userId: user.uid } })
await this.prisma.bookmark.deleteMany({ where: { userId: user.uid } })
await this.prisma.apikey.deleteMany({ where: { userId: user.uid } })
await this.prisma.confirmation.deleteMany({ where: { userId: user.uid } })
await this.delete()
} catch (err) {
log.warn(err, 'Error while removing account')
}
return this.setResponse200({
result: 'success',
data: {},
})
}
/* /*
* Loads a user from the database based on the where clause you pass it * Loads a user from the database based on the where clause you pass it
* In addition prepares it for returning the account data * In addition prepares it for returning the account data

View file

@ -73,6 +73,23 @@ export function usersRoutes(tools) {
app.get('/account/export/key', passport.authenticate(...bsc), (req, res) => app.get('/account/export/key', passport.authenticate(...bsc), (req, res) =>
Users.exportAccount(req, res, tools) Users.exportAccount(req, res, tools)
) )
// Restrict processing of account data
app.get('/account/restrict/jwt', passport.authenticate(...jwt), (req, res) =>
Users.restrictAccount(req, res, tools)
)
app.get('/account/restrict/key', passport.authenticate(...bsc), (req, res) =>
Users.restrictAccount(req, res, tools)
)
// Remove account
app.delete('/account/jwt', passport.authenticate(...jwt), (req, res) =>
Users.removeAccount(req, res, tools)
)
app.delete('/account/key', passport.authenticate(...bsc), (req, res) =>
Users.removeAccount(req, res, tools)
)
/* /*
// Remove account // Remove account

View file

@ -0,0 +1,59 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as reloadNs } from 'shared/components/account/reload.mjs'
// Translation namespaces used on this page
const ns = nsMerge(reloadNs, authNs, pageNs)
/*
* Some things should never generated as SSR
* So for these, we run a dynamic import and disable SSR rendering
*/
const DynamicAuthWrapper = dynamic(
() => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper),
{ ssr: false }
)
const DynamicRemove = dynamic(
() => import('shared/components/account/remove.mjs').then((mod) => mod.RemoveAccount),
{ ssr: false }
)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountRemovePage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('remove')}>
<DynamicAuthWrapper>
<DynamicRemove />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountRemovePage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'remove'],
},
},
}
}

View file

@ -0,0 +1,59 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as reloadNs } from 'shared/components/account/reload.mjs'
// Translation namespaces used on this page
const ns = nsMerge(reloadNs, authNs, pageNs)
/*
* Some things should never generated as SSR
* So for these, we run a dynamic import and disable SSR rendering
*/
const DynamicAuthWrapper = dynamic(
() => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper),
{ ssr: false }
)
const DynamicRestrict = dynamic(
() => import('shared/components/account/restrict.mjs').then((mod) => mod.RestrictAccount),
{ ssr: false }
)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountRestrictPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('restrict')}>
<DynamicAuthWrapper>
<DynamicRestrict />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountRestrictPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'restrict'],
},
},
}
}

View file

@ -56,6 +56,10 @@ restrict: Restrict processing of your data
disable: Disable your account disable: Disable your account
remove: Remove your account remove: Remove your account
proceedWithCaution: Proceed with caution
restrictWarning: While no data will be removed, this will disable your account. Furthermore, you can not undo this on your own, but will have to contact support when you want to restore access to your account.
noWayBack: There is no way back from this.
mdSupport: You can use markdown here mdSupport: You can use markdown here
or: or or: or
continue: Continue continue: Continue

View file

@ -0,0 +1,43 @@
// Dependencies
import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { WebLink } from 'shared/components/web-link.mjs'
export const ns = ['account', 'status']
export const RemoveAccount = () => {
// Hooks
const { setAccount, token, signOut } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation(ns)
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
// Helper method to export account
const removeAccount = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.removeAccount()
if (result.success) {
setLoadingStatus([true, 'nailedIt', true, true])
signOut()
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
<LoadingStatus />
<Popout warning>
<h3>{t('noWayBack')}</h3>
<button className="btn btn-error capitalize w-full my-2" onClick={removeAccount}>
{t('remove')}
</button>
</Popout>
<BackToAccountButton />
</div>
)
}

View file

@ -0,0 +1,42 @@
// Dependencies
import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { WebLink } from 'shared/components/web-link.mjs'
export const ns = ['account', 'status']
export const RestrictAccount = () => {
// Hooks
const { setAccount, token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation(ns)
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
// Helper method to export account
const restrictAccount = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.restrictAccount()
if (result.success) setLoadingStatus([true, 'nailedIt', true, true])
else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
<LoadingStatus />
<Popout warning>
<h5>{t('proceedWithCaution')}</h5>
<p className="text-lg">{t('restrictWarning')}</p>
<button className="btn btn-error capitalize w-full my-2" onClick={restrictAccount}>
{t('restrict')}
</button>
</Popout>
<BackToAccountButton />
</div>
)
}

View file

@ -152,37 +152,49 @@ Backend.prototype.confirmMfa = async function (data) {
* Disable MFA * Disable MFA
*/ */
Backend.prototype.disableMfa = async function (data) { Backend.prototype.disableMfa = async function (data) {
return responseHandler( return responseHandler(await api.post(`/account/mfa/jwt`, { ...data, mfa: false }, this.auth))
await await api.post(`/account/mfa/jwt`, { ...data, mfa: false }, this.auth)
)
} }
/* /*
* Reload account * Reload account
*/ */
Backend.prototype.reloadAccount = async function () { Backend.prototype.reloadAccount = async function () {
return responseHandler(await await api.get(`/whoami/jwt`, this.auth)) return responseHandler(await api.get(`/whoami/jwt`, this.auth))
} }
/* /*
* Export account data * Export account data
*/ */
Backend.prototype.exportAccount = async function () { Backend.prototype.exportAccount = async function () {
return responseHandler(await await api.get(`/account/export/jwt`, this.auth)) return responseHandler(await api.get(`/account/export/jwt`, this.auth))
}
/*
* Restrict processing of account data
*/
Backend.prototype.restrictAccount = async function () {
return responseHandler(await api.get(`/account/restrict/jwt`, this.auth))
}
/*
* Remove account
*/
Backend.prototype.restrictAccount = async function () {
return responseHandler(await api.delete(`/account/jwt`, this.auth))
} }
/* /*
* Load all user data * Load all user data
*/ */
Backend.prototype.getUserData = async function (uid) { Backend.prototype.getUserData = async function (uid) {
return responseHandler(await await api.get(`/users/${uid}/jwt`, this.auth)) return responseHandler(await api.get(`/users/${uid}/jwt`, this.auth))
} }
/* /*
* Load user profile * Load user profile
*/ */
Backend.prototype.getProfile = async function (uid) { Backend.prototype.getProfile = async function (uid) {
return responseHandler(await await api.get(`/users/${uid}`)) return responseHandler(await api.get(`/users/${uid}`))
} }
/* /*