Pulling out parts into separate files. Also minor clean up.

This commit is contained in:
Scott Idem
2025-05-02 10:48:09 -04:00
parent f2751cbaf9
commit 77c53065bc
8 changed files with 650 additions and 518 deletions

View File

@@ -0,0 +1,772 @@
<script lang="ts">
// *** Import Svelte specific
import { browser } from '$app/environment';
import { goto, invalidateAll } from '$app/navigation';
import { Modal } from 'flowbite-svelte';
// *** Import other supporting libraries
import {
CircleX,
Eye, EyeOff,
Key,
LogIn, LogOut, LockKeyhole,
Mail, MailCheck,
User, UserCheck
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/ae_stores';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { core_func } from '$lib/ae_core/ae_core_functions';
// *** Setup Svelte properties
interface Props {
log_lvl?: number;
data?: any;
hidden: null|boolean;
}
let {
log_lvl = 0,
data = null,
hidden = true,
}: Props = $props();
let url_user_id = data.url.searchParams.get('user_id');
let url_user_key = data.url.searchParams.get('user_key'); // Reminder that "key" is the site's auth key.
let url_user_username = data.url.searchParams.get('username');
let url_user_email = data.url.searchParams.get('user_email');
let ae_promises: key_val = {};
// let user_id: string|null = null;
// let user_obj: key_val = {};
// let person_id: string|null = null;
// let person_obj: key_val = {};
let trigger: null|boolean = $state(null); // Use $state to ensure reactivity
let user_id = $state(null); // Use $state to ensure reactivity
let user_obj: key_val = $state({}); // Use $state to ensure reactivity
let person_id = $state(null); // Use $state to ensure reactivity
let person_obj: key_val = $state({}); // Use $state to ensure reactivity
$effect(() => {
if (user_id && person_id && trigger) {
// alert(`Ready to sign in person! \n\nuser_id: ${user_id}\nperson_id: ${person_id}\n\nThis is a test alert to confirm the values are set.`);
trigger = false;
sign_in();
}
});
let new_password = $state('');
let confirm_password = $state('');
let is_changing_password = $state(false);
let show_password_text = $state('password'); // password or text
function sign_in() {
$ae_loc.person_id = person_id; // Set the person_id in the ae_loc store
$ae_loc.person = person_obj; // Store the full person object for reference
$ae_loc.user_id = user_id; // Set the user_id in the ae_loc store
$ae_loc.user = user_obj; // Store the full user object for reference
if (user_obj.super) {
$ae_loc.access_type = 'super';
} else if (user_obj.manager) {
$ae_loc.access_type = 'manager';
} else if (user_obj.administrator) {
$ae_loc.access_type = 'administrator';
} else if (user_obj.verified) {
// They are "trusted" because the user has been verified. External staff usually start with this level.
$ae_loc.access_type = 'trusted';
} else if (user_obj.public) {
$ae_loc.access_type = 'public';
} else {
$ae_loc.access_type = 'authenticated';
}
$ae_loc.user_access_type = $ae_loc.access_type; // Used to revert back to the user's access type after quick access (temporarily escalate permissions) is turned off.
let access_checks_results = ae_util.process_permission_checks($ae_loc.access_type);
// WARNING: I think this is causing a loop in Svelte or something.
// Last ten effects were:
// Uncaught Svelte error: effect_update_depth_exceeded
// Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
// https://svelte.dev/e/effect_update_depth_exceeded
$ae_loc = {
...$ae_loc,
...access_checks_results
};
// $ae_loc = {...access_checks_results};
console.log('Remove the sign in fields from the URL.');
data.url.searchParams.delete('user_id');
data.url.searchParams.delete('user_key');
data.url.searchParams.delete('username');
data.url.searchParams.delete('user_email');
data.url.searchParams.delete('valid_email'); // Part of sign in email for possible future use
let new_url = data.url.toString();
// We need to set browser history and force all load functions to rerun.
goto(new_url, {replaceState: true, invalidateAll: true});
}
function sign_out() {
// Clear the session information
$ae_loc.person_id = null;
$ae_loc.person = {};
$ae_loc.user_id = null;
$ae_loc.user = {};
user_id = null; // Clear the user_id
user_obj = {}; // Clear the user object
person_id = null; // Clear the person_id
person_obj = {}; // Clear the person object
$ae_loc.access_type = ''; // Clear the access type
let access_checks_results = ae_util.process_permission_checks('');
$ae_loc = {...$ae_loc, ...access_checks_results};
$ae_sess.auth__entered_user_id = null;
$ae_sess.auth__entered_user_key = null;
// $ae_sess.auth__entered_username = null; // Keeping the username
$ae_sess.auth__entered_password = null;
indexedDB.deleteDatabase('ae_archives_db'); // Archives module
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db'); // Events module
indexedDB.deleteDatabase('ae_journals_db'); // Journals module
indexedDB.deleteDatabase('ae_posts_db'); // Posts module
indexedDB.deleteDatabase('ae_sponsorships_db'); // Sponsorships module
// $ae_loc.allow_access = false;
$ae_loc.authenticated_access = false;
$ae_loc.edit_mode = false;
localStorage.clear();
sessionStorage.clear();
console.log('Remove the sign out fields from the URL.');
data.url.searchParams.delete('user_id');
data.url.searchParams.delete('user_key');
data.url.searchParams.delete('username');
data.url.searchParams.delete('user_email');
data.url.searchParams.delete('valid_email'); // Part of sign in email for possible future use
let new_url = data.url.toString();
// We need to set browser history and force all load functions to rerun.
// goto(new_url, {replaceState: true, invalidateAll: true});
// invalidateAll();
window.location.reload();
console.log('Signed out successfully.');
}
if (browser) {
if (url_user_id) {
// Pre-fill the auth__entered_user_id if passed in via the URL.
$ae_sess.auth__entered_user_id = url_user_id;
}
if (url_user_key) {
// Pre-fill the auth__entered_key if passed in via the URL.
$ae_sess.auth__entered_user_key = url_user_key;
}
if (url_user_username) {
// Pre-fill the auth__entered_username if passed in via the URL.
$ae_sess.auth__entered_username = url_user_username;
}
if (url_user_email) {
// Pre-fill the auth__entered_email if passed in via the URL.
$ae_sess.auth__entered_email = url_user_email;
}
}
// Use core_func.send_email_auth_ae_obj__user_id
function handle_send_auth_email({user_id}: {user_id: string}) {
console.log('handle_send_auth_email()');
console.log($ae_loc.base_url); // URL origin
console.log($ae_loc.hostname); // URL hostname
// This function creates a new auth_key and then sends an email to the user with the new auth key.
ae_promises.send_email_auth_ae_obj__user_id = core_func.send_email_auth_ae_obj__user_id({
api_cfg: $ae_api,
account_id: $slct.account_id,
user_id: user_id,
base_url: $ae_loc.base_url,
log_lvl: 2
});
}
// Use core_func.qry_ae_obj_li__user_email
function handle_lookup_user_email({email}: {email: string}) {
console.log('handle_lookup_user_email()');
// If a user is returned then send the new auth key email
ae_promises.load__user_obj_li = core_func.qry_ae_obj_li__user_email({
api_cfg: $ae_api,
account_id: $slct.account_id,
null_account_id: false,
email: email,
log_lvl: 1
}).then((user_response) => {
if (user_response?.user_id_random) {
console.log(`User found for email:`, user_response);
handle_send_auth_email({
user_id: user_response.user_id_random
});
} else if (user_response.length > 0) {
console.log(`Multiple users found for email:`, user_response);
handle_send_auth_email({
user_id: user_response[0].user_id_random
});
} else {
alert('No user found with that email address.');
}
});
}
async function handle_change_password() {
if (new_password !== confirm_password) {
alert('Passwords do not match.');
return;
}
if (!new_password) {
alert('Password cannot be empty.');
return;
}
is_changing_password = true;
try {
await core_func.auth_ae_obj__user_id_change_password({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
user_id: $ae_loc.user_id,
password: new_password,
log_lvl: 1
});
alert('Password changed successfully.');
$ae_sess.show__modal_change_password = false;
} catch (error) {
console.error('Error changing password:', error);
alert('Failed to change password.');
} finally {
is_changing_password = false;
}
}
</script>
<!-- It is important to keep in mind that a Person can only fully sign in if they have a linked User record. -->
<!-- *:hover:inline -->
<section
id="xAE-Sign-In-Out"
class="
ae_sign_in_out
hidden-print
bg-red-100 text-gray-900
transition-all duration-300 delay-150 hover:delay-1000 hover:ease-out hover:transition-all
flex flex-col flex-wrap gap-1
items-end justify-center
w-72 max-w-72
p-1 my-1
border-2 border-gray-200
"
class:hidden={hidden}
>
<button
type="button"
class="btn btn-sm variant-outline-surface hover:variant-ghost-warning *:hover:inline w-full"
title="Sign In"
onclick={() => {
$ae_sess.show__sign_in_out__fields = !$ae_sess.show__sign_in_out__fields; // Toggle the visibility of the sign-in form
}}
>
{#if $ae_loc?.person_id && $ae_loc?.user_id}
{#if $ae_sess.show__sign_in_out__fields}
<CircleX class="mx-1" />
Hide Sign-In Options
{:else}
<User class="mx-1 inline-block text-gray-500" />
{$ae_loc?.person?.full_name ?? '-- not set --'}
{/if}
{:else}
{#if $ae_sess.show__sign_in_out__fields}
<CircleX class="mx-1" />
Hide Sign-In Options
{:else}
<LockKeyhole size="1.25em" class="mx-1 inline-block text-gray-500" />
<!-- <User class="mx-1 inline-block text-gray-500" /> -->
<span class="hidden">
Sign-In?
</span>
{/if}
{/if}
</button>
<header
class:hidden={!$ae_sess.show__sign_in_out__fields}
class="ae_header w-full "
>
<h2 class="text-sm text-center font-semibold">
{#if $ae_loc?.person_id && $ae_loc?.user_id}
<!-- <button
type="button"
class="btn btn-sm variant-outline-surface hover:variant-filled-surface"
title="Show/Hide Sign-In/Out Options"
onclick={() => {
$ae_sess.show__sign_in_out__fields = !$ae_sess.show__sign_in_out__fields; // Toggle the visibility of the sign-in form
}}
> -->
{$ae_loc?.person?.full_name ?? '-- not set --'}
<!-- <span class="text-sm text-gray-500">
{$ae_loc?.user.username ?? '-- not set --'}
</span> -->
<!-- {#if log_lvl > 1}
<span class="text-xs text-gray-500">({$ae_loc.person_id} / {$ae_loc.user_id})</span>
{/if} -->
<!-- </button> -->
{:else}
<!-- <LogIn class="mx-1" /> -->
Sign In
{/if}
</h2>
</header>
<span
class:hidden={!$ae_sess.show__sign_in_out__fields}
class="flex flex-col gap-1 transition-all w-full"
>
{#if !$ae_loc?.person_id && !$ae_loc?.user_id}
<!-- Form for user look up based on email address -->
<form
class="
ae_sign_in_form
flex flex-col gap-1 items-end justify-center
p-1 my-1
"
onsubmit={async (e) => {
e.preventDefault();
if ($ae_sess.auth__entered_email) {
alert('Attempting to look up user by email address.');
handle_lookup_user_email({
email: $ae_sess.auth__entered_email
});
} else {
alert('Please enter an email address to look up.');
}
}}
>
<input
type="text"
class="input max-w-48"
placeholder="Email Address"
value={$ae_sess.auth__entered_email ?? ''}
oninput={(e) => $ae_sess.auth__entered_email = e.target.value}
>
<button
type="submit"
class="btn btn-sm variant-ghost-secondary hover:variant-filled-secondary"
title="Look up user by email and send sign in email"
>
<!-- <User class="mx-1" /> -->
<!-- <Mail class="mx-1" /> -->
<MailCheck class="mx-1" />
Email Sign In
</button>
</form>
<!-- We need to get the person's linked User ID and auth_key or User username and password to sign in. -->
<form
class="
ae_sign_in_form
flex flex-col gap-1 items-end
p-1 my-1
"
onsubmit={async (e) => {
e.preventDefault();
// WARNING: Logging in as a global user does not work yet. The API needs to be updated. Currently it returns multiple user records from the v_user view if there is more than one person record linked to the user ID.
if ($ae_sess.auth__entered_user_id && $ae_sess.auth__entered_user_key) {
// Try to use the user ID and user auth key passed in the URL params for authentication
alert('Attempting to authenticate with User ID and Auth Key.');
ae_promises['user'] = await core_func.auth_ae_obj__user_id_user_auth_key({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
// null_account_id: false, // Set to true to allow to authenticate as global user (Super or Manager)
user_id: $ae_sess.auth__entered_user_id,
user_auth_key: $ae_sess.auth__entered_user_key,
log_lvl: 2,
}).then((user_response) => {
// console.log(`HERE:`, user_response);
if (user_response.user_id_random) {
console.log(`Successfully authenticated in with User ID and User Auth Key: ${user_response.username}`, user_response);
user_obj = user_response; // Store the user object for later use
user_id = user_obj.user_id_random; // Use the user_id_random for further API calls
// person_id = user_obj.person_id_random;
} else {
alert('Failed to authenticate: ' + user_response.error);
}
}).then((response) => {
if (!user_id) {
// If we didn't get a user_id, return early
console.error('No user_id obtained from auth_ae_obj__username_password');
return;
}
// Next we need to get the person's information. This is odd because we need to look it up based on the account_id and user_id. There should only be one account person record per user. 99% of the time the user's account ID will be the same as the person's account ID. The exception is for AE Global users (Super or Manager) who can have multiple accounts but only one person record per account.
let params = {
user_id_random: user_id, // The user_id_random from the above authentication
}
let params_json: key_val = {};
params_json['and_qry'] = {};
if (user_id) {
params_json['and_qry']['user_id_random'] = user_id;
}
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
ae_promises['person'] = core_func.handle_load_ae_obj_li__person({
api_cfg: $ae_api,
for_obj_type: 'account',
for_obj_id: $ae_loc.account_id,
params_json: params_json,
params: params,
log_lvl: 1,
}).then((person_response) => {
if (person_response[0].person_id_random) {
console.log(`Successfully loaded person for user_id_random (${user_id}):`, person_response[0]);
person_obj = person_response[0];
person_id = person_obj.person_id_random;
trigger = true; // Set trigger to true to indicate we can now sign in
} else {
alert('Failed to load person information: ' + person_response.error);
}
}).then(() => {
// Once all promises are resolved, we can check if we have both user_id and person_id
if (user_id && person_id) {
// Set the session information
// $ae_loc.person_id = person_id; // Set the person_id in the ae_loc store
// $ae_loc.person = person_obj; // Store the full person object for reference
// $ae_loc.user_id = user_id; // Set the user_id in the ae_loc store
// $ae_loc.user = user_obj; // Store the full user object for reference
console.log(`Successfully authenticated and loaded user and person records:
user_id: ${user_id},
person_id: ${person_id}
`);
// window.location.reload(); // Reload to get the new session
} else {
console.error('Failed to authenticate and load data: missing user_id or person_id');
}
});
});
// console.log('DONE???', ae_promises);
} else if ($ae_sess.auth__entered_username && $ae_sess.auth__entered_password) {
// Try to use the username/password for authentication
// alert('Attempting to authenticate with Username and Password.');
ae_promises['user'] = await core_func.auth_ae_obj__username_password({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
// null_account_id: false, // Set to true to allow to authenticate as global user (Super or Manager)
username: $ae_sess.auth__entered_username,
password: $ae_sess.auth__entered_password,
log_lvl: 1,
}).then((user_response) => {
if (user_response.user_id_random) {
console.log(`Successfully authenticated in with Username (${user_response.username}) and Password:`, user_response);
user_obj = user_response; // Store the user object for later use
user_id = user_obj.user_id_random; // Use the user_id_random for further API calls
// person_id = user_obj.person_id_random;
} else {
alert('Failed to authenticate: ' + user_response.error);
}
}).then((response) => {
if (!user_id) {
// If we didn't get a user_id, return early
console.error('No user_id obtained from auth_ae_obj__username_password');
return;
}
// Next we need to get the person's information. This is odd because we need to look it up based on the account_id and user_id. There should only be one account person record per user. 99% of the time the user's account ID will be the same as the person's account ID. The exception is for AE Global users (Super or Manager) who can have multiple accounts but only one person record per account.
let params = {
user_id_random: user_id, // The user_id_random from the above authentication
}
let params_json: key_val = {};
params_json['and_qry'] = {};
if (user_id) {
params_json['and_qry']['user_id_random'] = user_id;
}
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
ae_promises['person'] = core_func.handle_load_ae_obj_li__person({
api_cfg: $ae_api,
for_obj_type: 'account',
for_obj_id: $ae_loc.account_id,
params_json: params_json,
params: params,
log_lvl: 1,
}).then((person_response) => {
if (person_response[0].person_id_random) {
console.log(`Successfully loaded person for user_id_random (${user_id}):`, person_response[0]);
person_obj = person_response[0];
person_id = person_obj.person_id_random;
trigger = true; // Set trigger to true to indicate we can now sign in
} else {
alert('Failed to load person information: ' + person_response.error);
}
}).then(() => {
// Once all promises are resolved, we can check if we have both user_id and person_id
if (user_id && person_id) {
// Set the session information
// $ae_loc.person_id = person_id; // Set the person_id in the ae_loc store
// $ae_loc.person = person_obj; // Store the full person object for reference
// $ae_loc.user_id = user_id; // Set the user_id in the ae_loc store
// $ae_loc.user = user_obj; // Store the full user object for reference
console.log(`Successfully authenticated and loaded user and person records:
user_id: ${user_id},
person_id: ${person_id}
`);
// window.location.reload(); // Reload to get the new session
} else {
console.error('Failed to authenticate and load data: missing user_id or person_id');
}
});
});
// console.log('DONE???', ae_promises);
} else {
alert('Please enter either a User ID and Auth Key or Username and Password.');
return false; // Prevent form submission if no credentials are provided
}
}}
>
{#if $ae_sess.auth__entered_user_id}
<input
type="text"
class="input max-w-36"
placeholder="User ID"
value={$ae_sess.auth__entered_user_id ?? ''}
oninput={(e) => $ae_sess.auth__entered_user_id = e.target.value}
>
<input
type="text"
class="input max-w-36"
placeholder="Auth Key"
value={$ae_sess.auth__entered_user_key ?? ''}
oninput={(e) => $ae_sess.auth__entered_user_key = e.target.value}
>
{:else}
<input
type="text"
class="input max-w-48"
placeholder="Username"
value={$ae_sess.auth__entered_username ?? ''}
oninput={(e) => $ae_sess.auth__entered_username = e.target.value}
>
<input
type="password"
class="input max-w-48"
placeholder="Password"
value={$ae_sess.auth__entered_password ?? ''}
oninput={(e) => $ae_sess.auth__entered_password = e.target.value}
>
{/if}
<button
type="submit"
class="btn btn-sm variant-ghost-secondary hover:variant-filled-secondary"
title="Sign in with username and password"
>
<!-- <LogIn class="mx-1" /> -->
<UserCheck class="mx-1" />
{#if $ae_sess.auth__entered_user_id}
User ID
{:else}
Username
{/if}
Sign In
</button>
</form>
{:else}
<div class="flex flex-col gap-1 items-end justify-center">
<span class="text-sm text-gray-500">
{$ae_loc?.user?.username ?? '-- not set --'}
</span>
<!-- Display change password option if the user is signed in and in Edit Mode -->
{#if $ae_loc.edit_mode}
<button
type="button"
class="btn btn-sm variant-ghost-warning hover:variant-filled-warning"
title="Change Password"
onclick={() => {
$ae_sess.show__modal_change_password = true;
}}
>
<LockKeyhole class="mx-1" />
<span class="hidden sm:inline">
Change Password
</span>
</button>
{/if}
<!-- Display sign out option if the user is signed in -->
<button
type="button"
class="btn btn-sm variant-ghost-warning hover:variant-filled-warning"
title="Sign Out"
onclick={async () => {
if (confirm('Are you sure you want to sign out?')) {
sign_out();
}
}}
>
<LogOut class="mx-1" />
<span class="hidden sm:inline">
Sign Out
</span>
</button>
</div>
{/if}
</span>
</section>
<!-- Change Password Modal -->
<!-- They should be able to see their password (text) -->
<!-- autocomplete="off" -->
{#if $ae_sess.show__modal_change_password}
<Modal
title="Change Password"
bind:open={$ae_sess.show__modal_change_password}
autoclose={false}
placement="top-center"
size="md"
class="top-center bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md relative mx-auto w-full divide-y"
>
<div class="modal-box flex flex-col gap-2 items-center justify-center">
<div class="flex flex-row flex-wrap gap-2">
<span class="flex flex-row flex-wrap gap-1">
<input
type={show_password_text}
autocomplete="new-password"
name="new_password"
placeholder="New Password"
bind:value={new_password}
class="input input-bordered w-48 max-w-full"
/>
<input
type={show_password_text}
autocomplete="off"
name="confirm_password"
placeholder="Confirm Password"
bind:value={confirm_password}
class="input input-bordered w-48 max-w-full"
/>
</span>
<!-- Show/Hide Password Text -->
<button
type="button"
class="btn btn-icon variant-soft-secondary"
onclick={() => {
// const inputs = document.querySelectorAll('.modal-box input[type="password"]');
// inputs.forEach((input) => {
// input.type = input.type === 'password' ? 'text' : 'password';
// });
if (show_password_text === 'password') {
show_password_text = 'text';
} else {
show_password_text = 'password';
}
}}
title="Show/Hide Password Text"
>
{#if show_password_text === 'password'}
<EyeOff class="mx-1"/>
{:else}
<Eye class="mx-1"/>
{/if}
<!-- <Eye class="mx-1"/> -->
<!-- <span class="hidden sm:inline">
Show/Hide
</span> -->
</button>
</div>
<div class="flex flex-row flex-wrap gap-2">
<button
type="button"
class="btn variant-filled-warning"
onclick={handle_change_password}
disabled={is_changing_password}
>
<Key class="mx-1" />
Change Password
</button>
<button
type="button"
class="btn variant-glass-secondary"
onclick={() => ($ae_sess.show__modal_change_password = false)}
>
<CircleX class="mx-1" />
Cancel
</button>
</div>
</div>
</Modal>
{/if}
<style lang="scss">
</style>