Signup fix (#331)

This commit is contained in:
Ben Allfree 2023-11-15 10:00:08 -08:00 committed by GitHub
parent 5b6c14a6e2
commit a826381be9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 215 additions and 140 deletions

View File

@ -31,7 +31,6 @@
"date-fns": "^2.30.0",
"highlight.js": "^11.8.0",
"pocketbase": "^0.19.0",
"random-word-slugs": "^0.1.6",
"sass": "^1.68.0",
"svelte": "^4.2.1",
"svelte-chartjs": "3.1.2",

View File

@ -1,29 +1,77 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import { client } from '$src/pocketbase-client'
import { handleInstanceGeneratorWidget } from '$util/database'
import { generateSlug } from 'random-word-slugs'
import { writable } from 'svelte/store'
import { slide } from 'svelte/transition'
export let isProcessing: boolean = false
export let isSignUpView: boolean = false
// Controls the spin animation of the instance regeneration button
let rotationCounter: number = 0
const instanceNameField = writable('')
const instanceInfo = writable({
name: '',
fetching: true,
available: false,
})
const generateSlug = async () => {
instanceInfo.update((info) => ({ ...info, fetching: true }))
const { instanceName: name } = await client().client.send(`/api/signup`, {})
instanceInfo.update((info) => ({
...info,
available: true,
name,
fetching: false,
}))
instanceNameField.set(name)
}
instanceNameField.subscribe(async (name) => {
if (name !== $instanceInfo.name) {
try {
instanceInfo.update((info) => ({
...info,
fetching: true,
}))
const res = await client().client.send(
`/api/signup?name=${encodeURIComponent(name)}`,
{},
)
instanceInfo.update((info) => ({
...info,
fetching: false,
available: true,
name,
}))
} catch (e) {
instanceInfo.update((info) => ({
...info,
fetching: false,
available: false,
name,
}))
}
}
})
generateSlug()
// Set up the variables to hold the form information
let email: string = ''
let password: string = ''
let instanceName: string = generateSlug(2)
let formError: string = ''
// Disable the form button until all fields are filled out
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled =
email.length === 0 || password.length === 0 || instanceName.length === 0
email.length === 0 ||
password.length === 0 ||
$instanceInfo.name.length === 0 ||
!$instanceInfo.available
// Generate a unique name for the PocketHost instance
const handleInstanceNameRegeneration = () => {
rotationCounter = rotationCounter + 180
instanceName = generateSlug(2)
generateSlug()
}
// Toggle between registration and login forms
@ -40,7 +88,7 @@
await handleInstanceGeneratorWidget(
email,
password,
instanceName,
$instanceInfo.name,
(error) => {
formError = error
},
@ -50,13 +98,54 @@
isProcessing = false
}
$: {
}
</script>
<form class="card-body" on:submit={handleSubmit}>
<h2 class="font-bold text-white mb-3 text-center text-2xl">
Register and Create Your <br />First Instance
You are 30 seconds away from your first <br />PocketBase Instance
</h2>
<div class="mb-3">
<label class="label" for="instance">
<span class="label-text">Instance Name</span>
</label>
<div class="input-group">
<input
type="text"
placeholder="instance-name"
bind:value={$instanceNameField}
id="instance"
class="input input-bordered w-full"
/>
<button
type="button"
class="btn btn-square"
on:click={handleInstanceNameRegeneration}
>
<i class="fa-solid fa-rotate"></i>
</button>
</div>
<div style="font-size: 15px; padding: 5px">
{#if $instanceInfo.fetching}
Verifying...
{:else if $instanceInfo.available}
<span class="text-success">
https://{$instanceInfo.name}.pockethost.io ✔︎</span
>
{:else}
<span class="text-error">
https://{$instanceInfo.name}.pockethost.io ❌</span
>
{/if}
</div>
</div>
<div class="mb-3">
<label class="label" for="id">
<span class="label-text">Email</span>
@ -73,7 +162,7 @@
/>
</div>
<div class="mb-3">
<div class="mb-12">
<label class="label" for="password">
<span class="label-text">Password</span>
</label>
@ -88,30 +177,6 @@
/>
</div>
<div class="mb-12">
<label class="label" for="instance">
<span class="label-text">Instance Name</span>
</label>
<div class="input-group">
<input
type="text"
placeholder="instance-name"
bind:value={instanceName}
id="instance"
class="input input-bordered w-full"
/>
<button
type="button"
class="btn btn-square"
on:click={handleInstanceNameRegeneration}
>
<i class="fa-solid fa-rotate"></i>
</button>
</div>
</div>
{#if formError}
<div transition:slide class="alert alert-error mb-5">
<i class="fa-solid fa-circle-exclamation"></i>

View File

@ -115,6 +115,13 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
): Promise<InstanceFields | undefined> =>
client.collection('instances').getOne<InstanceFields>(id)
const getInstanceBySubdomain = (
subdomain: InstanceFields['subdomain'],
): Promise<InstanceFields | undefined> =>
client
.collection('instances')
.getFirstListItem<InstanceFields>(`subdomain='${subdomain}'`)
const getAllInstancesById = async () =>
(await client.collection('instances').getFullList()).reduce(
(c, v) => {
@ -267,6 +274,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
getAuthStoreProps,
parseError,
getInstanceById,
getInstanceBySubdomain,
createInstance,
authViaEmail,
createUser,

View File

@ -3,32 +3,84 @@
import CardHeader from '$components/cards/CardHeader.svelte'
import { INSTANCE_URL } from '$src/env'
import { handleCreateNewInstance } from '$util/database'
import { generateSlug } from 'random-word-slugs'
import { slide } from 'svelte/transition'
import { writable } from 'svelte/store'
import { client } from '$src/pocketbase-client'
let instanceName: string = generateSlug(2)
let formError: string = ''
const instanceNameField = writable('')
const instanceInfo = writable({
name: '',
fetching: true,
available: false,
})
// Controls the spin animation of the instance regeneration button
let rotationCounter: number = 0
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled = instanceName.length === 0 || isSubmitting
const handleInstanceNameRegeneration = () => {
rotationCounter = rotationCounter + 180
instanceName = generateSlug(2)
const generateSlug = async () => {
instanceInfo.update((info) => ({ ...info, fetching: true }))
const { instanceName: name } = await client().client.send(`/api/signup`, {})
instanceInfo.update((info) => ({
...info,
available: true,
name,
fetching: false,
}))
instanceNameField.set(name)
}
instanceNameField.subscribe(async (name) => {
if (name !== $instanceInfo.name) {
try {
instanceInfo.update((info) => ({
...info,
fetching: true,
}))
await client().client.send(
`/api/signup?name=${encodeURIComponent(name)}`,
{},
)
instanceInfo.update((info) => ({
...info,
fetching: false,
available: true,
name,
}))
} catch (e) {
instanceInfo.update((info) => ({
...info,
fetching: false,
available: false,
name,
}))
}
}
})
// Generate the initial slug on load
generateSlug()
let formError: string = ''
let isSubmitting = false
// Disable the form button until all fields are filled out
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled =
$instanceInfo.name.length === 0 || !$instanceInfo.available
// Generate a unique name for the PocketHost instance
const handleInstanceNameRegeneration = () => {
generateSlug()
}
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault()
isSubmitting = true
formError = ''
await handleCreateNewInstance(instanceName, (error) => {
await handleCreateNewInstance($instanceNameField, (error) => {
formError = error
}).finally(() => {
}).finally(async () => {
isSubmitting = false
})
}
@ -50,7 +102,7 @@
<div class="flex rename-instance-form-container-query gap-4">
<input
type="text"
bind:value={instanceName}
bind:value={$instanceNameField}
class="input input-bordered w-full"
/>
@ -63,9 +115,19 @@
>
</div>
<h4 class="text-center font-bold py-12">
{INSTANCE_URL(instanceName)}
</h4>
<div style="font-size: 15px;" class="p-2 mb-8">
{#if $instanceInfo.fetching}
Verifying...
{:else if $instanceInfo.available}
<span class="text-success">
https://{$instanceInfo.name}.pockethost.io ✔︎</span
>
{:else}
<span class="text-error">
https://{$instanceInfo.name}.pockethost.io ❌</span
>
{/if}
</div>
{#if formError}
<div transition:slide class="alert alert-error mb-5">
@ -77,8 +139,16 @@
<div class="flex items-center justify-center gap-4">
<a href="/" class="btn">Cancel</a>
<button class="btn btn-primary" disabled={isFormButtonDisabled}>
Create <i class="bi bi-arrow-right-short" />
<button
type="submit"
class="btn btn-primary"
disabled={isFormButtonDisabled}
>
{#if isSubmitting}
<span class="loading loading-spinner loading-md"></span>
{:else}
Create <i class="bi bi-arrow-right-short" />
{/if}
</button>
</div>
</form>

View File

@ -28,7 +28,7 @@ export const handleLogin = async (
email: string,
password: string,
setError?: FormErrorHandler,
shouldRedirect: boolean = true,
redirect = '',
) => {
const { authViaEmail } = client()
// Reset the form error if the form is submitted
@ -37,8 +37,8 @@ export const handleLogin = async (
try {
await authViaEmail(email, password)
if (shouldRedirect) {
await goto('/')
if (redirect) {
await goto(redirect)
}
} catch (error) {
if (!(error instanceof Error)) {
@ -176,79 +176,19 @@ export const handleInstanceGeneratorWidget = async (
) => {
const { dbg, error, warn } = LoggerService()
const { user, parseError } = client()
try {
// Handle user creation/signin
// First, attempt to log in using the provided credentials.
// If they have a password manager or anything like that, it will have
// populated the form with their existing login. Try using it.
await handleLogin(email, password, undefined, false)
.then(() => {
dbg(`Account ${email} already exists. Logged in.`)
})
.catch((e) => {
warn(`Login failed, attempting account creation.`)
// This means login has failed.
// Either their credentials were incorrect, or the account
// did not exist, or there is a system issue.
// Try creating the account. This will fail if the email address
// is already in use.
return handleRegistration(email, password)
.then(() => {
dbg(`Account created, proceeding to log in.`)
// This means registration succeeded. That's good.
// Log in using the new credentials
return handleLogin(email, password, undefined, false)
.then(() => {
dbg(`Logged in after account creation`)
})
.catch((e) => {
error(`Panic, auth system down`)
// This should never happen.
// If registration succeeds, login should always succeed.
// If a login fails at this point, the system is broken.
throw new Error(
`Login system is currently down. Please contact us so we can fix this.`,
)
})
})
.catch((e) => {
warn(`User input error`)
// This is just for clarity
// If registration fails at this point, it means both
// login and account creation failed.
// This means there is something wrong with the user input.
// Bail out to show errors
// Transform the errors so they mention a problem with account creation.
const messages = parseError(e)
throw new Error(`Account creation: ${messages[0]}`)
})
})
dbg(`User before instance creation is `, user())
// We can only get here if we are successfully logged in using the credentials
// provided by the user.
// Instance creation could still fail if the name is taken
await handleCreateNewInstance(instanceName)
.then(() => {
dbg(`Creation of ${instanceName} succeeded`)
})
.catch((e) => {
warn(`Creation of ${instanceName} failed`)
// The instance creation could most likely fail if the name is taken.
// In any case, bail out to show errors.
if (e.data?.data?.subdomain?.code === 'validation_not_unique') {
// Handle this special and common case
throw new Error(`Instance name already taken.`)
}
// The errors remaining errors are kind of generic, so transofrm them into something about
// the instance name.
const messages = parseError(e)
throw new Error(`Instance creation: ${messages[0]}`)
})
} catch (error: any) {
error(`Caught widget error`, { error })
handleFormError(error, setError)
await client().client.send(`/api/signup`, {
method: 'POST',
body: { email, password, instanceName },
})
await handleLogin(email, password, setError)
const instance = await client().getInstanceBySubdomain(instanceName)
if (!instance) throw new Error(`This should never happen`)
window.location.href = `/app/instances/${instance.id}`
} catch (e) {
if (e instanceof Error) {
setError(e.message)
}
}
}

View File

@ -12,8 +12,8 @@ When Admin Sync is enabled, your pockethost.io account credentials will be copie
If you change your pockethost.io credentials while an instance is running, it will not be updated until the next time it is launched. To force your instance to shut down, place it in Maintenance Mode and then wait for the status to show as `idle`.
Admin Sync is enabeld by default. When an instance is first created, it will have an admin account matching your pockethost.io login. This is a security measure to prevent someone from creating the initial admin account before you've had a chance.
Admin Sync is enabled by default. When an instance is first created, it will have an admin account matching your pockethost.io login. This is a security measure to prevent someone from creating the initial admin account before you've had a chance.
## Admin Sync `Disabled`
Your pockethost.io credentails will not be copied to your instance on future invocations.
Your pockethost.io credentials will not be copied to your instance on future invocations.

7
pnpm-lock.yaml generated
View File

@ -264,9 +264,6 @@ importers:
pocketbase:
specifier: ^0.19.0
version: 0.19.0
random-word-slugs:
specifier: ^0.1.6
version: 0.1.7
sass:
specifier: ^1.68.0
version: 1.69.5
@ -5740,10 +5737,6 @@ packages:
engines: {node: '>=10'}
dev: true
/random-word-slugs@0.1.7:
resolution: {integrity: sha512-8cyzxOIDeLFvwSPTgCItMXHGT5ZPkjhuFKUTww06Xg1dNMXuGxIKlARvS7upk6JXIm41ZKXmtlKR1iCRWklKmg==}
dev: true
/rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true