SSO auth (1)
This commit is contained in:
@@ -104,17 +104,11 @@ async setSceneInteractive(interactive: boolean, shouldClick: boolean) : Promise<
|
||||
async setPetMenuState(id: string, open: boolean) : Promise<void> {
|
||||
await TAURI_INVOKE("set_pet_menu_state", { id, open });
|
||||
},
|
||||
async login(email: string, password: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("login", { email, password });
|
||||
async startGoogleAuth() : Promise<null> {
|
||||
return await TAURI_INVOKE("start_google_auth");
|
||||
},
|
||||
async register(email: string, password: string, name: string | null, username: string | null) : Promise<string> {
|
||||
return await TAURI_INVOKE("register", { email, password, name, username });
|
||||
},
|
||||
async changePassword(currentPassword: string, newPassword: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("change_password", { currentPassword, newPassword });
|
||||
},
|
||||
async resetPassword(oldPassword: string, newPassword: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("reset_password", { oldPassword, newPassword });
|
||||
async startDiscordAuth() : Promise<null> {
|
||||
return await TAURI_INVOKE("start_discord_auth");
|
||||
},
|
||||
async logoutAndRestart() : Promise<null> {
|
||||
return await TAURI_INVOKE("logout_and_restart");
|
||||
@@ -133,6 +127,7 @@ async getModules() : Promise<ModuleMetadata[]> {
|
||||
export const events = __makeEvents__<{
|
||||
activeDollSpriteChanged: ActiveDollSpriteChanged,
|
||||
appDataRefreshed: AppDataRefreshed,
|
||||
authFlowUpdated: AuthFlowUpdated,
|
||||
createDoll: CreateDoll,
|
||||
cursorMoved: CursorMoved,
|
||||
editDoll: EditDoll,
|
||||
@@ -153,6 +148,7 @@ userStatusChanged: UserStatusChanged
|
||||
}>({
|
||||
activeDollSpriteChanged: "active-doll-sprite-changed",
|
||||
appDataRefreshed: "app-data-refreshed",
|
||||
authFlowUpdated: "auth-flow-updated",
|
||||
createDoll: "create-doll",
|
||||
cursorMoved: "cursor-moved",
|
||||
editDoll: "edit-doll",
|
||||
@@ -181,6 +177,9 @@ userStatusChanged: "user-status-changed"
|
||||
export type ActiveDollSpriteChanged = string | null
|
||||
export type AppConfig = { api_base_url: string | null }
|
||||
export type AppDataRefreshed = UserData
|
||||
export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled"
|
||||
export type AuthFlowUpdated = AuthFlowUpdatedPayload
|
||||
export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null }
|
||||
export type CreateDoll = null
|
||||
export type CreateDollDto = { name: string; configuration: DollConfigurationDto | null }
|
||||
export type CursorMoved = CursorPositions
|
||||
|
||||
@@ -4,14 +4,6 @@
|
||||
import Power from "../../../assets/icons/power.svelte";
|
||||
|
||||
let signingOut = false;
|
||||
let isChangingPassword = false;
|
||||
let passwordError = "";
|
||||
let passwordSuccess = "";
|
||||
let passwordForm = {
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
|
||||
async function handleSignOut() {
|
||||
if (signingOut) return;
|
||||
@@ -31,99 +23,23 @@
|
||||
console.error("Failed to open client config", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
if (isChangingPassword) return;
|
||||
passwordError = "";
|
||||
passwordSuccess = "";
|
||||
|
||||
if (!passwordForm.currentPassword || !passwordForm.newPassword) {
|
||||
passwordError = "Current and new password are required";
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
passwordError = "New password confirmation does not match";
|
||||
return;
|
||||
}
|
||||
|
||||
isChangingPassword = true;
|
||||
try {
|
||||
await commands.changePassword(
|
||||
passwordForm.currentPassword,
|
||||
passwordForm.newPassword,
|
||||
);
|
||||
passwordSuccess = "Password updated";
|
||||
passwordForm.currentPassword = "";
|
||||
passwordForm.newPassword = "";
|
||||
passwordForm.confirmPassword = "";
|
||||
} catch (error) {
|
||||
console.error("Failed to change password", error);
|
||||
passwordError = error instanceof Error ? error.message : "Unable to update password";
|
||||
} finally {
|
||||
isChangingPassword = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="size-full flex flex-col justify-between">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4 max-w-md">
|
||||
<p>{$appData?.user?.name}'s preferences</p>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button class="btn" class:btn-disabled={signingOut} onclick={handleSignOut}>
|
||||
<button
|
||||
class="btn"
|
||||
class:btn-disabled={signingOut}
|
||||
onclick={handleSignOut}
|
||||
>
|
||||
{signingOut ? "Signing out..." : "Sign out"}
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick={openClientConfig}>
|
||||
Advanced options
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider my-0"></div>
|
||||
<div class="flex flex-col gap-3 max-w-sm">
|
||||
<p class="text-sm opacity-70">Change password</p>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs opacity-60">Current password</span>
|
||||
<input
|
||||
class="input input-bordered input-sm"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
bind:value={passwordForm.currentPassword}
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs opacity-60">New password</span>
|
||||
<input
|
||||
class="input input-bordered input-sm"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
bind:value={passwordForm.newPassword}
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs opacity-60">Confirm new password</span>
|
||||
<input
|
||||
class="input input-bordered input-sm"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
bind:value={passwordForm.confirmPassword}
|
||||
/>
|
||||
</label>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-disabled={isChangingPassword}
|
||||
disabled={isChangingPassword}
|
||||
onclick={handleChangePassword}
|
||||
>
|
||||
{isChangingPassword ? "Updating..." : "Update password"}
|
||||
</button>
|
||||
{#if passwordSuccess}
|
||||
<span class="text-xs text-success">{passwordSuccess}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if passwordError}
|
||||
<p class="text-xs text-error">{passwordError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-row justify-between">
|
||||
<div></div>
|
||||
|
||||
@@ -1,61 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { commands } from "$lib/bindings";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { commands, events } from "$lib/bindings";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import DollPreview from "../app-menu/components/doll-preview.svelte";
|
||||
import ExternalLink from "../../assets/icons/external-link.svelte";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
|
||||
let isContinuing = false;
|
||||
let useRegister = false;
|
||||
let activeProvider: "google" | "discord" | null = null;
|
||||
let errorMessage = "";
|
||||
let form = {
|
||||
email: "",
|
||||
password: "",
|
||||
name: "",
|
||||
username: "",
|
||||
let unlistenAuthFlow: UnlistenFn | null = null;
|
||||
|
||||
type AuthFlowUpdatedPayload = {
|
||||
provider: string;
|
||||
status: "started" | "succeeded" | "failed" | "cancelled";
|
||||
message: string | null;
|
||||
};
|
||||
|
||||
const normalizeError = (value: unknown) => {
|
||||
if (value instanceof Error) {
|
||||
return value.message;
|
||||
}
|
||||
|
||||
return typeof value === "string" ? value : "Something went wrong";
|
||||
};
|
||||
|
||||
const handleContinue = async () => {
|
||||
if (isContinuing) return;
|
||||
if (!form.email.trim() || !form.password) {
|
||||
errorMessage = "Email and password are required";
|
||||
const startAuth = async (provider: "google" | "discord") => {
|
||||
activeProvider = provider;
|
||||
errorMessage = "";
|
||||
|
||||
try {
|
||||
if (provider === "google") {
|
||||
await commands.startGoogleAuth();
|
||||
} else {
|
||||
await commands.startDiscordAuth();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to start ${provider} auth`, error);
|
||||
errorMessage = normalizeError(error);
|
||||
if (activeProvider === provider) {
|
||||
activeProvider = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const providerLabel = (provider: "google" | "discord") =>
|
||||
provider === "google" ? "Google" : "Discord";
|
||||
|
||||
const handleAuthFlowUpdated = ({ payload }: { payload: AuthFlowUpdatedPayload }) => {
|
||||
const provider = payload.provider as "google" | "discord";
|
||||
if (activeProvider !== provider) {
|
||||
return;
|
||||
}
|
||||
isContinuing = true;
|
||||
errorMessage = "";
|
||||
try {
|
||||
if (useRegister) {
|
||||
await commands.register(
|
||||
form.email.trim(),
|
||||
form.password,
|
||||
form.name.trim() || null,
|
||||
form.username.trim() || null,
|
||||
);
|
||||
useRegister = false;
|
||||
resetRegisterFields();
|
||||
form.password = "";
|
||||
return;
|
||||
}
|
||||
|
||||
await commands.login(form.email.trim(), form.password);
|
||||
await getCurrentWebviewWindow().close();
|
||||
} catch (error) {
|
||||
console.error("Failed to authenticate", error);
|
||||
errorMessage = normalizeError(error);
|
||||
if (payload.status === "started") {
|
||||
return;
|
||||
}
|
||||
isContinuing = false;
|
||||
|
||||
activeProvider = null;
|
||||
|
||||
if (payload.status === "succeeded") {
|
||||
errorMessage = "";
|
||||
return;
|
||||
}
|
||||
|
||||
errorMessage = payload.message ?? `Unable to sign in with ${providerLabel(provider)}.`;
|
||||
};
|
||||
|
||||
const resetRegisterFields = () => {
|
||||
form.name = "";
|
||||
form.username = "";
|
||||
};
|
||||
onMount(async () => {
|
||||
unlistenAuthFlow = await events.authFlowUpdated.listen(handleAuthFlowUpdated);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unlistenAuthFlow?.();
|
||||
});
|
||||
|
||||
const openClientConfig = async () => {
|
||||
try {
|
||||
@@ -81,76 +97,24 @@
|
||||
a cute passive socialization layer!
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs opacity-60">Email</span>
|
||||
<input
|
||||
class="input input-bordered input-sm"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
bind:value={form.email}
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs opacity-60">Password</span>
|
||||
<input
|
||||
class="input input-bordered input-sm"
|
||||
type="password"
|
||||
autocomplete={useRegister
|
||||
? "new-password"
|
||||
: "current-password"}
|
||||
bind:value={form.password}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</label>
|
||||
{#if useRegister}
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs opacity-60">Name (optional)</span>
|
||||
<input
|
||||
class="input input-bordered input-sm"
|
||||
autocomplete="name"
|
||||
bind:value={form.name}
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs opacity-60">Username (optional)</span>
|
||||
<input
|
||||
class="input input-bordered input-sm"
|
||||
autocomplete="username"
|
||||
bind:value={form.username}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 max-w-80">
|
||||
<button
|
||||
class="btn btn-primary btn-xl"
|
||||
onclick={handleContinue}
|
||||
disabled={isContinuing}
|
||||
class="btn btn-primary btn-xl justify-between"
|
||||
onclick={() => startAuth("google")}
|
||||
>
|
||||
{#if isContinuing}
|
||||
Loading...
|
||||
{:else}
|
||||
<div class="scale-70">
|
||||
<ExternalLink />
|
||||
</div>
|
||||
{useRegister ? "Create account" : "Sign in"}
|
||||
{/if}
|
||||
<span>{activeProvider === "google" ? "Restart Google sign-in" : "Continue with Google"}</span>
|
||||
<div class="scale-70">
|
||||
<ExternalLink />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm px-0 justify-start"
|
||||
onclick={() => {
|
||||
useRegister = !useRegister;
|
||||
errorMessage = "";
|
||||
if (!useRegister) {
|
||||
resetRegisterFields();
|
||||
}
|
||||
}}
|
||||
class="btn btn-outline btn-xl justify-between"
|
||||
onclick={() => startAuth("discord")}
|
||||
>
|
||||
{useRegister
|
||||
? "Already have an account? Sign in"
|
||||
: "New here? Create an account"}
|
||||
<span>{activeProvider === "discord" ? "Restart Discord sign-in" : "Continue with Discord"}</span>
|
||||
<div class="scale-70">
|
||||
<ExternalLink />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link p-0 btn-sm text-base-content w-max"
|
||||
@@ -163,8 +127,12 @@
|
||||
{#if errorMessage}
|
||||
<p class="text-xs text-error max-w-72">{errorMessage}</p>
|
||||
{:else}
|
||||
<p class="text-xs opacity-50 max-w-60">
|
||||
An account is needed to identify you for connecting with friends.
|
||||
<p class="text-xs opacity-50 max-w-72">
|
||||
{#if activeProvider}
|
||||
Friendolls is waiting for the browser callback. Click either button again to restart sign-in at any time.
|
||||
{:else}
|
||||
Sign in through your browser, then return here once Friendolls finishes the handshake.
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user