SvelteKit+Superforms+Prisma+Luciaでログイン機能を爆速で実装する
はじめに
こんにちは!最近SvelteKitにハマっている素人エンジニアの菅原です。
本記事はSvelteKitでログイン機能を実装する流れを雑に書いた備忘録です。
全体的に詳しい説明はしていなかったり、例外処理を厳密にやっていない部分があるため、参考程度に見ていただければ幸いです。
作成するもの
Github: https://github.com/MichitoSugawara/sveltekit-lucia-daisyui
Vercel: https://sveltekit-lucia-daisyui.vercel.app/login
主な使用技術
SvelteKit
SvelteKitは、Svelteベースのフレームワークで、Webアプリケーションの構築を簡単かつ迅速に行うことができます。魅力が多すぎるのでとりあえず触ってみてください。
公式チュートリアルもかなり優秀です。
Superforms
Superformsは、フォームのサーバーサイドバリデーションとクライアントサイドの表示をサポートするSvelteKitのライブラリです。
tRPCと同じようにバリデーションにZodスキーマを使用しており、クライアント・サーバー間で型安全にデータをやり取りできますが、それに加えてフォームのスナップショットやイベントハンドリングなど、正直一回使うともう離れられないような便利なライブラリです。
Prisma
Typescript製の型安全なORMです。もうSQL文なんて書きたくない、そんなあなたに。
もうEloquentには戻れない。
Lucia
SvelteKit・Next.js等に対応している認証ライブラリ。
かなりシンプルに安全に認証・認可を実装できる。
Auth.js(NextAuth.js)同様複数プロバイダでの認証に対応している。
Sveltekitのプロジェクト作成
$ pnpm create svelte@latest sveltekit-lucia
◇ Which Svelte app template?
│ Skeleton project
│
◇ Add type checking with TypeScript?
│ Yes, using TypeScript syntax
│
◇ Select additional options (use arrow keys/space bar)
│ Add ESLint for code linting, Add Prettier for code formatting
code linting, Add Prettier for code formatting
ひとまず必要な画面をマークアップ
実装に取り掛かる前に、ひとまず認証機能で必要な以下3つの画面をマークアップしておきます。
- ログインページ(非ログインユーザーのみ)
- 新規登録ページ(非ログインユーザーのみ)
- ユーザーページ(ログインユーザーのみ)
本プロジェクトでは、認証によってアクセスできる/できないページを明確に分けるために(loggedIn)/(loggedOut)というフォルダを作りグループ分けしています。
SvelteKitはファイスシステムベースのルーターを採用していますが、()で囲むことでURLに影響を与えずにグループ化できます。
ログインページと新規登録ページ
<div>
<h1>ログイン</h1>
<form method="POST">
<label for="username">ユーザー名</label>
<input type="text" name="username" />
<label for="password">パスワード</label>
<input type="password" name="password" />
<div><button>ログイン</button></div>
<a href="/signup">サインアップ</a>
</form>
</div>
<div>
<h1>サインアップ</h1>
<form method="POST">
<label for="username">ユーザー名</label>
<input type="text" name="username" />
<label for="password">パスワード</label>
<input type="password" name="password" />
<label for="confirmPassword">パスワード(確認)</label>
<input type="password" name="confirmPassword"/>
<div><button>サインアップ</button></div>
<a href="/login">ログイン</a>
</form>
</div>
ユーザーページ
ただページを用意しておくだけ。
<h1>ユーザーページ</h1>
スタイルはとりあえずSakuraCSSを入れておく
ログイン・新規登録ページのデザインはのちのちDaisyUIで作成しますが、ひとまずSakuraCSSを入れておいていい感じにしておきます。
テーマも複数あり、入れるだけで割とそれっぽくなるので、CSSを書きたくない・時間がないときにはかなり重宝しますね...!
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
+ <!-- Normalize CSS -->
+ <link rel="stylesheet" href="https://unpkg.com/sakura.css/css/normalize.css" />
+ <!-- Sakura CSS -->
+ <link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura-vader.css" />
%sveltekit.head%
</head>
Superformsの導入
以下SuperformsのGet Startedを参考に、フォームデータの送信処理・バリデーションを実装していきます。
sveltekit-superformsとzodをインストール
SuperformsではZodスキーマを用いてバリデーションをここなうため、superformsとzodをインストールします。
$ pnpm i -D sveltekit-superforms zod
ログインフォーム用のZodスキーマ作成とSuperformsの適用
まずログインフォームのバリデーションに使うZodスキーマを作成します。
ログインフォームでは細かいバリデーションはせず、必須(1文字以上)のみ指定しておきます。
また、このままではZodスキーマでデフォルトで定義されている英語のエラーメッセージが返されてしまうので、メッセージは日本語で指定しておきます。
(デフォルトのエラーメッセージの日本語化については、zod-i18nというライブラリを使うことにより可能ですが、ここは別記事でのちのち紹介したいと思います。)
import { z } from 'zod';
export const loginSchema = z.object({
username: z.string().min(1, { message: 'ユーザー名を入力してください' }),
password: z.string().min(1, { message: 'パスワードを入力してください' })
});
export type loginSchemaType = typeof loginSchema;
次にサーバー側のファイルを作成します。
SvelteKitでは、レンダリング前に取得したいデータ(ローディングデータ)をload関数に、フォームデータを受け取った際の処理(フォームアクション)をactions関数として定義できます。
Superformsを使用する場合、ローディングデータとしてsuperValidateメソッドで初期化したインスタンスをページデータとしてクライアント側に渡します。
これにより、クライアント側で再度フォームデータの定義をすることなく、フォームへデータバインドするだけで済みます。
また、フォームアクションでも、同じsuperValidateメソッドにリクエストイベントを渡して使用することで、送信されたフォームデータをバリデーションした上で、バリデーション結果を含んだインスタンスが返されます。
これも同じようにクライアント側に返却することにより、バリデーション結果をクライアント側に表示させることができます。
以下ではバリデーションに失敗した場合は、ステータスBad Requestでインスタンスをそのまま返却します。
成功した場合には、ログイン処理を行うこととなりますが、ここは後ほど実装します。
import type { Actions, PageServerLoad } from './$types';
import { loginSchema } from '$lib/schemas/userSchema';
import { superValidate } from 'sveltekit-superforms/server';
import { fail } from '@sveltejs/kit';
const schema = loginSchema;
export const load = (async () => {
const form = await superValidate(schema);
return { form };
}) satisfies PageServerLoad;
export const actions: Actions = {
default: async (event) => {
// フォームデータのバリデーション
const form = await superValidate(event, schema);
if (!form.valid) {
return fail(400, { form });
}
// TODO: バリデーション後の処理
return { form };
}
};
クライアント側ではsuperFormメソッドによりクライアント側で使用する以下プロパティ・メソッドを初期化します。
- form: フォームへバインドする用のフォームデータ
- message: サーバー側で任意に指定できるメッセージ
- errors: バリデーションの結果
- submitting: フォームが送信中かどうかの真偽値(Superformsのenhanceアクションを使用中のみ)
- capture/restore: フォームデータのsnapshotに使うメソッド(地味に助かりますね...)
https://superforms.vercel.app/concepts/snapshots - enhance: Superformsのカスタムenhanceアクション(本当に助かる...)
https://superforms.vercel.app/concepts/enhance
今回は詳しい説明を省きますが、Superformsについても別途記事を書く予定です。
<script lang="ts">
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms/client';
export let data: PageData;
const { form, message, errors, submitting, capture, restore, enhance } = superForm(data.form, {taintedMessage: false});
export const snapshot = { capture, restore };
</script>
<div>
<h1>ログイン</h1>
{#if $message}<span class="invalid">{$message}</span>{/if}
<form method="POST" use:enhance>
<label for="username">ユーザー名</label>
<input type="text" name="username" bind:value={$form.username} disabled={$submitting} />
{#if $errors.username}<span class="invalid">{$errors.username[0]}</span>{/if}
<label for="password">パスワード</label>
<input type="password" name="password" bind:value={$form.password} disabled={$submitting} />
{#if $errors.password}<span class="invalid">{$errors.password[0]}</span>{/if}
<div><button disabled={$submitting}>ログイン</button></div>
<a href="/signup">サインアップ</a>
</form>
</div>
<style>
.invalid {
color: red;
}
</style>
以上でログインフォームのサーバーサイドバリデーションとその表示が実装できました。
Superforms最高です。
新規登録フォーム用のZodスキーマ作成とSuperformsの適用
ログインフォームと同様に新規登録フォームにもZodスキーマの作成とSuperformsを適用を行います。
import { z } from 'zod';
export const signupSchema = z
.object({
username: z.string().min(3, { message: 'ユーザー名は3文字以上で入力してください' }),
password: z
.string()
.min(8, { message: 'パスワードは8文字以上で入力してください' })
.regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$/, {
message: 'パスワードには、小文字・大文字・数字を一文字以上含めて8文字以上で入力してください'
}),
confirmPassword: z.string().min(8, { message: 'パスワードは8文字以上で入力してください' })
})
.refine((data) => data.password === data.confirmPassword, {
message: 'パスワードが一致していません',
path: ['confirmPassword']
});
export type signupSchemaType = typeof signupSchema;
import type { Actions, PageServerLoad } from './$types';
import { signupSchema } from '$lib/schemas/signupSchema';
import { superValidate } from 'sveltekit-superforms/server';
import { fail } from '@sveltejs/kit';
const schema = signupSchema;
export const load = (async () => {
const form = await superValidate(schema);
return { form };
}) satisfies PageServerLoad;
export const actions: Actions = {
default: async (event) => {
// フォームデータのバリデーション
const form = await superValidate(event, schema);
if (!form.valid) {
return fail(400, { form });
}
// TODO: バリデーション後の処理
return { form };
}
};
<script lang="ts">
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms/client';
export let data: PageData;
const { form, message, errors, submitting, capture, restore, enhance } = superForm(data.form, {
taintedMessage: false
});
export const snapshot = { capture, restore };
</script>
<div>
<h1>サインアップ</h1>
{#if $message}<span class="invalid">{$message}</span>{/if}
<form method="POST" use:enhance>
<label for="username">ユーザー名</label>
<input type="text" name="username" bind:value={$form.username} disabled={$submitting} />
{#if $errors.username}<span class="invalid">{$errors.username[0]}</span>{/if}
<label for="password">パスワード</label>
<input type="password" name="password" bind:value={$form.password} disabled={$submitting} />
{#if $errors.password}<span class="invalid">{$errors.password[0]}</span>{/if}
<label for="confirmPassword">パスワード(確認)</label>
<input
type="password"
name="confirmPassword"
bind:value={$form.confirmPassword}
disabled={$submitting}
/>
{#if $errors.confirmPassword}<span class="invalid">{$errors.confirmPassword[0]}</span>{/if}
<div><button disabled={$submitting}>サインアップ</button></div>
<a href="/login">ログイン</a>
</form>
</div>
<style>
.invalid {
color: red;
}
</style>
データベースの作成とPrismaのセットアップ
PostgreSQLのコンテナ立ち上げ
データベースには後々無料プランのあるVercel PostgreSQL, Neon, Supabase等のクラウドデータベースにデプロイすることを考え、PostgreSQLを使います。
以下Composeファイルを用意してPostgreSQLのDockerコンテナを立ち上げます。
version: '3.8'
services:
db:
image: postgres:latest
restart: unless-stopped
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
ports:
- '5432:5432'
volumes:
db-data:
バックグランドで実行してもらいたいので、dオプションを指定しておきます。
$ docker-compose up -d
Prismaのインストール
PostgreSQL用にPrismaのセットアップを行います。
$ pnpm i -D prisma && pnpm i @prisma/client
$ prisma init --datasource-provider postgresql
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
また、Prismaクライアントはよく使うので、モジュール化しておきます。
ただ、DBクライアントはサーバー側で使用するもので、クライアント側のファイルとして含まれないようにサーバー専用モジュールとしてlib/server内に配置しています。
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
Luciaの導入と認証機能の実装
lucia-authとprisma用のアダプターをインストールします。
※2023/10/20追記: Lucia2.0以降書き方が変わっているため、下記コマンドにバージョンを追記しました。
$ pnpm i lucia-auth@1.5.0 @lucia-auth/adapter-prisma@2.0.0
以下ドキュメントを参考にLuciaのインスタンスとその型をエクスポートするファイルを作成します。
Prisma同様、クライアント側に含まれないようにlib/server内に配置します。
import lucia from "lucia-auth";
import { sveltekit } from "lucia-auth/middleware";
import prisma from "@lucia-auth/adapter-prisma";
import { db } from "$lib/server/db";
import { dev } from "$app/environment";
export const auth = lucia({
adapter: prisma(db),
env: dev ? "DEV" : "PROD",
middleware: sveltekit()
});
export type Auth = typeof auth;
src/app.d.tsに認証ユーザーの型定義と後々使うlocals.authの型定義を追加します。
/// <reference types="@sveltejs/kit" />
/// <reference types="unplugin-icons/types/svelte" />
/// <reference types="lucia-auth" />
declare namespace Lucia {
type Auth = import('$lib/server/lucia').Auth;
type UserAttributes = import('@prisma/client').AuthUser;
}
declare global {
namespace App {
interface Locals {
auth: import('lucia-auth').AuthRequest;
}
}
}
export {};
スキーマ定義
Luciaでは認証のために以下3つのテーブルが必要になります。
- AuthUser
ユーザー情報を保存するために使用されます。このテーブルにより、ユーザーの基本情報を一元管理することができます。 - AuthSession
ユーザーのセッション情報を保存するために使用されます。このテーブルにより、ユーザーのログイン状態やセッションの有効期限などを管理することができます。 - AuthKey
ユーザーの認証方法との関係を表すキーを保存するために使用されます。ユーザー情報と分けることで、メール/パスワードやOAuthといった複数の認証方法を使うことが可能になります。
以下公式ドキュメントを参考にPrismaスキーマファイル作成します。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model AuthUser {
id String @id @unique
auth_session AuthSession[]
auth_key AuthKey[]
username String? @unique
@@map("auth_user")
}
model AuthSession {
id String @id @unique
user_id String
active_expires BigInt
idle_expires BigInt
auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade)
@@index([user_id])
@@map("auth_session")
}
model AuthKey {
id String @id @unique
hashed_password String?
user_id String
primary_key Boolean
expires BigInt?
auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade)
@@index([user_id])
@@map("auth_key")
}
以下コマンドでPrismaスキーマをデータベースに同期しておきます。
$ prisma db push
認証ミドルウェア
HooksでLuciaのリクエストハンドラをミドルウェアとして設定しておき、event.locals.authからセッション情報を取得できるようにします。
import { auth } from '$lib/server/lucia';
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
const authHandler: Handle = async ({ event, resolve }) => {
event.locals.auth = auth.handleRequest(event);
return await resolve(event);
};
export const handle = sequence(authHandler);
ログイン処理の実装
LuciaではuseKeyメソッドを使用して複数認証方法で認証することが可能です。
今回はユーザー名とパスワードで認証を行いたいため、'usersame'を指定してバリデーションされたユーザー名とパスワードを渡します。
認証が成功したら、createSessionメソッドでデータベースにセッション情報を作成し、setSessionメソッドでクッキーにセッションIDを保存します。
import type { Actions, PageServerLoad } from './$types';
import { loginSchema } from '$lib/schemas/userSchema';
import { superValidate } from 'sveltekit-superforms/server';
import { fail } from '@sveltejs/kit';
+import { auth } from '$lib/server/lucia';
const schema = loginSchema;
export const load = (async () => {
const form = await superValidate(schema);
return { form };
}) satisfies PageServerLoad;
export const actions: Actions = {
default: async (event) => {
// フォームデータのバリデーション
const form = await superValidate(event, schema);
if (!form.valid) {
return fail(400, { form });
}
- // TODO: バリデーション後の処理
+ // ログイン処理
+ try {
+ const key = await auth.useKey('username', form.data.username, form.data.password);
+ const session = await auth.createSession(key.userId);
+ event.locals.auth.setSession(session);
+ } catch {
+ return fail(400, { form: { ...form, message: 'ログインエラー' } });
+ }
return { form };
}
};
新規登録処理の実装
新規登録処理ではcreateUserメソッドを使用してユーザーを作成します。
また、ログイン同様にセッションの作成とクッキーへのセッションIDの保存を行います。
import type { Actions, PageServerLoad } from './$types';
import { signupSchema } from '$lib/schemas/signupSchema';
import { superValidate } from 'sveltekit-superforms/server';
import { fail } from '@sveltejs/kit';
+import { auth } from '$lib/server/lucia';
const schema = signupSchema;
export const load = (async () => {
const form = await superValidate(schema);
return { form };
}) satisfies PageServerLoad;
export const actions: Actions = {
default: async (event) => {
// フォームデータのバリデーション
const form = await superValidate(event, schema);
if (!form.valid) {
return fail(400, { form });
}
- // TODO: バリデーション後の処理
+ // サインアップ処理
+ try {
+ const user = await auth.createUser({
+ primaryKey: {
+ providerId: 'username',
+ providerUserId: form.data.username,
+ password: form.data.password
+ },
+ attributes: {
+ username: form.data.username
+ }
+ });
+ const session = await auth.createSession(user.userId);
+ event.locals.auth.setSession(session);
+ } catch {
+ return fail(400, { form: { ...form, message: 'サインアップエラー' } });
+ }
return { form };
}
};
ログアウト処理の実装
ログアウト処理は本来POSTメソッドで作成するのが望ましいですが、今回はGETメソッドで実装して簡単にログアウトできるようにしておきます。
データベースにあるセッション情報を無効化し、クッキーからセッションIDを削除することでログアウトが完了します。
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ locals }) => {
const session = await locals.auth.validate();
if (!session) throw error(404);
// セッションの無効化
await auth.invalidateSession(session.sessionId);
// クッキーからセッションID削除
locals.auth.setSession(null);
throw redirect(302, '/login');
};
ユーザーの認可
以上で認証の実装は完了しましたが、まだユーザーによってアクセスを制限する認可の実装が必要です。
以下で(loggedIn)と(loggedOut)配下のページに対するリクエストに対して、解決前に割り込んで適切にリダイレクトさせる認可の処理を実装しています。
これにより、ログインしていない状態でユーザーページへリクエストした際、解決前に強制的にログインページへリダイレクトされます。
また逆に、ログインしている状態でログイン・新規登録ページへリクエストした際、解決前に強制的にユーザーページへリダイレクトされます。
import { auth } from '$lib/server/lucia';
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
const authHandler: Handle = async ({ event, resolve }) => {
event.locals.auth = auth.handleRequest(event);
+ const user = await event.locals.auth.validate();
+
+ if (event.route.id?.startsWith('/(loggedIn)') && !user) {
+ return Response.redirect(`${event.url.origin}/login`, 302);
+ }
+
+ if (event.route.id?.startsWith('/(loggedOut)') && user) {
+ return Response.redirect(`${event.url.origin}/user`, 302);
+ }
return await resolve(event);
};
export const handle = sequence(authHandler);
ただ、ユーザーページは現状サーバー側での処理がないため、クライアントサイドナビゲーションではサーバー側のHooksは呼び出されず、ユーザーページにアクセスできてしまいます。
サーバー側のファイルを追加して、サーバー側のHooksが機能するようにしましょう。
import type { PageServerLoad } from './$types';
export const load = (async () => {
return {};
}) satisfies PageServerLoad;
これでログイン・サインアップ・認証済みページの保護が実装できました。
認証機能の実装は完了しましたが、SakuraCSSだけでは少し味気ないのでDaisyUIを導入してUIを調整していきます。
DaisyUIの導入とUIの調整
SakuraCSSのCDN削除
DaisyUIを使用するので、使用していたSakuraCSSを削除します。
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
- <!-- Normalize CSS -->
- <link rel="stylesheet" href="https://unpkg.com/sakura.css/css/normalize.css" />
- <!-- Sakura CSS -->
- <link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura-vader.css" />
%sveltekit.head%
</head>
DaisyUIのインストール
DaisyUIはTailwindCSSをベースとしているので、svelte-addを使用してTailwindCSSを追加します。
$ pnpx svelte-add@latest tailwindcss
以下を参考にDaisyUIをインストールし、tailwind.config.cjsのpluginsに追加します。
テーマはいろいろありますが、緑色が好きなのでlemonadeにしておきます。const config = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
+ daisyui: {
+ themes: ['lemonade']
+ },
- plugins: []
+ plugins: [require("daisyui")]
};
module.exports = config;
ローディングアニメーションの実装
現状フォームの送信は非同期、ナビゲーションはクライアントサイドで行っているためUXをよくするためにローディングアニメーションが欲しくなりますよね。
こちらも爆速で作れます。SvelteKitならね。
ローディングコンポーネントの作成
ここはお好みで作成してください。
以下は画面を少し暗くして真ん中に緑色の何かが回ります。
<div class="absolute w-full h-full bg-black z-10 opacity-10" />
<div class="absolute w-full h-full z-10 flex justify-center items-center">
<div class="animate-spin h-16 w-8 bg-primary rounded-xl" />
</div>
ストアの作成
まずグローバルにローディングとフォーム送信のステータスを状態管理するために、下記ストアファイル作成します。
import { writable } from 'svelte/store';
export const loading = writable(false);
export const submitting = writable(false);
レイアウトにローディングコンポーネント反映
以下で遷移中もしくはsubmittingストアがtrueのときにローディングコンポーネントが表示されます。
そのため、各フォームがあるページから送信時にsubmittingストアをtrueにする必要があります。
<script>
import '../app.postcss';
import { navigating } from '$app/stores';
import Loading from '$lib/components/Loading.svelte';
import { loading, submitting } from '$lib/stores';
// submittingストアがtrue時にページ遷移が起きた場合はsubmittingストアをリセット
$: if ($navigating) $submitting = false;
// ページ遷移中もしくはsubmittingストア
$: $loading = !!$navigating || $submitting;
</script>
{#if $loading}
<Loading />
{/if}
<slot />
ログイン画面の調整
<script lang="ts">
import { submitting as submittingStore } from '$lib/stores';
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms/client';
export let data: PageData;
const { form, message, errors, submitting, enhance, capture, restore } = superForm(data.form, {
taintedMessage: false
});
$: $submittingStore = $submitting;
export const snapshot = { capture, restore };
</script>
<div class="relative flex flex-col items-center justify-center h-screen overflow-hidden">
<div class="w-full lg:max-w-lg p-6 m-auto rounded-md shadow-2xl">
<h1 class="text-3xl font-semibold text-center text-primary">ログイン</h1>
{#if $message}<span class="text-sm text-red-600">{$message}</span>{/if}
<form class="space-y-4" method="POST" use:enhance>
<div>
<label class="label" for="username"
><span class="text-base label-text">ユーザー名</span></label
>
<input
class="w-full input input-bordered input-primary {$errors.username || $message
? 'input-error'
: ''}"
type="text"
name="username"
bind:value={$form.username}
disabled={$submitting}
/>
{#if $errors.username}<span class="text-xs text-red-600">{$errors.username[0]}</span>{/if}
</div>
<div>
<label class="label" for="password"
><span class="text-base label-text">パスワード</span></label
>
<input
class="w-full input input-bordered input-primary {$errors.password || $message
? 'input-error'
: ''}"
type="password"
name="password"
bind:value={$form.password}
disabled={$submitting}
/>
{#if $errors.password}<span class="text-xs text-red-600">{$errors.password[0]}</span>{/if}
</div>
<div><button class="btn btn-block btn-primary" disabled={$submitting}>ログイン</button></div>
<div>
<span class="text-sm">アカウントをお持ちでない方は</span>
<a class="text-sm text-blue-600 hover:underline hover:text-blue-400" href="/signup"
>新規登録</a
>
</div>
</form>
</div>
</div>
サインアップ画面の調整
<script lang="ts">
import { submitting as submittingStore } from '$lib/stores';
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms/client';
export let data: PageData;
const { form, message, errors, submitting, enhance, capture, restore } = superForm(data.form, {
taintedMessage: false
});
$: $submittingStore = $submitting;
export const snapshot = { capture, restore };
</script>
<div class="relative flex flex-col items-center justify-center h-screen overflow-hidden">
<div class="w-full lg:max-w-lg p-6 m-auto rounded-md shadow-2xl">
<h1 class="text-3xl font-semibold text-center text-primary">新規登録</h1>
{#if $message}<span class="text-sm text-red-600">{$message}</span>{/if}
<form class="space-y-4" method="POST" use:enhance>
<div>
<label class="label" for="username"
><span class="text-base label-text">ユーザー名</span></label
>
<input
class="w-full input input-bordered input-primary {$errors.username || $message
? 'input-error'
: ''}"
type="text"
name="username"
bind:value={$form.username}
/>
{#if $errors.username}<span class="text-xs text-red-600">{$errors.username[0]}</span>{/if}
</div>
<div>
<label class="label" for="password"
><span class="text-base label-text">パスワード</span></label
>
<input
class="w-full input input-bordered input-primary {$errors.password || $message
? 'input-error'
: ''}"
type="password"
name="password"
bind:value={$form.password}
disabled={$submitting}
/>
{#if $errors.password}<span class="text-xs text-red-600">{$errors.password[0]}</span>{/if}
</div>
<div>
<label class="label" for="confirmPassword"
><span class="text-base label-text">パスワード(確認)</span></label
>
<input
class="w-full input input-bordered input-primary {$errors.confirmPassword || $message
? 'input-error'
: ''}"
type="password"
name="confirmPassword"
bind:value={$form.confirmPassword}
disabled={$submitting}
/>
{#if $errors.confirmPassword}<span class="text-xs text-red-600"
>{$errors.confirmPassword[0]}</span
>{/if}
</div>
<div><button class="btn btn-block btn-primary" disabled={$submitting}>新規登録</button></div>
<div>
<span class="text-sm">すでにアカウントをお持ちの方は</span>
<a class="text-sm text-blue-600 hover:underline hover:text-blue-400" href="/login"
>ログイン</a
>
</div>
</form>
</div>
</div>
おわりに
型安全って最高ですよね。
Discussion
アカウントをリンクさせることって可能ですか?
ありがとうございます!
Luciaの認証の仕様についてでしょうか?
1アカウントに複数のログイン手段をリンクさせることは可能です!
みたいですね! 質問した日からちょっとだけ勉強しました!笑