[#276] added option to list and unlink external user auth relations

This commit is contained in:
Gani Georgiev 2022-09-01 12:22:59 +03:00
parent f61d0ec6f7
commit f0b57c6b91
7 changed files with 240 additions and 118 deletions

View File

@ -152,14 +152,6 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
return err return err
} }
} else { } else {
// update the existing user verified state
if !user.Verified {
user.Verified = true
if err := txDao.SaveUser(user); err != nil {
return err
}
}
// update the existing user empty email if the authData has one // update the existing user empty email if the authData has one
// (this in case previously the user was created with // (this in case previously the user was created with
// an OAuth2 provider that didn't return an email address) // an OAuth2 provider that didn't return an email address)
@ -169,6 +161,15 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
return err return err
} }
} }
// update the existing user verified state
// (only if the user doesn't have an email or the user email match with the one in authData)
if !user.Verified && (user.Email == "" || user.Email == authData.Email) {
user.Verified = true
if err := txDao.SaveUser(user); err != nil {
return err
}
}
} }
// create ExternalAuth relation if missing // create ExternalAuth relation if missing

View File

@ -4,8 +4,4 @@ PB_PROFILE_COLLECTION = "profiles"
PB_INSTALLER_PARAM = "installer" PB_INSTALLER_PARAM = "installer"
PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/manage-collections#rules-filters-syntax" PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/manage-collections#rules-filters-syntax"
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases" PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
<<<<<<< HEAD
PB_VERSION = "v0.6.0" PB_VERSION = "v0.6.0"
=======
PB_VERSION = "v0.5.2"
>>>>>>> master

View File

