Prisma と Lucia を使った認証システム

更新


プログラミング/svelte

Prisma と Lucia を使った認証システムを作ってみる

SvelteKit+Superforms+Prisma+Luciaでログイン機能を爆速で実装する
https://zenn.dev/gawarago/articles/f75f5113a3803d

を参考に以下を使ったサイトを作る基本を身に着けたいなと。

  • Svelte
  • SvelteKit
  • SuperForms : フォームバリデーション
  • Prisma : データベースアクセス
  • Lucia : 認証ライブラリ
  • sveltekit-flash-message : flash メッセージの管理
  • svelte-french-toast : flash メッセージ(など)を一定時間だけ表示する

目次

スケルトンプロジェクトを作成

LANG:console
$ pnpm create svelte@latest authtest
 .../Local/pnpm/store/v3/tmp/dlx-30292    |   +6 +
 Packages are hard linked from the content-addressable store to the virtual store.
   Content-addressable store is at: C:\Users\osamu\AppData\Local\pnpm\store\v3
   Virtual store is at:             ../../AppData/Local/pnpm/store/v3/tmp/dlx-30292/node_modules/.pnpm
 .../Local/pnpm/store/v3/tmp/dlx-30292    | Progress: resolved 6, reused 5, downloaded 1, added 6, done
 
 create-svelte version 5.3.2
 
 ┌  Welcome to SvelteKit!
 │
 ◇  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, Add Playwright for browser testing, Add Vitest for unit testing
 │
 └  Your project is ready!
 
 ✔ Typescript
   Inside Svelte components, use <script lang="ts">
 
 ✔ ESLint
   https://github.com/sveltejs/eslint-plugin-svelte
 
 ✔ Prettier
   https://prettier.io/docs/en/options.html
   https://github.com/sveltejs/prettier-plugin-svelte#options
 
 ✔ Playwright
   https://playwright.dev
 
 ✔ Vitest
   https://vitest.dev
 
 Install community-maintained integrations:
   https://github.com/svelte-add/svelte-add
 
 Next steps:
   1: cd authtest
   2: pnpm install
   3: git init && git add -A && git commit -m "Initial commit" (optional)
   4: pnpm run dev -- --open
 
 To close the dev server, hit Ctrl-C
 
 Stuck? Visit us at https://svelte.dev/chat

そして指示の通りに、

LANG: console
$ cd authtest

$ pnpm install
  WARN  deprecated @playwright/test@1.28.1: Please update to the latest version of Playwright to test up-to-date browsers.
 Packages: +271
 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++  
 Packages are hard linked from the content-addressable store to the virtual store.
   Content-addressable store is at: C:\Users\osamu\AppData\Local\pnpm\store\v3    
   Virtual store is at:             node_modules/.pnpm
 Progress: resolved 293, reused 271, downloaded 0, added 271, done
 node_modules/.pnpm/@sveltejs+kit@1.27.4_svelte@4.2.7_vite@4.4.2/node_modules/@sveltejs/kit: Running postinstall script, done in 3.9s
 
 devDependencies:
 + @playwright/test 1.28.1 (1.40.0 is available) deprecated
 + @sveltejs/adapter-auto 2.0.0 (2.1.1 is available)
 + @sveltejs/kit 1.27.4 (1.27.6 is available)
 + @typescript-eslint/eslint-plugin 6.0.0 (6.13.1 is available)
 + @typescript-eslint/parser 6.0.0 (6.13.1 is available)
 + eslint 8.28.0 (8.54.0 is available)
 + eslint-config-prettier 9.0.0
 + eslint-plugin-svelte 2.30.0 (2.35.1 is available)
 + prettier 3.0.0 (3.1.0 is available)
 + prettier-plugin-svelte 3.0.0 (3.1.2 is available)
 + svelte 4.2.7
 + svelte-check 3.6.0 (3.6.2 is available)
 + tslib 2.4.1 (2.6.2 is available)
 + typescript 5.0.2 (5.3.2 is available)
 + vite 4.4.2 (5.0.2 is available)
 + vitest 0.32.2 (0.34.6 is available)
 
 Done in 14.3s
 
$ git init && git add -A && git commit -m "Initial commit"
 Initialized empty Git repository in C:/Users/osamu/Desktop/svelte/authtest/.git/
 [master (root-commit) 7646d52] Initial commit
  20 files changed, 2588 insertions(+)
  create mode 100644 .eslintignore
  create mode 100644 .eslintrc.cjs
  create mode 100644 .gitignore
  create mode 100644 .npmrc
  create mode 100644 .prettierignore
  create mode 100644 .prettierrc
  create mode 100644 README.md
  create mode 100644 package.json
  create mode 100644 playwright.config.ts
  create mode 100644 pnpm-lock.yaml
  create mode 100644 src/app.d.ts
  create mode 100644 src/app.html
  create mode 100644 src/index.test.ts
  create mode 100644 src/lib/index.ts
  create mode 100644 src/routes/+page.svelte
  create mode 100644 static/favicon.png
  create mode 100644 svelte.config.js
  create mode 100644 tests/test.ts
  create mode 100644 tsconfig.json
  create mode 100644 vite.config.ts

$ pnpm dev
 
 > authtest@0.0.1 dev C:\Users\osamu\Desktop\svelte\authtest
 > vite dev
 
 
 Forced re-optimization of dependencies
 
   VITE v4.4.2  ready in 1947 ms
 
   ➜  Local:   http://localhost:5173/
   ➜  Network: use --host to expose
   ➜  press h to show help

キーボードで "o" を押すとブラウザが立ち上がって http://localhost:5173/ に Welcome to SvelteKit が表示された。

コンポーネントのアップデート

インストール時にいろいろ古いと言われていたのでアップデートする。

LANG: console
^C
 ELIFECYCLE  Command failed with exit code 1.

として pnpm dev を抜けてから、

