Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic authentication and account page #7

Merged
merged 2 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
feat: ✨ customers can now register
added registration page and cleaned up the form actions a tad
  • Loading branch information
joshistoast committed Jun 21, 2023
commit 787a6996cd752b67bc7ffd2a71dfca46758bc174
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ This is my attempt to svelte-ify Shopify's Hydrogen storefront features. It's a

## To Do

- [ ] Customer Auth and Account Management
- [ ] Product Search
- [ ] Collection Filtering

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"type": "module",
"dependencies": {
"@apollo/client": "^3.7.15",
"@apollo/client": "^3.7.16",
"graphql": "^16.6.0",
"isbot": "^3.6.12",
"svelte-kit-cookie-session": "^3.4.1",
Expand Down
16 changes: 8 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/lib/server/data/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,3 +538,18 @@ export const CUSTOMER_QUERY = gql`
}
}
`

export const CUSTOMER_CREATE_MUTATION = gql`
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
customer {
id
}
customerUserErrors {
code
field
message
}
}
}
`
10 changes: 10 additions & 0 deletions src/lib/validations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod'

export const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})

export const registerSchema = z.object({
...loginSchema.shape,
})
128 changes: 82 additions & 46 deletions src/routes/(app)/[[locale]]/account/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { PageServerLoad, Actions } from './$types'
import { fail, redirect } from '@sveltejs/kit'
import { error, fail, redirect } from '@sveltejs/kit'
import { useLocaleKey } from '$lib/utils'
import { LOGIN_MUTATION, CUSTOMER_QUERY } from '$lib/server/data'
import {
LOGIN_MUTATION,
CUSTOMER_QUERY,
CUSTOMER_CREATE_MUTATION,
} from '$lib/server/data'
import { setError, superValidate } from 'sveltekit-superforms/server'
import { z } from 'zod'
import type { Customer, CustomerAccessTokenCreatePayload } from '$lib/types'

const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
import type {
Customer,
CustomerAccessTokenCreatePayload,
CustomerCreatePayload,
} from '$lib/types'
import { loginSchema, registerSchema } from '$lib/validations'

