Skip to content

Commit

Permalink
Merge pull request #14 from MIERUNE/demo-resetmail-magiclink
Browse files Browse the repository at this point in the history
password reset and magic link
  • Loading branch information
ciscorn authored Dec 3, 2024
2 parents f3d8f04 + 0286358 commit 04c4045
Show file tree
Hide file tree
Showing 16 changed files with 120 additions and 67 deletions.
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dotenv
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ node_modules
/dist
.wrangler

# npm
package-lock.json

# OS
.DS_Store
Thumbs.db
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,11 @@ npm install -D @mierune/sveltekit-firebase-auth-ssr
3. **Implement sign-in and sign-out functionality** in your application. (Example: TODO)
4. Use the user information and implement database integration if needed.
5. Ensure that the required environment variables are set in the execution environment.


## Development

```bash
direnv allow
pnpm dev-in-emulator
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"test": "npm run test:integration && npm run test:unit",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"start-firebase-emulator": "firebase emulators:start --project fukada-delete-me",
"start-emulator": "firebase emulators:start --project fukada-delete-me",
"dev-in-emulator": "firebase emulators:exec --project fukada-delete-me 'pnpm run dev'",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:integration": "playwright test",
Expand Down
2 changes: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const app = new Hono<{ Bindings: Env; Variables: AuthVariables }>()
const currentUser = ensureUser(c);
const posts = Array.from({ length: 20 }, () => ({
title: 'Great Article',
author: currentUser.name
author: currentUser.name ?? 'Unknown'
}));
return c.json(posts);
});
Expand Down
18 changes: 1 addition & 17 deletions src/lib/firebase-auth/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,17 @@ export async function signInWithTwitter() {
await signInWithProvider(provider);
}

// export async function refreshSession() {
// const auth = getAuth();
// await auth.authStateReady();
// if (auth.currentUser) {
// const idToken = await auth.currentUser.getIdToken(true);
// console.log(auth.currentUser.emailVerified, idToken);
// await updateSession(idToken);
// invalidate('auth:session');
// }
// }

export async function signInWithEmailAndPassword(email: string, password: string) {
const auth = getAuth();
const cred = await _signInWithEmailAndPassword(auth, email, password);
await updateSession(await cred.user.getIdToken());
invalidate('auth:session');
return cred;
}

export async function createUserWithEmailAndPassword(email: string, password: string) {
const auth = getAuth();
const cred = await _createUserWithEmailAndPassword(auth, email, password);
await updateSession(await cred.user.getIdToken());
invalidate('auth:session');
return cred;
}

Expand All @@ -97,7 +84,6 @@ export async function signInWithProvider(provider: AuthProvider, withRedirect =
// Fall back to sign-in by popup method if authDomain is different from location.host
const cred = await signInWithPopup(auth, provider);
await updateSession(await cred.user.getIdToken());
invalidate('auth:session');
}
}

Expand All @@ -122,9 +108,7 @@ export async function updateSession(idToken: string | undefined) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken })
});
if (previousIdToken) {
invalidate('auth:session');
}
invalidate('auth:session');
previousIdToken = idToken;
}

Expand Down
32 changes: 17 additions & 15 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { getAuth, sendEmailVerification } from 'firebase/auth';
import { signOut } from '$lib/firebase-auth/client';
import type { PageData } from './$types';
import { getAuth, sendEmailVerification } from 'firebase/auth';
import { page } from '$app/stores';
let {
Expand All @@ -17,10 +17,26 @@
const auth = getAuth();
if (auth.currentUser) {
await sendEmailVerification(auth.currentUser, { url: $page.url.origin + '/verify_email' });
alert('Verification email sent!');
}
}
</script>

{#if data.currentIdToken !== undefined}
<p>
Current User: <code>{JSON.stringify(data.currentUser)}</code>
<button onclick={signOut} disabled={data.currentUser === undefined}>Logout</button>
</p>
{#if data.currentUser?.email_verified === false}
<p style="color: white; background-color: brown; padding: 0.3em;">
Your email isn’t verified yet. Check your inbox for the verification link. <button
onclick={() => _sendEmailVerification()}>Resend verification email.</button
>
</p>
{/if}
<hr />
{/if}

<p>
GitHub: <a href="https://github.com/MIERUNE/sveltekit-firebase-auth-ssr" target="_blank"
>MIERUNE/sveltekit-firebase-auth-ssr</a
Expand All @@ -36,18 +52,4 @@
{/if}
</ul>

{#if data.currentIdToken !== undefined}
<p>
<code>{JSON.stringify(data.currentUser)}</code>
<button onclick={signOut} disabled={data.currentUser === undefined}>Logout</button>
</p>
{#if data.currentUser?.email_verified === false}
<p style="color: red;">
Your email address is not verified yet. <button onclick={() => _sendEmailVerification()}
>Resend verification email.</button
>
</p>
{/if}
{/if}

{@render children()}
3 changes: 2 additions & 1 deletion src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

<p>Home</p>
71 changes: 57 additions & 14 deletions src/routes/login/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
<script lang="ts">
import { FirebaseError } from 'firebase/app';
import { getAuth, sendEmailVerification, sendPasswordResetEmail } from 'firebase/auth';
import {
getAuth,
sendEmailVerification,
sendPasswordResetEmail,
sendSignInLinkToEmail,
signInWithEmailLink,
isSignInWithEmailLink
} from 'firebase/auth';
import {
signInWithGoogle,
waitForRedirectResult,
signInWithEmailAndPassword,
createUserWithEmailAndPassword
} from '$lib/firebase-auth/client';
import { page } from '$app/stores';
import { browser } from '$app/environment';
const redirectResult = waitForRedirectResult();
Expand All @@ -17,6 +25,21 @@
let password = $state('');
let errorCode = $state('');
if (browser) {
const auth = getAuth();
if (isSignInWithEmailLink(auth, window.location.href)) {
let email = window.localStorage.getItem('emailForSignIn');
if (!email) {
email = window.prompt('Please provide your email for confirmation');
}
if (email) {
signInWithEmailLink(auth, email, window.location.href).then((result) => {
window.localStorage.removeItem('emailForSignIn');
});
}
}
}
async function signInWithPassword() {
try {
await signInWithEmailAndPassword(email, password);
Expand All @@ -27,6 +50,23 @@
}
}
async function sendMagicLink() {
try {
const auth = getAuth();
await sendSignInLinkToEmail(auth, email, {
url: $page.url.origin + '/login?email-link',
handleCodeInApp: true,
dynamicLinkDomain: $page.url.hostname
});
window.localStorage.setItem('emailForSignIn', email);
alert("We've sent you an email with a link to sign in!");
} catch (error) {
if (error instanceof FirebaseError) {
errorCode = error.code;
}
}
}
async function signUpWithPassword() {
try {
const cred = await createUserWithEmailAndPassword(email, password);
Expand All @@ -41,9 +81,14 @@
async function resetPassword() {
if (email !== '') {
const auth = getAuth();
await sendPasswordResetEmail(auth, email, {
url: $page.url.origin + '/login'
});
try {
await sendPasswordResetEmail(auth, email, { url: $page.url.origin + '/login' });
} catch (error) {
if (error instanceof FirebaseError) {
errorCode = error.code;
return;
}
}
alert('Password reset email sent.');
}
email = '';
Expand All @@ -57,26 +102,23 @@
{:then result}
{#if !result}
{#if $page.url.searchParams.get('next')}
<p>You need to log in.</p>
<p>You need to log in to view this page.</p>
{/if}

Single Sign-On:
<button onclick={signInWithGoogle} disabled={data.currentIdToken !== undefined}
>Sign-in with Google</button
>
<!--
<button onclick={signInWithTwitter} disabled={data.currentIdToken !== undefined}
>Sign-in with Twitter</button
>
-->
<hr />
<div>
<p>Or sign-in with email and password:</p>
{#if errorCode}
<p style="color: red;">{errorCode}</p>
{/if}
<p>
<label
>Email: <input
type="text"
type="email"
size="30"
bind:value={email}
placeholder="[email protected]"
Expand All @@ -92,9 +134,10 @@
>
</p>
<p>
<button onclick={signInWithPassword}>Sign-In</button>
<button onclick={signUpWithPassword}>Sign-Up</button>
<button onclick={resetPassword}>Reset Password</button>
<button onclick={signInWithPassword} disabled={!email || !password}>Sign-In</button>
<button onclick={signUpWithPassword} disabled={!email || !password}>Sign-Up</button>
<button onclick={resetPassword} disabled={!email}>Reset Password</button>
<button onclick={sendMagicLink} disabled={!email}>Send Magic Link</button>
</p>
</div>
{/if}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/private/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { redirect } from '@sveltejs/kit';
export async function load({ locals, url, depends }) {
depends('auth:session');
if (!locals.currentIdToken) {
redirect(303, '/login?next=' + encodeURIComponent(url.pathname + url.search));
redirect(307, '/login?next=' + encodeURIComponent(url.pathname + url.search));
}
}
4 changes: 3 additions & 1 deletion src/routes/shop/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { redirect } from '@sveltejs/kit';

export async function load({ locals, url }) {
if (!locals.currentIdToken) {
redirect(303, '/login?next=' + encodeURIComponent(url.pathname + url.search));
redirect(307, '/login?next=' + encodeURIComponent(url.pathname + url.search));
} else if (locals.currentIdToken.email_verified === false) {
redirect(307, '/verify_email');
}
}
4 changes: 2 additions & 2 deletions src/routes/shop/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ Menu:
</li>
<li>
<a href="/shop/subscription/price_1PkepxLtNIgQdVME3rIN4nel"
>Subscription: Monthly MIERUNE (Ultimate Edition)</a
>Subscription: Mouthly MIERUNE (Ultimate Edition)</a
>
</li>
</ul>

<p><a href="/shop/billing-portal">Go to Stripe Customer Portal</a></p>
<p><a href="/shop/billing-portal">Go to the Stripe Customer Portal</a></p>
6 changes: 3 additions & 3 deletions src/routes/shop/billing-portal/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { stripe } from '$lib/stripe/stripe';

export async function GET({ url, cookies, locals }) {
if (!locals.currentIdToken) {
redirect(303, '/');
redirect(307, '/');
}
const email = locals.currentIdToken.email || '';

Expand All @@ -19,13 +19,13 @@ export async function GET({ url, cookies, locals }) {
}

if (customerId === undefined) {
throw redirect(303, '/');
throw redirect(307, '/');
}

const billingPortalSession = await stripe.billingPortal.sessions.create({
// configuration: bpc.id, // Billing Portal をカスタムする場合に使用
customer: customerId,
return_url: url.origin + '/'
});
throw redirect(302, billingPortalSession.url);
throw redirect(307, billingPortalSession.url);
}
8 changes: 4 additions & 4 deletions src/routes/shop/payment/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { redirect } from '@sveltejs/kit';

export async function load({ url, cookies, locals }) {
if (!locals.currentIdToken) {
redirect(303, '/');
redirect(307, '/');
}
const email = locals.currentIdToken.email;

// マネしちゃだめ!
// 永続化層がないので、 Cookieを使ってStripeの顧客IDを保持しておく
// DO NOT DO THIS IN PRODUCTION!!!
// DO NOT DO THIS IN PRODUCTION!!!
let customerId = cookies.get('customer_id');
if (!customerId) {
const customer = await stripe.customers.create({
Expand All @@ -23,7 +23,7 @@ export async function load({ url, cookies, locals }) {
customer: customerId,
line_items: [
{
price: 'price_1PkKYCLtNIgQdVMEIO2U71nd', // どら焼き
price: 'price_1PkKYCLtNIgQdVMEIO2U71nd', // Dorayaki
quantity: 1
}
],
Expand Down
9 changes: 5 additions & 4 deletions src/routes/shop/subscription/[price_id]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { redirect } from '@sveltejs/kit';

export async function load({ url, cookies, params, locals }) {
if (!locals.currentIdToken) {
redirect(303, '/');
redirect(307, '/');
}
const email = locals.currentIdToken.email || '';

// マネしちゃだめ! URL から価格IDを取得する
// DO NOT DO THIS IN PRODUCTION!!!
// DO NOT DO THIS IN PRODUCTION!!!
const priceId = params.price_id;

// マネしちゃだめ!
// 永続化層がないので、 Cookieを使ってStripeの顧客IDを保持しておく
// DO NOT DO THIS IN PRODUCTION!!!
// DO NOT DO THIS IN PRODUCTION!!!
let customerId = cookies.get('customer_id');
if (!customerId) {
const customer = await stripe.customers.create({
Expand Down
Loading

0 comments on commit 04c4045

Please sign in to comment.