Open4

SvelteKitでデモアプリ作るぞ

kawarimidollkawarimidoll

Zennみたいな記事投稿サイトを作ろう

やりたいこと

  • ユーザー・管理者の両方があるwebアプリ

使いたい技術
SvelteKitで入るやつは便利そう

  • bun
    • パッケージ管理
  • SvelteKit
    • 大枠として
  • lucia-auth
    • ログインに使用
  • paraglide
    • i18n対応
  • mode-watcher
    • ダークモード対応
  • drizzle / pglite
    • データベース
  • mdsvex
    • マークダウンを採用する場合
  • storybook
    • コンポーネント単位で編集するとなるとあったほうが良いか
  • unocss
    • スタイリング
使わない
  • prettier
    • eslintでフォーマットまでやるため
  • tailwind
    • unoを使うため

要件

  • ユーザーをCRUDできる
  • ユーザーは自分の投稿をCRUDできる
  • 管理者はユーザーとは別画面でいろいろ管理できる
  • 管理者は自由にCRUDできる必要はない
    • 作成は他の管理者が代わりにできればよいであろう
    • そうすると管理者権限の管理者と一般権限の管理者が生まれることになるな…これめんどいやつ

https://github.com/kawarimidoll/sveltekit-demoapp

kawarimidollkawarimidoll

IDは識別子でありソートに使うべきではない!
数値カラムはナシだな

https://zenn.dev/mpyw/articles/rdb-ids-and-timestamps-best-practices

とはいえ時刻でソートすると同時に作成されたレコードの順序が安定しない可能性がある
そうするとIDはUUID v7またはcuid2が適切 どっちが良いかしら

  • UUID v7 → uuid型があるのでデータ効率が良い
  • cuid2 → uuidよりちょっと短い

と思ったけどIDにタイムスタンプが含まれているって脆弱性になり得ないか?
たとえばユーザーIDからユーザーの登録時刻がわかってしまう

そしてこれ結局created_atでソートしてるのと同じやん
→ じゃあUUID v4でいいや

kawarimidollkawarimidoll

luciaのサンプルから設定を書き換える必要がある

https://lucia-auth.com/sessions/basic-api/drizzle-orm

  • userのidをuuidにする
  • session.idはencodedTokenという名前にしたい
  • session.user_idはuniqueだよな…?→複数デバイスでログインする可能性があるのでユーザーが複数のセッションを持つことはおかしくない
  • userにはcreated_at / updated_atを追加する
    • sessionには不要でしょう

こんな感じのスキーマになった

import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';

// wrapper function for timestamp with zone
function tsz() {
  return timestamp({ withTimezone: true, mode: 'date', precision: 3 }).notNull();
}

const timestamps = {
  createdAt: tsz().$default(() => new Date()),
  updatedAt: tsz().$onUpdate(() => new Date()),
};

export const user = pgTable('user', {
  id: uuid().defaultRandom().primaryKey(),
  username: text().notNull().unique(),
  passwordHash: text().notNull(),
  ...timestamps,
});

export const session = pgTable('session', {
  encodedToken: text().primaryKey(),
  userId: uuid().notNull().references(() => user.id),
  expiresAt: tsz(),
});

export type Session = typeof session.$inferSelect;

export type User = typeof user.$inferSelect;

ちなみにcasingを設定しているのでスネークケース名の再定義は不要

https://orm.drizzle.team/docs/sql-schema-declaration#camel-and-snake-casing

kawarimidollkawarimidoll

user

  • passkeysでログインする
    • ユーザーネームを入れるとパスキーの設定を行いそれでログインする
    • 別デバイスの場合はパスキーを連携してログインする
    • すでにログインしているデバイスがあれば、ログイン画面内でOTPを生成してログインできるようにしてもいい

admin

  • メールアドレスとパスワードでログインする
  • 登録はすでに登録している管理者がメールを送信し、そのメールのマジックリンクから登録する
    • 最初は開発者がコンソールから登録するか初期データを入れる
  • permission_levelを持つ smallintでよい
    • administrator
      • 他のアドミンの追加はこのレベルからしかできないとする
    • member
      • だいたいの機能の編集が出来る、ただしアドミンは除く
    • supporter デフォルト
      • クリティカルな操作はできない
    • developer
      • 最強権限として用意しようかと思ったけど開発者は直接コンソール叩いたりするはずなので不要