feat(sign-in): inline status feedback on username/password sign-in button

Replaces all alert() calls in the user/pass auth flow with reactive state.
Button shows: Verifying… (disabled) → Failed — retry? (red) →
Enter credentials first (amber) → Username/User ID Sign In (default).
Error messages (wrong password, no person record, no server response)
appear as small text below the button on failure.
Clicking the button resets to default so retry is clean.
Also removes dead commented-out alert and cleans up the promise chains.

No type="button" issues found — all non-submit buttons were already typed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-13 17:09:09 -04:00
parent 9dde412781
commit e5b4805916

View File

@@ -73,9 +73,13 @@
let is_changing_password = $state(false);
let show_password_text = $state('password'); // password or text
// Email sign-in status: null | 'sending' | 'sent' | 'not_found' | 'error'
// Email sign-in status: null | 'sending' | 'sent' | 'not_found' | 'no_email' | 'error'
let email_send_status: null | string = $state(null);
// Username/password sign-in status: null | 'signing_in' | 'error' | 'no_credentials'
let user_signin_status: null | string = $state(null);
let user_signin_error: null | string = $state(null);
function sign_in() {
$ae_loc.jwt = user_obj.jwt; // Store the JSON Web Token
$ae_loc.person_id = person_id; // Set the person_id in the ae_loc store
@@ -445,9 +449,15 @@
// 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.
// Helper to set error state without repeating the pattern
const set_error = (msg: string) => {
user_signin_status = 'error';
user_signin_error = msg;
};
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.');
user_signin_status = 'signing_in';
user_signin_error = null;
ae_promises['user'] = await core_func
.auth_ae_obj__user_id_user_auth_key({
@@ -459,93 +469,60 @@
log_lvl: 0
})
.then((user_response) => {
// console.log(`HERE:`, user_response);
if (user_response?.user_id) {
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; // Use the user_id for further API calls
// person_id = user_obj.person_id_random;
console.log(`Successfully authenticated with User ID and Auth Key: ${user_response.username}`, user_response);
user_obj = user_response;
user_id = user_obj.user_id;
} else if (!user_response) {
alert('Sign in failed: No response from the server. Check your connection and try again.');
set_error('No server response. Check your connection and retry.');
} else {
// API returns 'detail' for validation errors (FastAPI standard), 'error' for app-level errors
const reason = user_response?.detail || user_response?.error || 'Invalid User ID or Auth Key.';
alert('Sign in failed: ' + reason);
set_error(reason);
}
})
.then((response) => {
.then(() => {
if (!user_id) {
// Auth failed in the previous .then() — user has already been alerted
// Auth failed above — status already set
console.error('Auth (user_id+key): user_id not set after authentication attempt.');
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.
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
// We use enabled: 'all' and hidden: 'all' to ensure we find the person record even if
// Look up the person record linked to this user.
// We use enabled:'all' and hidden:'all' to find the record even if
// technical fields like 'hide' are NULL or the record is temporarily disabled.
ae_promises['person'] = core_func
.load_ae_obj_li__person({
api_cfg: $ae_api,
for_obj_type: 'account',
for_obj_id: $ae_loc.account_id,
qry_user_id: user_id, // The user_id_random from the above authentication
qry_user_id: user_id,
enabled: 'all',
hidden: 'all',
// params_json: params_json,
// params: params,
log_lvl: 0
})
.then((person_response) => {
// Safety Check: Ensure the response is valid and contains at least one record before accessing index 0.
// V3 CRUD returns 'id' as the random identifier; 'person_id_random' is a legacy field name.
const person_rec = person_response?.[0];
if (person_rec && (person_rec.id || person_rec.person_id_random)) {
console.log(
`Successfully loaded person (${user_id}):`,
person_rec
);
console.log(`Successfully loaded person (${user_id}):`, person_rec);
person_obj = person_rec;
person_id = person_rec.id ?? person_rec.person_id_random;
trigger = true; // Set trigger to true to indicate we can now sign in
trigger = true; // triggers sign_in() via $effect → page redirect
} else {
alert(
'Sign in failed: No person record is linked to this user account. Please contact your administrator.'
);
set_error('No person record linked to this account. Contact your administrator.');
}
})
.then(() => {
if (user_id && person_id) {
console.log(`Successfully authenticated and loaded user and person records: user_id: ${user_id}, person_id: ${person_id}`);
} else {
console.error(
'Auth (user_id+key): finished but missing user_id or person_id — sign_in() will not be called.'
);
if (!user_id || !person_id) {
console.error('Auth (user_id+key): finished but missing user_id or person_id — sign_in() will not be called.');
}
});
});
// 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.');
user_signin_status = 'signing_in';
user_signin_error = null;
ae_promises['user'] = await core_func
.auth_ae_obj__username_password({
api_cfg: $ae_api,
@@ -557,94 +534,58 @@
})
.then((user_response) => {
if (user_response?.user_id) {
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; // Use the user_id for further API calls
// person_id = user_obj.person_id_random;
console.log(`Successfully authenticated with Username (${user_response.username}) and Password:`, user_response);
user_obj = user_response;
user_id = user_obj.user_id;
} else if (!user_response) {
alert('Sign in failed: No response from the server. Check your connection and try again.');
set_error('No server response. Check your connection and retry.');
} else {
// API returns 'detail' for validation errors (FastAPI standard), 'error' for app-level errors
const reason = user_response?.detail || user_response?.error || 'Invalid username or password.';
alert('Sign in failed: ' + reason);
set_error(reason);
}
})
.then((response) => {
.then(() => {
if (!user_id) {
// Auth failed in the previous .then() — user has already been alerted
// Auth failed above — status already set
console.error('Auth (username+password): user_id not set after authentication attempt.');
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.
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
// We use enabled: 'all' and hidden: 'all' to ensure we find the person record even if
// Look up the person record linked to this user.
// We use enabled:'all' and hidden:'all' to find the record even if
// technical fields like 'hide' are NULL or the record is temporarily disabled.
ae_promises['person'] = core_func
.load_ae_obj_li__person({
api_cfg: $ae_api,
for_obj_type: 'account',
for_obj_id: $ae_loc.account_id,
qry_user_id: user_id, // The user_id_random from the above authentication
qry_user_id: user_id,
enabled: 'all',
hidden: 'all',
// params_json: params_json,
// params: params,
log_lvl: 0
})
.then((person_response) => {
// Safety Check: Ensure the response is valid and contains at least one record before accessing index 0.
// V3 CRUD returns 'id' as the random identifier; 'person_id_random' is a legacy field name.
const person_rec = person_response?.[0];
if (person_rec && (person_rec.id || person_rec.person_id_random)) {
console.log(
`Successfully loaded person (${user_id}):`,
person_rec
);
console.log(`Successfully loaded person (${user_id}):`, person_rec);
person_obj = person_rec;
person_id = person_rec.id ?? person_rec.person_id_random;
trigger = true; // Set trigger to true to indicate we can now sign in
trigger = true; // triggers sign_in() via $effect → page redirect
} else {
alert(
'Sign in failed: No person record is linked to this user account. Please contact your administrator.'
);
set_error('No person record linked to this account. Contact your administrator.');
}
})
.then(() => {
if (user_id && person_id) {
console.log(`Successfully authenticated and loaded user and person records: user_id: ${user_id}, person_id: ${person_id}`);
} else {
console.error(
'Auth (username+password): finished but missing user_id or person_id — sign_in() will not be called.'
);
if (!user_id || !person_id) {
console.error('Auth (username+password): finished but missing user_id or person_id — sign_in() will not be called.');
}
});
});
// 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
user_signin_status = 'no_credentials';
return false;
}
}}
>
@@ -689,15 +630,33 @@
<button
type="submit"
class="btn btn-sm preset-tonal-secondary border border-secondary-500 hover:preset-filled-secondary-500 w-full justify-center"
class="btn btn-sm w-full justify-center transition-all
{user_signin_status === 'error' ? 'preset-tonal-error border border-error-500'
: user_signin_status === 'signing_in' ? 'preset-tonal-surface opacity-70 cursor-wait'
: user_signin_status === 'no_credentials' ? 'preset-tonal-warning border border-warning-500'
: 'preset-tonal-secondary border border-secondary-500 hover:preset-filled-secondary-500'}"
disabled={user_signin_status === 'signing_in'}
title="Sign in with username and password"
onclick={() => { if (user_signin_status !== 'signing_in') { user_signin_status = null; user_signin_error = null; } }}
>
<UserCheck size="1em" class="shrink-0" />
<span class="ml-1">
{#if $ae_sess.auth__entered_user_id}User ID{:else}Username{/if}
Sign In
{#if user_signin_status === 'signing_in'}
Verifying…
{:else if user_signin_status === 'error'}
Failed — retry?
{:else if user_signin_status === 'no_credentials'}
Enter credentials first
{:else}
{$ae_sess.auth__entered_user_id ? 'User ID' : 'Username'} Sign In
{/if}
</span>
</button>
{#if user_signin_error}
<p class="text-xs text-error-600 dark:text-error-400 text-center leading-snug">
{user_signin_error}
</p>
{/if}
</form>
{:else}
<!-- Signed in: show username, optional change password, sign out -->