LANG: console
$ pnpm update
 Packages: +48 -56
 ++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------
 Progress: resolved 286, reused 263, downloaded 0, added 48, done
 node_modules/.pnpm/@sveltejs+kit@1.27.6_svelte@4.2.7_vite@4.5.0/node_modules/@sveltejs/kit: Running postinstall script, done in 4.1s
 
 devDependencies:
 - @playwright/test 1.28.1
 + @playwright/test 1.40.0
 - @sveltejs/adapter-auto 2.0.0
 + @sveltejs/adapter-auto 2.1.1
 - @sveltejs/kit 1.27.4
 + @sveltejs/kit 1.27.6
 - @typescript-eslint/eslint-plugin 6.0.0
 + @typescript-eslint/eslint-plugin 6.13.1
 - @typescript-eslint/parser 6.0.0
 + @typescript-eslint/parser 6.13.1
 - eslint 8.28.0
 + eslint 8.54.0
 - eslint-plugin-svelte 2.30.0
 + eslint-plugin-svelte 2.35.1
 - prettier 3.0.0
 + prettier 3.1.0
 - prettier-plugin-svelte 3.0.0
 + prettier-plugin-svelte 3.1.2
 - svelte-check 3.6.0
 + svelte-check 3.6.2
 - tslib 2.4.1
 + tslib 2.6.2
 - typescript 5.0.2
 + typescript 5.3.2
 - vite 4.4.2
 + vite 4.5.0 (5.0.2 is available)
 - vitest 0.32.2
 + vitest 0.32.4 (0.34.6 is available)
 
 Done in 14.5s

さらに新しいものがあると言っているのはなぜなのか?

LANG: console
$ pnpm update
 Already up to date
 Progress: resolved 286, reused 263, downloaded 0, added 0, done
 Done in 5.8s

https://github.com/sveltejs/kit/issues/11062

とかあるし、新しすぎて他とバッティングするということなのかな?

とりあえず今はこれで。

LANG: console
$ git add .

$ git status
 On branch master
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
         modified:   package.json  
         modified:   pnpm-lock.yaml

$ git commit -m "pnpm update"
 [master 3621e0b] pnpm update
  2 files changed, 267 insertions(+), 302 deletions(-)

再度開発用サーバーを起動しておく。

LANG: console
$ pnpm dev

prettier を通して commit する

既存のソースを prettier に通す前に設定を変更

.prettierrc

LANG: js
  {
-   "useTabs": true,
+   "useTabs": false,
    "singleQuote": true,
-   "trailingComma": "none",
+   "trailingComma": "es5",
+   "quoteProps": "consistent",
    "printWidth": 100,
    "plugins": ["prettier-plugin-svelte"],
    "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
  }

pnpm format で通せる

LANG:console
$ pnpm format
 
 > authtest@0.0.1 format C:\Users\osamu\Desktop\svelte\authtest
 > prettier --write .
 
 .eslintrc.cjs 70ms
 .prettierrc 26ms
 package.json 3ms
 playwright.config.ts 170ms
 README.md 35ms (unchanged)
 src/app.d.ts 5ms
 src/app.html 55ms
 src/index.test.ts 12ms
 src/lib/index.ts 2ms (unchanged)
 src/routes/+page.svelte 136ms (unchanged)
 svelte.config.js 14ms
 tests/test.ts 12ms
 tsconfig.json 8ms
 vite.config.ts 8ms

$ git add .  

$ git status
 On branch master
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
         modified:   .eslintrc.cjs
         modified:   .prettierrc
         modified:   package.json
         modified:   playwright.config.ts
         modified:   src/app.d.ts
         modified:   src/app.html
         modified:   src/index.test.ts
         modified:   svelte.config.js
         modified:   tests/test.ts
         modified:   tsconfig.json
         modified:   vite.config.ts
 
$ git commit -m "prettier を通した"

コミット時に自動的に prettier を通すよう設定

add されたファイルに対してコミット直前に prettier を通すよう設定する

https://prettier.io/docs/en/precommit.html#option-5-shell-script に従って、

LANG: console
$ cat > .git/hooks/pre-commit
#!/bin/sh
FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g')
[ -z "$FILES" ] && exit 0

# Prettify all selected files
echo "$FILES" | xargs ./node_modules/.bin/prettier --ignore-unknown --write

# Add back the modified/prettified files to staging
echo "$FILES" | xargs git add

exit 0
^D

$ chmod u+x .git/hooks/pre-commit

サインアップフォームを作成

src/routes/account/new/+page.svelte

LANG: html
<div>
  <h1>サインアップ</h1>
  <form method="POST">
    <label for="name">ユーザー名</label>
    <input type="text" name="name" />

    <label for="email">メールアドレス</label>
    <input type="text" name="email" />

    <label for="password">パスワード</label>
    <input type="password" name="password" />

    <label for="confirmPassword">パスワード(確認)</label>
    <input type="password" name="confirm"/>
		
    <div><button>サインアップ</button></div>
  </form>
</div>

<style>
  input {
    display: block;
    margin-bottom: 10px;
  }
</style>

http://localhost:5173/signup でフォームを表示可能なことを確認

signup.png

POST に対応する Action を書いていないため「サインアップ」を押すと

405
POST method not allowed. No actions exist for this page

になることを確認。

src/routes/account/new/+page.server.ts

LANG: ts
import type { Actions } from './$types';
import { fail } from '@sveltejs/kit';

export const actions: Actions = {
  default: async () => {

    return fail(400, {message: 'サインアップ処理が未実装'})

  }
};

これで「ログイン」を押してもエラーは出なくなった一方、エラーメッセージも表示されない。

src/routes/account/new/+page.svelte の冒頭と末尾を下記のように変更

LANG: html
<script lang="ts">
  import type { ActionData } from './$types';
  export let data: ActionData;
</script>

<div>
  <h1>サインアップ</h1>

  {#if data?.message}<span class="invalid">{data.message}</span>{/if}
  ...

</div>
<style>
  ...

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

これでエラーメッセージが表示されるようになった。

LANG: console
$ git add .  

$ git commit -m "/account/new ページを作成"
src/routes/account/new/+page.server.ts 218ms (unchanged)
src/routes/account/new/+page.svelte 433ms (unchanged)
[master 779169b] C:/Program Files/Git/account/new ページを作成
 2 files changed, 44 insertions(+)
 create mode 100644 src/routes/account/new/+page.server.ts
 create mode 100644 src/routes/account/new/+page.svelte

ちゃんと prettier が走っている。

すばらしい。

入力内容の validation を行うために SuperForms を入れる

LANG:console
$ pnpm add -D sveltekit-superforms zod
 Packages: +2
 ++
 Progress: resolved 288, reused 264, downloaded 1, added 2, done
 
 devDependencies:
 + sveltekit-superforms 1.11.0
 + zod 3.22.4
 
 Done in 3.3s

$ git add .

$ git commit -m "superforms を追加"
package.json 37ms (unchanged)
[master ed1163c] superforms を追加
 2 files changed, 25 insertions(+), 1 deletion(-)

サインアップフォーム用のスキーマを追加

src/lib/zod/account/new.ts

LANG:ts
import { z } from 'zod';

// メールアドレスを表す現実的な正規表現 by @sakuro 
// https://qiita.com/sakuro/items/1eaa307609ceaaf51123
const emailRegexp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;

export const schema = z
  .object({
    name: z.string().min(3, { message: 'ユーザー名は3文字以上で入力してください' }),
    email: z.string().regex(emailRegexp, { message: 'メールアドレスが不正です' }),
    password: z
      .string()
      .regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$/, {
        message: 'パスワードは小文字・大文字・数字を一文字以上含めて8文字以上で入力してください'
      }),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: '確認用パスワードが一致しません',
    path: ['confirm']
  });

zod スキーマの置き場所について

これ以降しばらく、フォーム検証用のスキーマを $lib/zod/ に置いていたのだけれど、 プログラミング/svelte/コード配置の整理 で考え直してすべてそのスキーマを使うページと 同じフォルダに zod-schema.ts という名前で置くようにした。

その方がコードの所有権がはっきりすると思うので。

バリデーションを行う

src/routes/account/new/+page.server.ts

LANG:ts
import type { Actions, PageServerLoad } from './$types';
import { schema } from '$lib/zod/account/new';
import { superValidate } from 'sveltekit-superforms/server';
import { fail } from '@sveltejs/kit';

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 };
  }
};

エラーメッセージを表示するためのフィールドを用意する

src/routes/account/new/+page.svelte

LANG:html
<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="name">ユーザー名</label>
    <input type="text" name="name" bind:value={$form.name} disabled={$submitting} />
    {#if $errors.name}<span class="invalid">{$errors.name}</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}</span>{/if}

    <label for="confirm">パスワード(確認)</label>
    <input type="password" name="confirm" bind:value={$form.confirm} disabled={$submitting} />
    {#if $errors.confirm}<span class="invalid">{$errors.confirm}</span>{/if}

    <div><button disabled={$submitting}>サインアップ</button></div>
  </form>
</div>

<style>
  input {
    display: block;
    margin-bottom: 10px;
  }
  .invalid {
    display: block;
    color: red;
  }
</style>

これで正しくエラーが検出されるようになった。

signup-error.png
LANG: console
$ git add . && git commit -m "account/new にバリデーションを追加"
 src/lib/zod/account/new.ts 259ms
 src/routes/account/new/+page.server.ts 25ms
 src/routes/account/new/+page.svelte 429ms
 [master 1a89e62] account/new にバリデーションを追加
  3 files changed, 58 insertions(+), 15 deletions(-)
  create mode 100644 src/lib/zod/account/new.ts

Prisma の導入

https://www.prisma.io/docs/getting-started/quickstart に従って さしあたり `sqlite` を使うよう初期化

LANG:console
$ pnpm add -D prisma && pnpm add @prisma/client
 Packages: +2
 ++
 Progress: resolved 290, reused 267, downloaded 0, added 2, done
 
 devDependencies:
 + prisma 5.6.0
 
 Done in 4.5s
 Packages: +2
 ++
 Progress: resolved 292, reused 269, downloaded 0, added 2, done
 
 dependencies:
 + @prisma/client 5.6.0
 
 Done in 5.3s
 
$ pnpm dlx prisma init --datasource-provider sqlite
 
 ✔ Your Prisma schema was created at prisma/schema.prisma
   You can now open it in your favorite editor.
 
 warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
 
 Next steps:
 1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
 2. Run prisma db pull to turn your database schema into a Prisma schema.
 3. Run prisma generate to generate the Prisma Client. You can then start querying your database.
 
 More information in our documentation:
 https://pris.ly/d/getting-started

$ cat .env
 ...
 DATABASE_URL="file:./dev.db"

データベースは `(app)/prisma/dev.db` に作成される。

コミットされないよう .gitignore に追加しておく

LANG: console
$ echo "/prisma/*.db" >> .gitignore
$ echo "/prisma/*.db-journal" >> .gitignore
$ git add . && git commit -m "Prisma をインストールした"

Lucia に対応したテーブルを定義

https://lucia-auth.com/database-adapters/prisma/#prisma-schema あたりを参考に:

prisma/schema.prisma

...

model User {
  id       String       @id @unique
  sessions Session[]
  keys     AuthKey[]

  name     String       @unique
  email    String       @unique
}

model Session {
  id             String   @id @unique
  user_id        String
  active_expires BigInt
  idle_expires   BigInt
  user           User @relation(references: [id], fields: [user_id], onDelete: Cascade)

  @@index([user_id])
}

model AuthKey {
  id              String   @id @unique
  hashed_password String?
  user_id         String
  user            User @relation(references: [id], fields: [user_id], onDelete: Cascade)

  @@index([user_id])
}

migrate する

LANG: console
$ pnpm prisma migrate dev --name "add tables for lucia"
 Environment variables loaded from .env
 Prisma schema loaded from prisma\schema.prisma
 Datasource "db": SQLite database "dev.db" at "file:./dev.db"
 
 SQLite database dev.db created at file:./dev.db
 
 Applying migration `20231128121010_add_tables_for_lucia`
 
 The following migration(s) have been created and applied from new schema changes:
 
 migrations/
   └─ 20231128121010_add_tables_for_lucia/
     └─ migration.sql
 
 Your database is now in sync with your schema.
 
 ✔ Generated Prisma Client (v5.6.0) to .\node_modules\.pnpm\@prisma+client@5.6.0_prisma@5.6.0\node_modules\@prisma\client in 154ms

ライブラリに追加

サーバー上でしか使わないので lib/server の下に置く

src/lib/server/db.ts

LANG: ts
import { PrismaClient } from '@prisma/client';

export const db = new PrismaClient();

コミットしておく

LANG: console
$ git add . && git commit -m "Lucia 用のテーブルを準備"
 src/lib/server/db.ts 169ms
 [master 184f947] Lucia 用のテーブルを準備
  5 files changed, 79 insertions(+)
  create mode 100644 prisma/migrations/20231128121010_add_tables_for_lucia/migration.sql
  create mode 100644 prisma/migrations/migration_lock.toml
  create mode 100644 src/lib/server/db.ts

Lucia を導入

Lucia は認証やセッション管理を行うライブラリ。

https://lucia-auth.com/getting-started/sveltekit/ を見ながら v2 を使う

LANG: console
$ pnpm add lucia @lucia-auth/adapter-prisma
 Packages: +2
 ++
 Progress: resolved 294, reused 271, downloaded 0, added 2, done
 
 dependencies:
 + @lucia-auth/adapter-prisma 3.0.2
 + lucia 2.7.4
 
 Done in 3s

Lucia の初期化

テーブルの対応や User テーブルに追加した追加の属性をライブラリに教える。

src/lib/server/lucia.ts

LANG: ts
import { lucia } from "lucia";
import { sveltekit } from "lucia/middleware";
import { dev } from "$app/environment";
import { prisma } from "@lucia-auth/adapter-prisma";
import { db } from "$lib/server/db";

export const auth = lucia({
  env: dev ? "DEV" : "PROD",
  middleware: sveltekit(),
  adapter: prisma(db, {
    user: "user",
    key: "authKey",
    session: "session"
  }),
  getUserAttributes: (data) => {
    return {
      // IMPORTANT!!!!
      // `userId` included by default!!
      id: data.id,
      name: data.name,
      email: data.email
    };
  },
});

export type Auth = typeof auth;

locals.auth と locals.session を追加

User および Session に追加したフィールドの型定義を与えるとともに、

locals.auth に認証情報を持てるようにする

src/app.d.ts

LANG: ts
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
  declare namespace Lucia {
    type Auth = import('$lib/server/lucia').Auth;
    type DatabaseUserAttributes = {name: string, email: string};
    type DatabaseSessionAttributes = object;
  }
 namespace App {
    // interface Error {}
    interface Locals {
      auth: import('lucia').AuthRequest;
      session: import('lucia').Session | null;
    }
   // interface PageData {}
    // interface Platform {}
  }
}

export {};

hook で認証情報を読み込む

https://lucia-auth.com/basics/handle-requests/#sveltekit の通り、 hooks で event から認証情報を得て locals.auth へ入れる。

src/hooks.server.ts

LANG: ts
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);
  event.locals.session = await event.locals.auth.validate();

  return await resolve(event);
};

