feat(org): Added restrict/remove account
This commit is contained in:
parent
5d1249c419
commit
2b4d5c240a
9 changed files with 338 additions and 8 deletions
|
@ -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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
59
sites/org/pages/account/remove.mjs
Normal file
59
sites/org/pages/account/remove.mjs
Normal 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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
59
sites/org/pages/account/restrict.mjs
Normal file
59
sites/org/pages/account/restrict.mjs
Normal 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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
43
sites/shared/components/account/remove.mjs
Normal file
43
sites/shared/components/account/remove.mjs
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
42
sites/shared/components/account/restrict.mjs
Normal file
42
sites/shared/components/account/restrict.mjs
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue