Skip to content

Commit

Permalink
[SOA-48] Ability to delete account (#49)
Browse files Browse the repository at this point in the history
* feat 🎸 (be): destroy user action

* fix βœ… (be): delete cascade user_posts

* fix βœ… (be): delete user sessions as well

* styles πŸ’… : add user deletion action

* refactor ✨ : user related routes revision

* test πŸ§ͺ (browser): add browser test for the user profile deletion
  • Loading branch information
mariadriana-deemaze authored Nov 29, 2024
1 parent 4af0381 commit 43f68c8
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 19 deletions.
17 changes: 14 additions & 3 deletions app/controllers/users_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import { errorsReducer } from '#utils/index'
import { UserService } from '#services/user_service'
import { UserResponse } from '#interfaces/user'
import { PageObject } from '@adonisjs/inertia/types'
import AuthService from '#services/auth_service'

@inject()
export default class UsersController {
constructor(private readonly service: UserService) {}
constructor(
private readonly authService: AuthService,
private readonly service: UserService
) {}

async index(ctx: HttpContext) {
const searchTerm = ctx.request.qs().search || ''
Expand Down Expand Up @@ -57,7 +61,14 @@ export default class UsersController {
}
}

async destroy() {
// TODO: Implement.
async destroy(ctx: HttpContext) {
const user = ctx.auth.user!
try {
await this.service.destroy(user)
await this.authService.destroy(ctx)
} catch (error) {
ctx.session.flash('errors', { message: 'Error deleting user.' })
return ctx.response.redirect().back()
}
}
}
4 changes: 4 additions & 0 deletions app/services/user_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export class UserService {
await user.save()
}

async destroy(user: User) {
await user.delete()
}

async storeAttachments(ctx: HttpContext) {
const currentUserId = ctx.auth.user?.id!

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BaseSchema } from '@adonisjs/lucid/schema'

export default class extends BaseSchema {
protected tableName = 'posts'

async up() {
this.schema.alterTable(this.tableName, (table) => {
table.dropForeign('user_id');
table.uuid('user_id').references('users.id').notNullable().onDelete('CASCADE').alter()
})
}

async down() {
this.schema.alterTable(this.tableName, (table) => {
table.dropForeign('user_id');
table.uuid('user_id').references('users.id').notNullable().alter()
})
}
}
73 changes: 69 additions & 4 deletions inertia/pages/users/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import HeadOG from '@/components/generic/head_og'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useToast } from '@/components/ui/use_toast'
Expand All @@ -23,14 +31,21 @@ export default function UserSettings({
() => user.attachments.avatar?.link || undefined
)
const [coverPreview, setCoverPreview] = useState(() => user.attachments.cover?.link || undefined)
const [deleteIntentModal, setDeleteIntentModal] = useState(false)

if (!user) return <></>

const { props } = usePage()

const { toast } = useToast()

const { data, setData, patch, processing } = useForm<{
const {
data,
setData,
patch,
delete: deleteReq,
processing,
} = useForm<{
name: string
surname: string
username: string
Expand All @@ -51,7 +66,7 @@ export default function UserSettings({

function handleSubmit(e: React.FormEvent) {
e.preventDefault()
patch(route('users.update', { params: { id: user?.id! } }).path, {
patch(route('users.update').path, {
preserveState: true,
preserveScroll: true,
onSuccess: () => {
Expand Down Expand Up @@ -86,9 +101,20 @@ export default function UserSettings({
fileReader.readAsDataURL(e.target.files[0])
}

function deleteAccount() {
deleteReq(route('users.destroy').path, {
preserveState: true,
preserveScroll: true,
data: undefined,
onSuccess: () => {
toast({ title: 'Account succesfully deleted.' })
},
})
}

useEffect(() => {
if (props?.errors && Object.entries(props.errors).length) {
toast({ title: 'Error updating profile details.' })
toast({ title: props?.errors.message || 'Error updating profile details.' })
}
}, [props?.errors])

Expand All @@ -99,7 +125,7 @@ export default function UserSettings({
description="Your profile settings on Social Adonis."
url={route('settings.show').path}
/>
<div className="relative flex flex-col pt-0 w-full">
<div className="relative flex flex-col pt-0 w-full items-center gap-4 pb-5">
<form className="flex flex-col items-center gap-4 w-full" onSubmit={handleSubmit}>
<div className="relative bg-slate-300 h-64 w-full rounded-2xl mb-20 shadow-inner">
<div className="w-full h-full rounded-2xl overflow-hidden">
Expand Down Expand Up @@ -231,6 +257,45 @@ export default function UserSettings({
</CardContent>
</Card>
</form>

<Card className="flex flex-col lg:flex-row justify-between items-center bg-gray-100 w-full max-w-screen-md border-red-100">
<CardHeader className="text-center lg:text-left text-wrap">
<CardTitle className="text-md">Danger zone</CardTitle>
<CardDescription>Permanently delete account.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col lg:pb-0">
<Dialog open={deleteIntentModal} onOpenChange={setDeleteIntentModal}>
<DialogTrigger asChild>
<Button variant="destructive" className="w-52 trigger-delete-account">
Delete account
</Button>
</DialogTrigger>
<DialogContent className="default-dialog">
<DialogHeader>
<DialogTitle>Delete account</DialogTitle>
<DialogDescription>Confirm deletion</DialogDescription>
</DialogHeader>
<p className="text-center text-sm">
This is a non-reversible action. Everything related to your account, as well as
your connections and content will be lost forever - into the void.
</p>
<div className="flex flex-row w-full justify-center gap-4">
<Button onClick={() => setDeleteIntentModal(false)} type="button">
Cancel
</Button>
<Button
className="delete-account"
type="button"
onClick={deleteAccount}
variant="destructive"
>
Delete
</Button>
</div>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</div>
</>
)
Expand Down
4 changes: 2 additions & 2 deletions start/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ router
router
.group(() => {
router.get('/', [UsersController, 'index']).as('users.index')
router.patch(':id', [UsersController, 'update']).as('users.update')
router.delete(':id', [UsersController, 'destroy']).as('users.destroy')
router.patch('/', [UsersController, 'update']).as('users.update')
router.delete('/', [UsersController, 'destroy']).as('users.destroy')
})
.prefix('users')

Expand Down
6 changes: 4 additions & 2 deletions tests/browser/pages/feed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { test } from '@japa/runner'
test.group('Acessing feed', (group) => {
group.each.setup(() => testUtils.db().truncate())

test('Fails to access the feed without being authenticated', async ({ visit }) => {
const page = await visit(route('feed.show').path)
test('Successfully acesses the feed without being authenticated', async ({ visit }) => {
const url = route('feed.show').path
const page = await visit(url)
await page.assertTextContains('body', 'Sign in')
await page.assertPath(url)
})

test('Successfully acesses the feed while authenticated', async ({ visit, browserContext }) => {
Expand Down
18 changes: 10 additions & 8 deletions tests/browser/pages/user_feed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import { test } from '@japa/runner'
test.group('Acessing user profile feed', (group) => {
group.each.setup(() => testUtils.db().truncate())

test('Fails to access the user profile feed without being authenticated', async ({ visit }) => {
test('Successfully acesses the user profile feed without being authenticated', async ({
visit,
}) => {
const user = await UserFactory.create()
const page = await visit(
route('users.show', {
params: {
id: user.id,
},
}).path
)
const url = route('users.show', {
params: {
id: user.id,
},
}).path
const page = await visit(url)
await page.assertTextContains('body', 'Sign in')
await page.assertPath(url)
})

test('Successfully acesses own user profile feed while authenticated', async ({
Expand Down
15 changes: 15 additions & 0 deletions tests/browser/user/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ test.group('User settings', (group) => {
user = await UserFactory.create()
})

test('Fails to access the users settings without being authenticated', async ({ visit }) => {
const page = await visit(route('settings.show').path)
await page.assertTextContains('body', 'Sign in')
})

test('Sucessfully updates profile', async ({ visit, browserContext, assert }) => {
const authUser = user!
await browserContext.loginAs(authUser)
Expand Down Expand Up @@ -79,4 +84,14 @@ test.group('User settings', (group) => {
surname: 'The surname field format is invalid',
})
})

test('Successfully deletes profile', async ({ visit, browserContext }) => {
const authUser = user!
await browserContext.loginAs(authUser)
const page = await visit(url)
await page.locator('button.trigger-delete-account').click()
await page.locator('button.delete-account').click()
await page.waitForURL('/')
await page.assertTextContains('body', 'Sign in')
})
})

0 comments on commit 43f68c8

Please sign in to comment.