mirror of
https://github.com/pockethost/pockethost.git
synced 2025-03-30 15:08:30 +00:00
Signup fix (#331)
This commit is contained in:
parent
5b6c14a6e2
commit
a826381be9
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
7
pnpm-lock.yaml
generated
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user