export const handle = sequence(authHandler);

サインアップ処理

Lucia v2 では createUser にて primaryKey の代わりに key とする。

src/lib/server/index.ts

LANG: ts
// place files you want to import through the `$lib` alias in this folder.

export const urlRoot = process.env["URL_ROOT"] || '/';
export function path(relative: string) {
  return urlRoot + relative;
}

この $lib/server に置いた urlRoot と path は失敗だった。
プログラミング/svelte/コード配置の整理#pf8b050e

src/routes/account/new/+page.server.ts

LANG: ts
// 冒頭に追加
import { fail, redirect } from '@sveltejs/kit';
import { auth } from '$lib/server/lucia';
import { LuciaError } from 'lucia';
import { path } from '$lib/server';
...
    // サインアップ処理
    try {
      const user = await auth.createUser({
        key: {
          providerId: 'email',
          providerUserId: form.data.email,
          password: form.data.password
        },
        attributes: {
          name: form.data.name,
          email: form.data.email,
        }
      });

      // そのままログイン状態にする
      const session = await auth.createSession({userId: user.userId, attributes: {}});
      event.locals.auth.setSession(session);
    } catch  (e) {
      if (e instanceof LuciaError && e.message === `AUTH_DUPLICATE_KEY_ID`) {
        return fail(400, { form: { ...form, message: '名前またはメールアドレスが既存のアカウントと重複しています' } });
      }
      // provided user attributes violates database rules (e.g. unique constraint)
      // or unexpected database errors
      return fail(400, { form: { ...form, message: 'サインアップエラー' } });
    }

    throw redirect(302, path('/'));
  },
};

これでサインアップできるようになった。

ここではそのままログイン状態にしてしまっている。

データベースを確認

テスト用にサインアップをしてみた後、

LANG: console
$ pnpm prisma studio

とすると Prisma Studio が立ち上がるので、AuthUser テーブルや AuthKey テーブルに正しく情報が登録していることを確認する。

ログイン状態を確認

上記のコードではサインアップと同時にログインしているので、 hooks により locals.session にログイン情報が入っているはず。

そこで、トップページでログイン情報を参照してみることにする。

src/routes/+page.server.ts

LANG: ts
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({locals}) => {
  return {user: locals.session?.user}
}

session.user に格納されるログインユーザーの情報をページに渡す。

ページ側でそれを受け取って、

src/routes/+page.svelte

LANG: html
<script lang="ts">
  import type { PageData } from './$types';
  export let data: PageData;
</script>

<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
{#if data.user}
<p>Hello { data.user.name } !!</p>
{/if}

とすれば、ログインしている場合に "Hello (ユーザー名) !!" と表示されるはずで・・・

試したところうまく行ってるようだった。

ログアウト処理

locals.auth の持つ validate() 結果 (session 情報) の内部キャッシュを無効化し、 それを参照するクッキーも削除する。

src/routes/session/delete/+server.ts

LANG: ts
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { path } from '$lib/server';

export const GET: RequestHandler = async ({ locals }) => {
  // 読み出し済みセッション情報の無効化
  locals.auth.invalidate();

  // クッキーからセッションID削除
  locals.auth.setSession(null);

  throw redirect(302, path('/'));
};

画面表示が必要ないので +server.ts とする。

これで確かにログアウトできた。

locals.auth は読み出し済みのセッション情報のキャッシュを消すだけで データベース上のセッションレコードは消さないみたいなのだけれどそれでいいのかしら???

ログイン処理

フォームのバリデーション

メールアドレスやパスワードが正しいかどうかはログイン処理で確認するので ここでは1文字でも入力されていれば良いことにする。

src/lib/zod/session/new.ts

LANG: ts
import { z } from 'zod';

export const schema = z
  .object({
    email: z.string().min(1, 'メールアドレスを入力して下さい'),
    password: z.string().min(1, 'パスワードを入力して下さい'),
  });

ログインフォーム

src/routes/session/new/+page.svelte

LANG: html
<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="email">ユーザー名</label>
    <input type="text" name="email" bind:value={$form.email} disabled={$submitting} />
    {#if $errors.email}<span class="invalid">{$errors.email[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>
  </form>
</div>

<style>
  input {
    display: block;
    margin-bottom: 10px;
  }
  .invalid {
    display: block;
    color: red;
  }
</style>

ログイン処理

src/routes/session/new/+page.server.ts

LANG: ts
import type { Actions, PageServerLoad } from './$types';
import { schema } from '$lib/zod/session/new';
import { superValidate } from 'sveltekit-superforms/server';
import { fail, redirect } from '@sveltejs/kit';

import { path } from '$lib/server';
import { auth } from '$lib/server/lucia';

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 });
    }

    // ログイン処理
    try {
      const key = await auth.useKey('email', form.data.email, form.data.password);
      const session = await auth.createSession({userId: key.userId, attributes: {}});
      event.locals.auth.setSession(session);
    } catch {
      return fail(400, { form: { ...form, message: 'ログインエラー' } });
    }

    throw redirect(302, path('/'));
  }
};

ログインもできるようになった

LANG: console
$ git add . && git commit -m "サインイン・ログイン・ログアウトができるようになった"
 package.json 124ms (unchanged)
 src/app.d.ts 298ms
 src/hooks.server.ts 18ms
 src/lib/index.ts 8ms
 src/lib/server/lucia.ts 12ms
 src/lib/zod/session/new.ts 14ms
 src/routes/+page.server.ts 5ms
 src/routes/+page.svelte 167ms
 src/routes/account/new/+page.server.ts 44ms
 src/routes/session/delete/+server.ts 10ms
 src/routes/session/new/+page.server.ts 21ms
 src/routes/session/new/+page.svelte 112ms
 [master 902570e] サインイン・ログイン・ログアウトができるようになった
  13 files changed, 208 insertions(+), 5 deletions(-)
  create mode 100644 src/hooks.server.ts
  create mode 100644 src/lib/server/lucia.ts
  create mode 100644 src/lib/zod/session/new.ts
  create mode 100644 src/routes/+page.server.ts
  create mode 100644 src/routes/session/delete/+server.ts
  create mode 100644 src/routes/session/new/+page.server.ts
  create mode 100644 src/routes/session/new/+page.svelte

ログインあり・なしで閲覧制限する

route に (loggedIn) や (loggedOut) があれば、 ログインしている/していないときしかアクセスを許さないようにする。

src/hooks.server.ts

LANG: ts
  import { auth } from '$lib/server/lucia';
* import { redirect, type Handle } from '@sveltejs/kit';
  import { sequence } from '@sveltejs/kit/hooks';