const getCustomer = async (locals: App.Locals) => {
const { storefront, session, locale } = locals
Expand All @@ -29,6 +32,24 @@ const getCustomer = async (locals: App.Locals) => {
return data?.customer
}

const doLogin = async (locals: App.Locals, email: string, password: string) => {
const { storefront, session } = locals

const { data } = await storefront.mutate<{
customerAccessTokenCreate: CustomerAccessTokenCreatePayload
}>({
mutation: LOGIN_MUTATION,
variables: {
input: { email, password },
},
})
if (data?.customerAccessTokenCreate?.customerAccessToken?.accessToken) {
return data.customerAccessTokenCreate.customerAccessToken.accessToken
}

throw error(400, JSON.stringify(data?.customerAccessTokenCreate?.customerUserErrors))
}

export const load: PageServerLoad = async ({ locals, setHeaders }) => {
const { session, locale } = locals
const { customerAccessToken } = session.data
Expand Down Expand Up @@ -57,43 +78,23 @@ export const load: PageServerLoad = async ({ locals, setHeaders }) => {

export const actions: Actions = {
login: async ({ locals, request }) => {
const { storefront, session } = locals

// validate form
const { session } = locals
const form = await superValidate(request, loginSchema)
if (!form.valid)
return fail(400, { form })
const { email, password } = form.data

// perform login
const { data } = await storefront.mutate<{
customerAccessTokenCreate: CustomerAccessTokenCreatePayload
}>({
mutation: LOGIN_MUTATION,
variables: {
input: { email, password },
},
})

// set session or fail
if (data?.customerAccessTokenCreate?.customerAccessToken?.accessToken) {
await session.set({ 'customerAccessToken': data.customerAccessTokenCreate.customerAccessToken.accessToken })
} else {
data?.customerAccessTokenCreate?.customerUserErrors?.forEach(({ message }) => {
let field: 'email' | 'password' = 'email'
let stagedMessage = message

switch (message) {
case 'Unidentified customer':
stagedMessage = 'Invalid email or password'
field = 'password'
break
default:
break
}

setError(form, field, stagedMessage)
})

try {
// validate form
if (!form.valid)
return fail(400, { form })
const { email, password } = form.data

// perform login
const customerAccessToken = await doLogin(locals, email, password)
await session.set({ 'customerAccessToken': customerAccessToken })
} catch (err) {
if (err instanceof Error)
setError(form, 'password', 'Something went wrong, please try again later.')
else
setError(form, 'password', 'Invalid email or password.')
}

// return form
Expand All @@ -105,7 +106,42 @@ export const actions: Actions = {
const redirectPath = useLocaleKey(locals.locale) ? `/${useLocaleKey(locals.locale) }` : ''
throw redirect(302, redirectPath)
},
register: async ({ locals, request }) => {}, // TODO
register: async ({ locals, request }) => {
const { storefront, session } = locals
const form = await superValidate(request, registerSchema)

try {
// validate form
if (!form.valid)
return fail(400, { form })
const { email, password } = form.data

// perform registration
const { data } = await storefront.mutate<{
customerCreate: CustomerCreatePayload
}>({
mutation: CUSTOMER_CREATE_MUTATION,
variables: {
input: { email, password },
},
})
if (!data?.customerCreate?.customer?.id)
throw error(400, JSON.stringify(data?.customerCreate?.customerUserErrors))

// Login new customer
const customerAccessToken = await doLogin(locals, email, password)
await session.set({ 'customerAccessToken': customerAccessToken })

throw redirect(302, useLocaleKey(locals.locale) + '/account')
} catch (err) {
if (err instanceof Error)
setError(form, 'password', 'Something went wrong, please try again later.')
else
setError(form, 'password', 'Sorry. We could not create an account with this email. User might already exist, try to login instead.')
}

return { form }
},
edit: async ({ locals, request }) => {}, // TODO
recover: async ({ locals, request }) => {}, // TODO
activate: async ({ locals, request }) => {}, // TODO
Expand Down
7 changes: 1 addition & 6 deletions src/routes/(app)/[[locale]]/account/login/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import type { PageServerLoad } from './$types'
import { z } from 'zod'
import { superValidate } from 'sveltekit-superforms/server'
import {
redirect,
} from '@sveltejs/kit'
import { useLocaleKey } from '$lib/utils'

const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
import { loginSchema } from '$lib/validations'

export const load: PageServerLoad = async ({ locals }) => {
const { session, locale } = locals
Expand Down
5 changes: 4 additions & 1 deletion src/routes/(app)/[[locale]]/account/login/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { PageServerData } from './$types'
import { superForm } from 'sveltekit-superforms/client'

export let data: PageServerData
const { shop } = data.layout

const {
form,
Expand Down Expand Up @@ -53,8 +54,10 @@ const {
{#if $errors.password}<span class="invalid">{$errors.password}</span>{/if}

<button type="submit" class="bg-gray-100 hover:bg-gray-200">
Submit{#if $submitting}ting...{/if}
Log{#if $submitting}ging{/if} in
</button>

<p>New to {shop.name}? <a href="/account/register">Register</a></p>
</form>
</div>

Expand Down
18 changes: 18 additions & 0 deletions src/routes/(app)/[[locale]]/account/register/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types'
import { superValidate } from 'sveltekit-superforms/server'
import {
redirect,
} from '@sveltejs/kit'
import { useLocaleKey } from '$lib/utils'
import { loginSchema } from '$lib/validations'

export const load: PageServerLoad = async ({ locals }) => {
const { session, locale } = locals
const { customerAccessToken } = session.data

if (customerAccessToken)
throw redirect(302, `${useLocaleKey(locale) ? useLocaleKey(locale) : ''}/account`)

const form = await superValidate(loginSchema)
return { form }
}
67 changes: 67 additions & 0 deletions src/routes/(app)/[[locale]]/account/register/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts">
import type { PageServerData } from './$types'
import { superForm } from 'sveltekit-superforms/client'

export let data: PageServerData

const {
form,
enhance,
errors,
constraints,
submitting,
} = superForm(data.form)
</script>

<div>
<h1>Register</h1>

<form
method="POST"
action="/account?/register"
use:enhance
class="flex flex-col max-w-md gap-2"
>
<!-- svelte-ignore a11y-autofocus -->
<input
type="text"
id="email"
name="email"
autocomplete="email"
required
placeholder="Email address"
aria-label="Email address"
autofocus
aria-invalid={$errors.email ? 'true' : undefined}
bind:value={$form.email}
{...$constraints.email}
/>
{#if $errors.email}<span class="invalid">{$errors.email}</span>{/if}
<input
type="password"
id="password"
name="password"
autocomplete="current-password"
required
minlength="8"
placeholder="Password"
aria-label="Password"
aria-invalid={$errors.password ? 'true' : undefined}
bind:value={$form.password}
{...$constraints.password}
/>
{#if $errors.password}<span class="invalid">{$errors.password}</span>{/if}

<button type="submit" class="bg-gray-100 hover:bg-gray-200">
Creat{#if $submitting}ting{:else}e {/if} account
</button>

<p>Already have an account? <a href="/account/login">Log in</a></p>
</form>
</div>

<style>
.invalid {
color: red;
}
</style>