Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Settings to allow users to delete their account #166

Merged
merged 19 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions backend/LexBoxApi/GraphQL/UserMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,13 @@ public record DeleteUserByAdminInput(Guid UserId);
[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
[AdminRequired]
public async Task<User> DeleteUserByAdmin(DeleteUserByAdminInput input, LexBoxDbContext dbContext)
public async Task<User> DeleteUserByAdminOrSelf(DeleteUserByAdminOrSelfInput input, LexBoxDbContext dbContext, LoggedInContext loggedInContext)
{
var User = await dbContext.Users.FindAsync(input.UserId);
var user = dbContext.Users.Where(u => u.Id == input.UserId);
await user.ExecuteDeleteAsync();
return User;
loggedInContext.User.AssertCanDeleteAccount(input.UserId);
var user = await dbContext.Users.FindAsync(input.UserId);
if (user is null) throw new NotFoundException("User not found");
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync();
return user;
}
}
2 changes: 2 additions & 0 deletions backend/LexBoxApi/Models/Project/ChangeProjectInputs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ namespace LexBoxApi.Models.Project;
public record ChangeProjectNameInput(Guid ProjectId, string Name);

public record ChangeProjectDescriptionInput(Guid ProjectId, string Description);

public record DeleteUserByAdminOrSelfInput(Guid UserId);
3 changes: 3 additions & 0 deletions backend/LexCore/Auth/LexAuthUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ public void AssertCanAccessProject(string projectCode)
{
if (Role != UserRole.admin && Projects.All(p => p.Code != projectCode)) throw new UnauthorizedAccessException();
}
public void AssertCanDeleteAccount(Guid userid){
if (this.Id != userid && this.Role != UserRole.admin) throw new UnauthorizedAccessException();
}
}

public record AuthUserProject(string Code, ProjectRole Role, Guid ProjectId);
Expand Down
10 changes: 5 additions & 5 deletions frontend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,14 @@ enum DbErrorCode {
UNKNOWN
}

union DeleteUserByAdminError = DbError | NotFoundError
union DeleteUserByAdminOrSelfError = DbError | NotFoundError