+ import { path } from '$lib/server';
  
  const authHandler: Handle = async ({ event, resolve }) => {
    event.locals.auth = auth.handleRequest(event);
    event.locals.session = await event.locals.auth.validate();
  
+   if (event.route.id?.match(/\/\(login\)\//) && !event.locals.session?.user) {
+     throw redirect(302, path('/session/new'));
+   }
+ 
+   if (event.route.id?.match(/\/\(logout\)\//) && event.locals.session?.user) {
+     throw redirect(302, path('/'));
+   }
+ 
    return await resolve(event);
  };
  
  export const handle = sequence(authHandler);

ページに適用する

routes を変更してアクセス制限を掛ける

  • /account/new を /account/(logout)/new
  • /session/new を /session/(logout)/new
  • /session/delete を /session/(login)/delete
LANG: console
$ mkdir src/routes/account/\(logout\)
$ git mv src/routes/account/new src/routes/account/\(logout\)/new
$ mkdir src/routes/session/\(logout\)
$ git mv src/routes/session/new src/routes/session/\(logout\)/new
$ mkdir src/routes/session/\(login\)
$ git mv src/routes/session/delete src/routes/session/\(login\)/delete
$ git add . && git commit -m "ログインしているかどうかでアクセス制限を掛けた"
 src/hooks.server.ts 370ms (unchanged)
 src/routes/account/(logout)/new/+page.server.ts 55ms (unchanged)
 src/routes/account/(logout)/new/+page.svelte 508ms (unchanged)
 src/routes/session/(login)/delete/+server.ts 11ms (unchanged)
 src/routes/session/(logout)/new/+page.server.ts 31ms (unchanged)
 src/routes/session/(logout)/new/+page.svelte 57ms (unchanged)
 [master f8b071a] ログインしているかどうかでアクセス制限を掛けた
  6 files changed, 10 insertions(+), 1 deletion(-)
  rename src/routes/account/{ => (logout)}/new/+page.server.ts (100%)
  rename src/routes/account/{ => (logout)}/new/+page.svelte (100%)
  rename src/routes/session/{ => (login)}/delete/+server.ts (100%)
  rename src/routes/session/{ => (logout)}/new/+page.server.ts (100%)
  rename src/routes/session/{ => (logout)}/new/+page.svelte (100%)

flash メッセージ

redirect による遷移先でメッセージを表示したい

http では遷移元と遷移先のページの表示は独立した処理になるため、 遷移元ページの処理結果に応じて遷移先ページにメッセージを出すには工夫がいる。

それを簡単に行えるようにするため sveltekit-flash-message というライブラリを利用する

もっともよくある使い方としては、フォームの POST が成功した場合に redirect で 別ページに移るのが良い作りとされているので、 その遷移先で送信成功を示すメッセージを表示するのにこのライブラリを使える

https://www.npmjs.com/package/sveltekit-flash-message の通りの手順で、

LANG: console
$ pnpm add -D sveltekit-flash-message
 Packages: +1
 +
 Progress: resolved 295, reused 272, downloaded 0, added 0, done
 
 devDependencies:
 + sveltekit-flash-message ^2.2.2
 
 Done in 3.4s

src/app.d.ts

LANG: ts
declare namespace App {
  ...

  interface PageData {
    flash?: { type: 'success' | 'error'; message: string };
  };
  ...
}

src/routes/+layout.server.ts

LANG: ts
export { load } from 'sveltekit-flash-message/server';

src/routes/+layout.svelte

LANG: html
<script lang="ts">
  import { getFlash } from 'sveltekit-flash-message';
  import { page } from '$app/stores';

  const flash = getFlash(page);
</script>

{#if $flash}
  {@const bg = $flash.type == 'success' ? '#3D9970' : '#FF4136'}
  <div style:background-color={bg} class="flash">{$flash.message}</div>
{/if}

<slot />

これで準備完了。

LANG: console
$ git commit -m "sveltekit-flash-message を導入"

redirect しているところに片っ端からメッセージを付ける (server 側)

src/routes/account/(logout)/new/+page.svelte

LANG: ts
+ import { setFlash } from 'sveltekit-flash-message/server';
...

+     setFlash({type: 'success', message: 'サインアップ&ログインしました'}, event);
      throw redirect(302, path('/'));

みたいな感じ。

src/routes/session/(logout)/new/+page.svelte

LANG: ts
+   setFlash({type: 'success', message: 'ログインしました'}, event);
    throw redirect(302, path('/'));

も問題ない。

ところが +server.ts の RequestHandler からだと event が手に入らないため困ってしまう。 実は setFlash には cookies を渡すこともできるので、それでしのげる。

src/routes/session/(login)/delete/+page.svelte

LANG: ts
* export const GET: RequestHandler = async ({ locals, cookies }) => {
    ...

+   setFlash({type: 'success', message: 'ログアウトしました'}, cookies);
    throw redirect(302, path('/'));

また、

src/hooks.server.ts

+ import { setFlash } from 'sveltekit-flash-message/server';

...

    if (event.route.id?.match(/\/\(login\)\//) && !event.locals.session?.user) {
+     setFlash({type: 'error', message: 'ログインユーザーのみアクセスできます'}, event);
      throw redirect(302, path('/session/new'));
    }
  
    if (event.route.id?.match(/\/\(logout\)\//) && event.locals.session?.user) {
+     setFlash({type: 'error', message: 'すでにログインしています'}, event);
      throw redirect(302, path('/'));
    }

ここで Response.redirect を使ってリダイレクトしようとするとうまく行かないので 注意が必要だ。setFlash は event.cookies あるいは cookies の中身を書き換えるのだけれど Response.redirect はそれらの変数を参照しないので書き換えた内容が反映されない。 cookies を渡したければ through redirect する必要がある。

このようにして redirect 先で成功・失敗を通知できるようになった。

LANG: console
$ git add . && git commit -m "flash メッセージを追加した"
 package.json 121ms (unchanged)
 src/app.d.ts 299ms
 src/hooks.server.ts 45ms
 src/routes/+layout.server.ts 4ms
 src/routes/+layout.svelte 298ms
 src/routes/account/(logout)/new/+page.server.ts 42ms
 src/routes/session/(login)/delete/+server.ts 12ms
 src/routes/session/(logout)/new/+page.server.ts 23ms
 [master 602e71e] flash メッセージを追加した
  9 files changed, 41 insertions(+), 2 deletions(-)
  create mode 100644 src/routes/+layout.server.ts
  create mode 100644 src/routes/+layout.svelte

flash メッセージの表示方法

特にクライアントサイドから表示した場合には 画面上部に出るだけだと気づけない場合もあるので、
「表示領域の上部に出て、しばらくしたら引っ込む」
みたいな動作が望ましい気がする。

https://github.com/ciscoheat/sveltekit-flash-message#toast-messages-event-style

によれば svelte-french-toast を使うと良いらしい。

LANG: console
$ pnpm add svelte-french-toast
Packages: +2
++
Progress: resolved 297, reused 274, downloaded 0, added 0, done

dependencies:
+ svelte-french-toast 1.2.0

Done in 2.5s

src/routes/+layout.svelte

LANG: html
  <script lang="ts">
    import { getFlash } from 'sveltekit-flash-message';
    import { page } from '$app/stores';
+   import toast, { Toaster } from 'svelte-french-toast';
 
    const flash = getFlash(page);
  
+   flash.subscribe(($flash) => {
+     if (!$flash) return;
+ 
+     toast($flash.message, {
+ +       icon: $flash.type == 'success' ? '✅' : '❌'
+     });
+ 
+     // Clearing the flash message could sometimes
+     // be required here to avoid double-toasting.
+     flash.set(undefined);
+   });
   </script>
 
+  <Toaster />
   <slot />

これで望み通りの表示になった。

toast-message.gif
LANG: console
$ git add . && git commit -m "flash メッセージを toast するようにした"
 package.json 119ms (unchanged)
 src/routes/+layout.svelte 340ms
 [master cd6fe78] flash メッセージを toast するようにした
  3 files changed, 36 insertions(+), 22 deletions(-)

クライアントサイドで使う例も後で勉強すべき

https://www.npmjs.com/package/sveltekit-flash-message#client-side

どういう風に実装されているかまで理解しないと どうしてこういう処理が必要になるのか納得しづらいかも。

見た目を整える

DaisyUI を入れる

https://daisyui.com/

DaisyUI は TailwindCSS を元にしているので、まずは TailwindCSS を入れる。

LANG: console
$ pnpm dlx svelte-add@latest tailwindcss
 .../Local/pnpm/store/v3/tmp/dlx-21132    | +121 ++++++++++++
 .../Local/pnpm/store/v3/tmp/dlx-21132    | Progress: resolved 121, reused 121, downloaded 0, added 121, done
 ➕ Svelte Add (Version 2023.11.191.00)
 The project directory you're giving to this command cannot be determined to be guaranteed fresh — maybe it is, maybe it isn't. If any issues arise after running this command, please try again, making sure you've run it on a freshly initialized SvelteKit or Vite–Svelte app template.
 {
     "type": "Script",
     "start": 0,
     "end": 251,
     "context": "default",
     "content": {
         "type": "Program",
         "start": 240,
         "end": 242,
         "loc": {
             "start": {
                 "line": 1,
                 "column": 0
             },
             "end": {
                 "line": 1,
                 "column": 242
             }
         },
         "body": [
             {
                 "type": "BlockStatement",
                 "start": 240,
                 "end": 242,
                 "loc": {
                     "start": {
                         "line": 1,
                         "column": 240
                     },
                     "end": {
                         "line": 1,
                         "column": 242
                     }
                 },
                 "body": []
             }
         ],
         "sourceType": "module"
     }
 }
 {
     "type": "Script",
     "start": 0,
     "end": 251,
     "context": "default",
     "content": {
         "type": "Program",
         "start": 240,
         "end": 242,
         "loc": {
             "start": {
                 "line": 1,
                 "column": 0
             },
             "end": {
                 "line": 1,
                 "column": 242
             }
         },
         "body": [
             {
                 "type": "BlockStatement",
                 "start": 240,
                 "end": 242,
                 "loc": {
                     "start": {
                         "line": 1,
                         "column": 240
                     },
                     "end": {
                         "line": 1,
                         "column": 242
                     }
                 },
                 "body": []
             }
         ],
         "sourceType": "module"
     }
 }
 
 PostCSS
  ✅ successfully set up and repaired (it looks like it was in a broken setup before this command was run)!
 Create or find an existing issue at https://github.com/svelte-add/svelte-add/issues if this is wrong.
 
 Tailwind CSS
  ✅ successfully set up!
 Create or find an existing issue at https://github.com/svelte-add/svelte-add/issues if this is wrong.
 
 Run pnpm install to install new dependencies, and then reload your IDE before starting your app.

言われた通りに、

LANG: console
$ pnpm install
 Packages: +45 -3
 +++++++++++++++++++++++++++++++++++++++++++++---
 Progress: resolved 339, reused 315, downloaded 1, added 1, done
 node_modules/.pnpm/svelte-preprocess@5.1.1_postcss-load-config@4.0.1_postcss@8.4.31_svelte@4.2.7_typescript@5.3.2/node_modules/svelte-preprocess: Running postinstall script, done in 51ms
 
 devDependencies:
 + autoprefixer 10.4.14 (10.4.16 is available)
 + postcss 8.4.31
 + postcss-load-config 4.0.1 (4.0.2 is available)
 - prettier-plugin-svelte 3.1.2
 + prettier-plugin-tailwindcss 0.4.1 (0.5.7 is available)
 + tailwindcss 3.3.2 (3.3.5 is available)
 
 Done in 9.6s

そして、

LANG: console
$ pnpm add -D daisyui@latest @tailwindcss/typography
 Packages: +8
 ++++++++
 Progress: resolved 347, reused 323, downloaded 1, added 1, done
 
 devDependencies:
 + @tailwindcss/typography 0.5.10
 + daisyui 4.4.14
 
 Done in 4.1s

https://daisyui.com/docs/layout-and-typography/#-1

tailwind.config.cjs

+   daisyui: {
+     themes: ['light']
+   },
  
+   plugins: [
+     require("@tailwindcss/typography"),
+     require("daisyui")
+   ],
-   plugins: [],

コンポーネントの準備

src/lib/components/Dialog.svelte

LANG: html
<script lang="ts">
  export let title: string;
</script>

<div class="dialog relative flex flex-col items-center justify-center min-h-screen">
  <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">{title}</h1>
    <slot />
  </div>
</div>

src/lib/components/Form.svelte

LANG: html
<script lang="ts">
  import { afterUpdate } from 'svelte';

  export let message= '';
  export let enhance: ((el: HTMLFormElement) => object) = ()=>{return {}};
  export let props: svelteHTML.IntrinsicElements['form'] | undefined = undefined;

  let form: HTMLFormElement;
  afterUpdate(()=> {
    if(message) {
      // エラーメッセージが付いていたらすべてのコントロールをエラー表示にする
      ['checkbox', 'file-input', 'radio', 'range', 'select', 'ihput', 'textarea', 'toggle'].forEach(kind=>{
        for(const elem of form.querySelectorAll(`.${kind}`)){
          elem.classList.add(`${kind}-error`);
        }
      })
    }
  })
</script>

{#if message}<span class="text-error text-sm">{message}</span>{/if}
<form bind:this={form} class="space-y-4" method="POST" use:enhance {...props}>
  <slot />
</form>

src/lib/components/InputText.svelte

LANG: html
<script lang="ts">
  import type { Writable } from 'svelte/store';

  export let name: string;
  export let label: string;
  export let labelAlt: string = '';
  export let value: string;
  export let disabled = false;
  export let errors: Writable<{}> | undefined = undefined;
  export let props: svelteHTML.IntrinsicElements['input'] | undefined = undefined;

  let key = name as keyof typeof errors;
</script>

<div class="form-control w-full">
  <label for={name} class="label">
    <span class="label-text">{label}</span>
    {#if labelAlt}<span class="label-text-alt">{labelAlt}</span>{/if}
  </label>
  <input
    {...{ name, type: 'text', ...props }}
    bind:value
    {disabled}
    on:input
    class="w-full input input-bordered input-primary"
    class:input-error={errors && $errors && $errors[key]}
  />
  {#if errors && $errors && $errors[key]}
    <label class="label" for={name}>
      <span class="label-text-alt text-error">{$errors[key]}</span>
    </label>
  {/if}
</div>

src/lib/components/InputPassword.svelte

LANG: html
<script lang="ts">
  import InputText from '$lib/components/InputText.svelte';
  import type { Writable } from 'svelte/store';

  export let name: string;
  export let label: string;
  export let labelAlt: string = '';
  export let value: string;
  export let disabled = false;
  export let errors: Writable<{}> | undefined = undefined;
  export let props: svelteHTML.IntrinsicElements['input'] | undefined = undefined;
</script>

<InputText {...{name, label, labelAlt, disabled, errors}}
  props= {{type: "password", ...props}} bind:value on:input />

src/lib/components/SubmitButton.svelte

LANG: html
<script lang="ts">
  export let disabled = false;
</script>

<div>
  <button disabled={disabled} class="btn btn-block btn-primary text-white"><slot /></button>
</div>

書き換える

こんな風に書ける

src/routes/account/(logout)/new/+page.svelte

LANG: html
<script lang="ts">
  import type { PageData } from './$types';
  import { superForm } from 'sveltekit-superforms/client';
  import Dialog from '$lib/components/Dialog.svelte';
  import Form from '$lib/components/Form.svelte';
  import InputText from '$lib/components/InputText.svelte';
  import InputPassword from '$lib/components/InputPassword.svelte';
  import Button from '$lib/components/Button.svelte';
  import { path } from '$lib/server';

  export let data: PageData;
  const { form, message, errors, submitting, capture, restore, enhance } = superForm(data.form, {
    taintedMessage: false,
  });
  export const snapshot = { capture, restore };
</script>

<Dialog title="サインアップ">
  <Form message={$message} {enhance}>
    <InputText name="name" label="ユーザー名" bind:value={$form.name} disabled={$submitting} errors={errors} />
    <InputText name="email" label="メールアドレス" bind:value={$form.email} disabled={$submitting} errors={errors} props={{placeholder: "account@example.com"}} />
    <InputPassword name="password" label="パスワード" bind:value={$form.password} disabled={$submitting} errors={errors} />
    <InputPassword name="confirm" label="パスワード(確認)" bind:value={$form.confirm} disabled={$submitting} errors={errors} />
    <Button disabled={$submitting}>サインアップ</Button>
    <p>アカウントをお持ちなら <a href={path('/session/new')}>サインアップ</a></p>
  </Form>
</Dialog>

signup-formatted.png

src/routes/session/(logout)/new/+page.svelte

LANG: html
  <script lang="ts">
    ...
+   import Dialog from '$lib/components/Dialog.svelte';
+   import Form from '$lib/components/Form.svelte';
+   import InputText from '$lib/components/InputText.svelte';
+   import InputPassword from '$lib/components/InputPassword.svelte';
+   import Button from '$lib/components/Button.svelte';
    ...
  </script>

* <Dialog title="ログイン">
*   <Form message={$message} {enhance}>
*     <InputText name="email" label="メールアドレス" bind:value={$form.email} disabled={$submitting} errors={errors} props={{placeholder: "account@example.com"}} />
*     <InputPassword name="password" label="パスワード" bind:value={$form.password} disabled={$submitting} errors={errors} />
*     <Button disabled={$submitting}>ログイン</Button>
*     <p>アカウントをお持ちでなければ <a class="link link-primary" href={data.urlRoot+'/account/new'}>サインアップ</a></p>
*   </Form>
</Dialog>

うまくコミットできなくなっていた

LANG: console
$ git add . && git commit -m "DaisyUI を入れて見た目を整えた"
 .prettierrc 231ms (unchanged)
 package.json 10ms (unchanged)
 
 🌼   daisyUI 4.4.14
 ├─ ✔︎ 1 theme added             https://daisyui.com/docs/themes
 ╰─ ★ Star daisyUI on GitHub     https://github.com/saadeghi/daisyui
 
 postcss.config.cjs 1575ms (unchanged)
 src/app.pcss 126ms (unchanged)
 src/hooks.server.ts 115ms (unchanged)
 [error] Couldn't resolve parser "svelte".
 [master 9e98390] DaisyUI を入れて見た目を整えた
  22 files changed, 644 insertions(+), 111 deletions(-)
  create mode 100644 postcss.config.cjs
  create mode 100644 src/app.pcss
  create mode 100644 src/lib/components/Button.svelte
  create mode 100644 src/lib/components/Dialog.svelte
  create mode 100644 src/lib/components/Form.svelte
  create mode 100644 src/lib/components/InputPassword.svelte
  create mode 100644 src/lib/components/InputText.svelte
  create mode 100644 src/lib/server/index.ts
  create mode 100644 tailwind.config.cjs

あれ、エラー出た。

LANG: console
 [error] Couldn't resolve parser "svelte".

これか→ https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/113#issuecomment-1368282240

.prettierrc

-   "plugins": ["prettier-plugin-tailwindcss"],
+   "plugins": [
+     "prettier-plugin-svelte",
+     "prettier-plugin-tailwindcss"
+   ],

として、

LANG: console
$ pnpm add -D prettier-plugin-svelte
 Packages: +2 -1
 ++-
 Progress: resolved 348, reused 325, downloaded 0, added 1, done
 
 devDependencies:
 + prettier-plugin-svelte 3.1.2
 
 Done in 5.3s
$ git reset --soft HEAD^
$ git add package.json pnpm-lock.yaml .prettierrc
$ git commit -m "DaisyUI を入れて見た目を整えた"
 .prettierrc 187ms
 package.json 6ms (unchanged)
 
 🌼   daisyUI 4.4.14
 ├─ ✔︎ 1 theme added             https://daisyui.com/docs/themes
 ╰─ ★ Star daisyUI on GitHub     https://github.com/saadeghi/daisyui
 
 postcss.config.cjs 3734ms (unchanged)
 src/app.pcss 109ms (unchanged)
 src/hooks.server.ts 104ms (unchanged)
 src/lib/components/Button.svelte 228ms
 src/lib/components/Dialog.svelte 44ms
 src/lib/components/Form.svelte 58ms
 src/lib/components/InputPassword.svelte 35ms
 src/lib/components/InputText.svelte 51ms
 src/lib/index.ts 16ms (unchanged)
 src/lib/server/index.ts 19ms (unchanged)
 src/routes/+layout.svelte 31ms
 src/routes/+page.svelte 21ms (unchanged)
 src/routes/account/(logout)/new/+page.server.ts 38ms (unchanged)
 src/routes/account/(logout)/new/+page.svelte 59ms
 src/routes/session/(login)/delete/+server.ts 26ms (unchanged)
 src/routes/session/(logout)/new/+page.server.ts 31ms (unchanged)
 src/routes/session/(logout)/new/+page.svelte 33ms
 svelte.config.js 12ms (unchanged)
 tailwind.config.cjs 11ms
 [master 6531fb9] DaisyUI を入れて見た目を整えた
  22 files changed, 678 insertions(+), 82 deletions(-)
  create mode 100644 postcss.config.cjs
  create mode 100644 src/app.pcss
  create mode 100644 src/lib/components/Button.svelte
  create mode 100644 src/lib/components/Dialog.svelte
  create mode 100644 src/lib/components/Form.svelte
  create mode 100644 src/lib/components/InputPassword.svelte
  create mode 100644 src/lib/components/InputText.svelte
  create mode 100644 src/lib/server/index.ts
  create mode 100644 tailwind.config.cjs

どうして prettier-plugin-svelte を取り除こうと思ったし???

リンクを追加

LANG: html
<p>アカウントをお持ちなら <a class="link link-primary" href="/session/new">ログイン</a></p>

みたいのを入れたいのだけれど URL_ROOT を変えられるようにしておきたいので、

src/app.d.ts

LANG: ts
      interface PageData {
        flash?: { type: 'success' | 'error'; message: string };
+       urlRoot: string;
      }

src/routes/+layout.server.ts

LANG: ts
import { loadFlash } from 'sveltekit-flash-message/server';
import { urlRoot } from '$lib/server';

export const load = loadFlash(async () => {
  const data = { urlRoot };
  return data;
});

として各ページで data.urlRoot を参照できるようにする。

追記:これは失敗だった。
プログラミング/svelte/コード配置の整理#pf8b050e

これを使って、

src/routes/account/(logout)/new/+page.svelte

LANG: html
+     <p>アカウントをお持ちなら <a class="link link-primary" href={data.urlRoot+'/session/new'}>ログイン</a></p>

src/routes/session/(logout)/new/+page.svelte

LANG: html
+     <p>アカウントをお持ちでなければ <a class="link link-primary" href={data.urlRoot+'/account/new'}>サインアップ</a></p>

とすればいい。

LANG: console
$ git add . && git commit -m "ログイン・サインイン間にリンクを追加"
 
 🌼   daisyUI 4.4.14
 ├─ ✔︎ 1 theme added             https://daisyui.com/docs/themes
 ╰─ ★ Star daisyUI on GitHub     https://github.com/saadeghi/daisyui
 
 src/app.d.ts 1713ms (unchanged)
 src/routes/+layout.server.ts 30ms (unchanged)
 src/routes/account/(logout)/new/+page.svelte 286ms
 src/routes/session/(logout)/new/+page.svelte 81ms
 [master 304ac3e] ログイン・サインイン間にリンクを追加
  4 files changed, 18 insertions(+), 1 deletion(-)

毎回 daisyUI のメッセージが出るのはそういうもの?

https://github.com/saadeghi/daisyui/blob/35dbea89ca5b82dd0c3ba4bb69d5f39a7b7c4d54/src/index.js#L119

これなわけだけど・・・


添付ファイル: filesignup-formatted.png 137件 [詳細] filetoast-message.gif 146件 [詳細] filesignup-error.png 137件 [詳細] filesignup.png 140件 [詳細]

Counter: 2219 (from 2010/06/03), today: 2, yesterday: 7