@ -0,0 +1,80 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { confirm } from "@/stores/confirmation";
import { addSuccessToast } from "@/stores/toasts";
const dispatch = createEventDispatcher();
export let user;
let externalAuths = [];
let isLoading = false;
async function loadExternalAuths() {
if (!user?.id) {
externalAuths = [];
isLoading = false;
return;
}
isLoading = true;
try {
externalAuths = await ApiClient.users.listExternalAuths(user.id);
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isLoading = false;
}
function unlinkExternalAuth(provider) {
if (!user?.id || !provider) {
return; // nothing to unlink
}
confirm(`Do you really want to unlink the selected provider?`, () => {
return ApiClient.users
.unlinkExternalAuth(user.id, provider)
.then(() => {
addSuccessToast("Successfully unlinked the provider.");
dispatch("unlink", provider);
loadExternalAuths(); // reload list
})
.catch((err) => {
ApiClient.errorResponseHandler(err);
});
});
}
onMount(() => {
loadExternalAuths();
});
</script>
{#if isLoading}
<div class="block txt-center">
<span class="loader" />
</div>
{:else if user?.id && externalAuths.length}
<div class="list">
{#each externalAuths as auth}
<div class="list-item">
<i class="ri-{auth.provider}-line" />
<span class="txt">{CommonHelper.sentenize(auth.provider, false)}</span>
<div class="txt-hint">ID: {auth.providerId}</div>
<button
type="button"
class="btn btn-secondary link-hint btn-circle btn-sm m-l-auto"
on:click={() => unlinkExternalAuth(auth.provider)}
>
<i class="ri-close-line" />
</button>
</div>
{/each}
</div>
{:else}
<p class="txt-hint txt-center">No authorized OAuth2 providers.</p>
{/if}

View File

@ -199,17 +199,19 @@
<div class="inline-flex"> <div class="inline-flex">
{#if user.email} {#if user.email}
<span class="txt" title={user.email}>{user.email}</span> <span class="txt" title={user.email}>{user.email}</span>
<span
class="label"
class:label-success={user.verified}
class:label-warning={!user.verified}
>
{user.verified ? "Verified" : "Unverified"}
</span>
{:else} {:else}
<div class="txt-hint">N/A</div> <div class="txt-hint">N/A</div>
{#if user.verified}
<span class="label label-success">OAuth2 verified</span>
{/if}
{/if} {/if}
<span
class="label"
class:label-success={user.verified}
class:label-warning={!user.verified}
>
{user.verified ? "Verified" : "Unverified"}
</span>
</div> </div>
</td> </td>

View File

@ -11,5 +11,7 @@
<div class="content"> <div class="content">
<div class="block txt-ellipsis">{item.id}</div> <div class="block txt-ellipsis">{item.id}</div>
<small class="block txt-hint txt-ellipsis">{item.email}</small> {#if item.email}
<small class="block txt-hint txt-ellipsis">{item.email}</small>
{/if}
</div> </div>

View File

@ -11,9 +11,13 @@
import Field from "@/components/base/Field.svelte"; import Field from "@/components/base/Field.svelte";
import Toggler from "@/components/base/Toggler.svelte"; import Toggler from "@/components/base/Toggler.svelte";
import OverlayPanel from "@/components/base/OverlayPanel.svelte"; import OverlayPanel from "@/components/base/OverlayPanel.svelte";
import ExternalAuthsList from "./ExternalAuthsList.svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const formId = "user_" + CommonHelper.randomString(5); const formId = "user_" + CommonHelper.randomString(5);
const accountTab = "account";
const providersTab = "providers";
let panel; let panel;
let user = new User(); let user = new User();
@ -24,6 +28,7 @@
let passwordConfirm = ""; let passwordConfirm = "";
let changePasswordToggle = false; let changePasswordToggle = false;
let verificationEmailToggle = true; let verificationEmailToggle = true;
let activeTab = accountTab;
$: hasChanges = (user.isNew && email != "") || changePasswordToggle || email !== user.email; $: hasChanges = (user.isNew && email != "") || changePasswordToggle || email !== user.email;
@ -41,7 +46,9 @@
function load(model) { function load(model) {
setErrors({}); // reset errors setErrors({}); // reset errors
user = model?.clone ? model.clone() : new User(); user = model?.clone ? model.clone() : new User();
reset(); // reset form reset(); // reset form
} }
@ -75,6 +82,8 @@
request request
.then(async (result) => { .then(async (result) => {
user = result;
if (verificationEmailToggle) { if (verificationEmailToggle) {
sendVerificationEmail(false); sendVerificationEmail(false);
} }
@ -114,7 +123,7 @@
function sendVerificationEmail(notify = true) { function sendVerificationEmail(notify = true) {
return ApiClient.users return ApiClient.users
.requestVerification(user.isNew ? email : user.email) .requestVerification(user.email || email)
.then(() => { .then(() => {
confirmClose = false; confirmClose = false;
hide(); hide();
@ -130,8 +139,8 @@
<OverlayPanel <OverlayPanel
bind:this={panel} bind:this={panel}
popup
class="user-panel" class="user-panel"
popup={user.isNew}
beforeHide={() => { beforeHide={() => {
if (hasChanges && confirmClose) { if (hasChanges && confirmClose) {
confirm("You have unsaved changes. Do you really want to close the panel?", () => { confirm("You have unsaved changes. Do you really want to close the panel?", () => {
@ -146,95 +155,15 @@
on:show on:show
> >
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<h4> <h4>{user.isNew ? "New user" : "Edit user"}</h4>
{user.isNew ? "New user" : "Edit user"}
</h4>
</svelte:fragment>
<form id={formId} class="grid" autocomplete="off" on:submit|preventDefault={save}>
{#if !user.isNew}
<Field class="form-field disabled" name="id" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("primary")} />
<span class="txt">ID</span>
</label>
<input type="text" id={uniqueId} value={user.id} disabled />
</Field>
{/if}
<Field class="form-field required" name="email" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("email")} />
<span class="txt">Email</span>
</label>
{#if user.verified}
<div class="form-field-addon txt-success" use:tooltip={"Verified"}>
<i class="ri-shield-check-line" />
</div>
{/if}
<input type="email" autocomplete="off" id={uniqueId} required bind:value={email} />
</Field>
{#if !user.isNew} {#if !user.isNew}
<Field class="form-field form-field-toggle" let:uniqueId> <button type="button" class="btn btn-sm btn-circle btn-secondary m-l-auto">
<input type="checkbox" id={uniqueId} bind:checked={changePasswordToggle} />
<label for={uniqueId}>Change password</label>
</Field>
{/if}
{#if user.isNew || changePasswordToggle}
<div class="col-12">
<div class="grid" transition:slide|local={{ duration: 150 }}>
<div class="col-sm-6">
<Field class="form-field required" name="password" let:uniqueId>
<label for={uniqueId}>
<i class="ri-lock-line" />
<span class="txt">Password</span>
</label>
<input
type="password"
autocomplete="new-password"
id={uniqueId}
required
bind:value={password}
/>
</Field>
</div>
<div class="col-sm-6">
<Field class="form-field required" name="passwordConfirm" let:uniqueId>
<label for={uniqueId}>
<i class="ri-lock-line" />
<span class="txt">Password confirm</span>
</label>
<input
type="password"
autocomplete="new-password"
id={uniqueId}
required
bind:value={passwordConfirm}
/>
</Field>
</div>
</div>
</div>
{/if}
{#if user.isNew}
<Field class="form-field form-field-toggle" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={verificationEmailToggle} />
<label for={uniqueId}>Send verification email</label>
</Field>
{/if}
</form>
<svelte:fragment slot="footer">
{#if !user.isNew}
<button type="button" class="btn btn-sm btn-circle btn-secondary">
<!-- empty span for alignment --> <!-- empty span for alignment -->
<span /> <span />
<i class="ri-more-line" /> <i class="ri-more-line" />
<Toggler class="dropdown dropdown-upside dropdown-left dropdown-nowrap"> <Toggler class="dropdown dropdown-right dropdown-nowrap">
{#if !user.verified} {#if !user.verified && user.email}
<button type="button" class="dropdown-item" on:click={() => sendVerificationEmail()}> <button type="button" class="dropdown-item" on:click={() => sendVerificationEmail()}>
<i class="ri-mail-check-line" /> <i class="ri-mail-check-line" />
<span class="txt">Send verification email</span> <span class="txt">Send verification email</span>
@ -246,20 +175,132 @@
</button> </button>
</Toggler> </Toggler>
</button> </button>
<div class="flex-fill" />
{/if} {/if}
</svelte:fragment>
<div class="tabs user-tabs">
{#if !user.isNew}
<div class="tabs-header stretched">
<button
type="button"
class="tab-item"
class:active={activeTab === accountTab}
on:click={() => (activeTab = accountTab)}
>
Account
</button>
<button
type="button"
class="tab-item"
class:active={activeTab === providersTab}
on:click={() => (activeTab = providersTab)}
>
Authorized providers
</button>
</div>
{/if}
<div class="tabs-content">
<div class="tab-item" class:active={activeTab === accountTab}>
<form id={formId} class="grid" autocomplete="off" on:submit|preventDefault={save}>
{#if !user.isNew}
<Field class="form-field disabled" name="id" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("primary")} />
<span class="txt">ID</span>
</label>
<input type="text" id={uniqueId} value={user.id} disabled />
</Field>
{/if}
<Field class="form-field required" name="email" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("email")} />
<span class="txt">Email</span>
</label>
{#if user.verified && user.email}
<div class="form-field-addon txt-success" use:tooltip={"Verified"}>
<i class="ri-shield-check-line" />
</div>
{/if}
<input type="email" autocomplete="off" id={uniqueId} required bind:value={email} />
</Field>
{#if !user.isNew && user.email}
<Field class="form-field form-field-toggle" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={changePasswordToggle} />
<label for={uniqueId}>Change password</label>
</Field>
{/if}
{#if user.isNew || !user.email || changePasswordToggle}
<div class="col-12">
<div class="grid" transition:slide|local={{ duration: 150 }}>
<div class="col-sm-6">
<Field class="form-field required" name="password" let:uniqueId>
<label for={uniqueId}>
<i class="ri-lock-line" />
<span class="txt">Password</span>
</label>
<input
type="password"
autocomplete="new-password"
id={uniqueId}
required
bind:value={password}
/>
</Field>
</div>
<div class="col-sm-6">
<Field class="form-field required" name="passwordConfirm" let:uniqueId>
<label for={uniqueId}>
<i class="ri-lock-line" />
<span class="txt">Password confirm</span>
</label>
<input
type="password"
autocomplete="new-password"
id={uniqueId}
required
bind:value={passwordConfirm}
/>
</Field>
</div>
</div>
</div>
{/if}
{#if user.isNew || !user.email}
<Field class="form-field form-field-toggle" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={verificationEmailToggle} />
<label for={uniqueId}>Send verification email</label>
</Field>
{/if}
</form>
</div>
{#if !user.isNew}
<div class="tab-item" class:active={activeTab === providersTab}>
<ExternalAuthsList {user} />
</div>
{/if}
</div>
</div>
<svelte:fragment slot="footer">
<button type="button" class="btn btn-secondary" disabled={isSaving} on:click={() => hide()}> <button type="button" class="btn btn-secondary" disabled={isSaving} on:click={() => hide()}>
<span class="txt">Cancel</span> <span class="txt">Cancel</span>
</button> </button>
<button
type="submit" {#if activeTab === accountTab}
form={formId} <button
class="btn btn-expanded" type="submit"
class:btn-loading={isSaving} form={formId}
disabled={!hasChanges || isSaving} class="btn btn-expanded"
> class:btn-loading={isSaving}
<span class="txt">{user.isNew ? "Create" : "Save changes"}</span> disabled={!hasChanges || isSaving}
</button> >
<span class="txt">{user.isNew ? "Create" : "Save changes"}</span>
</button>
{/if}
</svelte:fragment> </svelte:fragment>
</OverlayPanel> </OverlayPanel>

View File

@ -229,7 +229,7 @@ button {
padding: 0; padding: 0;
gap: 0; gap: 0;
i { i {
$iconSize: 23px; $iconSize: 24px;
font-size: 1.2857rem; font-size: 1.2857rem;
text-align: center; text-align: center;
width: $iconSize; width: $iconSize;