input DeleteUserByAdminInput {
input DeleteUserByAdminOrSelfInput {
userId: UUID!
}

type DeleteUserByAdminPayload {
errors: [DeleteUserByAdminError!]
type DeleteUserByAdminOrSelfPayload {
errors: [DeleteUserByAdminOrSelfError!]
user: User
}

Expand Down Expand Up @@ -204,7 +204,7 @@ type Mutation {
changeUserAccountByAdmin(input: ChangeUserAccountDataInput!): ChangeUserAccountByAdminPayload!
changeUserAccountData(input: ChangeUserAccountDataInput!): ChangeUserAccountDataPayload!
createProject(input: CreateProjectInput!): CreateProjectPayload!
deleteUserByAdmin(input: DeleteUserByAdminInput!): DeleteUserByAdminPayload!
deleteUserByAdminOrSelf(input: DeleteUserByAdminOrSelfInput!): DeleteUserByAdminOrSelfPayload!
removeProjectMember(input: RemoveProjectMemberInput!): Project
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@
import t from '$lib/i18n';
import { z } from 'zod';
import { FormModal } from '$lib/components/modals';
import { _deleteUserByAdmin } from './+page';
import type { DeleteUserByAdminInput } from '$lib/gql/types';
import type { FormModalResult } from '$lib/components/modals';
import { _deleteUserByAdminOrSelf } from '../../routes/(authenticated)/(dashboards)/admin/+page';
import type { DeleteUserByAdminOrSelfInput } from '$lib/gql/types';
import type { FormModalResult } from '$lib/components/modals/FormModal.svelte';

const verify = z.object({
keyphrase: z.string().refine((value) => value.match(`^${$t('admin_dashboard.enter_to_delete.user.value')}$`)),
});
type Schema = typeof verify;

let deletionFormModal: FormModal<Schema>;
$: deletionForm = deletionFormModal?.form();

export async function open(id: string): Promise<FormModalResult<Schema>> {
export async function open(id: string): Promise<FormModalResult<Schema>> {
return await deletionFormModal.open(async () => {
const deleteUserInput: DeleteUserByAdminInput = {
const deleteUserInput: DeleteUserByAdminOrSelfInput = {
userId: id,
};
const { error } = await _deleteUserByAdmin(deleteUserInput);
const { error } = await _deleteUserByAdminOrSelf(deleteUserInput);
return error?.message;
});
}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
"reset_password": "Reset your password instead?",
"update_success": "Your account has been updated.",
"button_update": "Update account info",
"more_settings": "More Settings",
"delete_account": "Delete Account",
"delete_user": "delete account",
"keyphrase": "delete account",
"delete_success": "Your account has been deleted.",
"verify_email": {
"verify_success": "You've successfully verified your email address.",
"change_success": "You've successfully updated your email address.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import t from '$lib/i18n';
import type { PageData } from './$types';
import IconButton from '$lib/components/IconButton.svelte';
import DeleteUserModal from './DeleteUserModal.svelte';
import DeleteUserModal from '$lib/components/DeleteUserModal.svelte';
import EditUserAccount from './EditUserAccount.svelte';
import type { LoadAdminDashboardQuery } from '$lib/gql/types';
import { Duration, notifySuccess, notifyWarning } from '$lib/notify';
Expand Down
11 changes: 5 additions & 6 deletions frontend/src/routes/(authenticated)/(dashboards)/admin/+page.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type {
$OpResult,
ChangeUserAccountByAdminMutation,
ChangeUserAccountDataInput,
DeleteUserByAdminInput,
DeleteUserByAdminMutation,
DeleteUserByAdminOrSelfInput,
DeleteUserByAdminOrSelfMutation,
} from '$lib/gql/types';
import { getClient, graphql } from '$lib/gql';

Expand Down Expand Up @@ -74,13 +73,13 @@ export async function _changeUserAccountByAdmin(input: ChangeUserAccountDataInpu
)
return result;
}
export async function _deleteUserByAdmin(input: DeleteUserByAdminInput): $OpResult<DeleteUserByAdminMutation> {
export async function _deleteUserByAdminOrSelf(input: DeleteUserByAdminOrSelfInput): $OpResult<DeleteUserByAdminOrSelfMutation> {
//language=GraphQL
const result = await getClient()
.mutation(
graphql(`
mutation DeleteUserByAdmin($input: DeleteUserByAdminInput!) {
deleteUserByAdmin(input: $input) {
mutation DeleteUserByAdminOrSelf($input: DeleteUserByAdminOrSelfInput!) {
deleteUserByAdminOrSelf(input: $input) {
user {
id
}
Expand Down
32 changes: 26 additions & 6 deletions frontend/src/routes/(authenticated)/user/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@
import { Button, Form, FormError, Input, lexSuperForm } from '$lib/forms';
import t from '$lib/i18n';
import { Page } from '$lib/layout';
import { notifySuccess } from '$lib/notify';
import { _changeUserAccountData } from './+page';
import { notifySuccess, notifyWarning } from '$lib/notify';
import z from 'zod';
import { goto } from '$app/navigation';
import DeleteUserModal from '$lib/components/DeleteUserModal.svelte';
import type { PageData } from './$types';
import { _changeUserAccountData } from './+page';

import { TrashIcon } from '$lib/icons';
import {onMount} from 'svelte';
export let data: PageData;
const user = data.user;
$: user = data?.user;
$: userid = user?.id;
let deleteModal: DeleteUserModal;


async function openDeleteModal(): Promise<void> {
await deleteModal.open(userid);
notifyWarning($t('account_settings.delete_success'))
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
await goto('/logout');
dadukhankevin marked this conversation as resolved.
Show resolved Hide resolved
}
const formSchema = z.object({
email: z.string().email(),
name: z.string(),
Expand All @@ -37,14 +48,15 @@
notifySuccess($t('account_settings.update_success'));
}
});

form.set(
onMount(()=>{
form.set(
{
email: user.email,
name: user.name,
},
{ taint: false }
);
})
</script>

<Page>
Expand Down Expand Up @@ -72,10 +84,18 @@
error={$errors.email}
bind:value={$form.email}
/>
<div class="collapse text-error w-full underline rounded-box collapse-arrow">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">{$t('account_settings.more_settings')}</div>
<div class="collapse-content">
<span class="btn btn-error" on:click={openDeleteModal}>{$t('account_settings.delete_account')}<TrashIcon/></span>
</div>
</div>
<a class="link my-4" href="/resetPassword">
{$t('account_settings.reset_password')}
</a>
<FormError error={$message} />
<Button loading={$submitting}>{$t('account_settings.button_update')}</Button>
</Form>
</Page>
<DeleteUserModal bind:this={deleteModal} />
3 changes: 3 additions & 0 deletions frontend/src/routes/(authenticated)/user/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type {
$OpResult,
ChangeUserAccountDataMutation,
ChangeUserAccountDataInput,
DeleteUserByAdminOrSelfMutation,
DeleteUserByAdminOrSelfInput

} from '$lib/gql/types';
import { getClient, graphql } from '$lib/gql';
import { goto, invalidate } from '$app/navigation';
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/routes/(authenticated)/user/DeleteAccountModal.svelte
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once we modify the existing delete user mutation we can delete this modal and just use the same one we use on the admin dashboard (moving that one into a shared location like lib/components)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dadukhankevin I believe this file is unused right?

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
// This script handles the account settings page.
// For now, it only allows you to modify the users display name
// Minimal changes allow it to modify the users email as well (but this should have a re-verification system)
import Input from '$lib/forms/Input.svelte';
import t from '$lib/i18n';
import { z } from 'zod';
import { FormModal } from '$lib/components/modals';


const verify = z.object({
keyphrase: z.string().refine((value) => value.match(`^${$t('account_settings.keyphrase')}$`)),
});
export let deleteUser: CallableFunction;
let deletionFormModal: FormModal<typeof verify>;
$: deletionForm = deletionFormModal?.form();
export async function open(): Promise<void> {
await deletionFormModal.open(async () => {
await deleteUser();
});
}
</script>

<FormModal bind:this={deletionFormModal} schema={verify} let:errors>
<span slot="title">{$t('account_settings.delete_account')}</span>
<Input
id="keyphrase"
type="text"
label={$t('account_settings.delete_user_label')}
error={errors.keyphrase}
placeholder = {$t('account_settings.keyphrase')}
bind:value={$deletionForm.keyphrase}
/>
<span slot="submitText">{$t('admin_dashboard.form_modal.delete_user')}</span>
</FormModal>
Loading