[Keystone] 認証とセッション

認証とセッション

Keystone アプリにパスワード、セッション データ、および認証を追加する方法を学びます。

公開ワークフロー

keystone.ts

import { config, list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { text, relationship, timestamp, select } from '@keystone-6/core/fields';

const lists = {
  User: list({
    access: allowAll,
    fields: {
      name: text({ validation: { isRequired: true } }),
      email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),
      posts: relationship({ ref: 'Post.author', many: true }),
    },
  }),
  Post: list({
    access: allowAll,
    fields: {
      title: text(),
      publishedAt: timestamp(),
      author: relationship({
        ref: 'User.posts',
        ui: {
          displayMode: 'cards',
          cardFields: ['name', 'email'],
          inlineEdit: { fields: ['name', 'email'] },
          linkToItem: true,
          inlineCreate: { fields: ['name', 'email'] },
        },
      }),
      status: select({
        options: [
          { label: 'Published', value: 'published' },
          { label: 'Draft', value: 'draft' },
        ],
        defaultValue: 'draft',
        ui: { displayMode: 'segmented-control' },
      }),
    },
  }),
};

export default config({
  db: {
    provider: 'sqlite',
    url: 'file:./keystone.db',
  },
  lists,
});

パスワードフィールドを追加する

Keystone のパスワードフィールドは、データベース内のパスワードのハッシュ化や、管理 UI 入力フィールドのパスワードのマスキングなど、一般的なパスワード セキュリティの推奨事項に従っています。
keystone.ts

import { config, list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { password, text, relationship, timestamp, select } from '@keystone-6/core/fields';

const lists = {
  User: list({
    access: allowAll,
    fields: {
      name: text({ validation: { isRequired: true } }),
      email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),
      posts: relationship({ ref: 'Post.author', many: true }),
      password: password({ validation: { isRequired: true } })
    },
  }),
...

認証を追加

auth パッケージをインストールする

アプリで使用するには、Keystone の認証パッケージを追加する必要があります。

yarn add @keystone-6/auth

パッケージができたので、プロジェクトのルートに新しいファイルを作成して、認証構成を書き込みましょう。
auth.ts

import { createAuth } from '@keystone-6/auth';

const { withAuth } = createAuth({
  listKey: 'User',
  identityField: 'email',
  sessionData: 'name',
  secretField: 'password',
});

export { withAuth };

セッションを追加

認証方法を追加したら、更新間で認証を維持できるように「セッション」を追加する必要があります。
auth.ts

import { createAuth } from '@keystone-6/auth';
import { statelessSessions } from '@keystone-6/core/session' ;

const { withAuth } = createAuth({
  listKey: 'User',
  identityField: 'email',
  sessionData: 'name',
  secretField: 'password',
});

let sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --';
let sessionMaxAge = 60 * 60 * 24 * 30; // 30 days

const session = statelessSessions({
  maxAge: sessionMaxAge,
  secret: sessionSecret,
});

export { withAuth, session }

認証とセッションを Keystone 構成にインポートする

keystone ファイルに戻って、withAuth 関数と session オブジェクトをインポートする必要があります。
withAuth は、デフォルトエクスポートをラップし、設定を完了する最後のステップで変更します。セッションはエクスポートにアタッチされます。
最後に、有効なセッションを持つユーザーだけが管理UIを見ることができるように、isAccessAllowed 関数をエクスポートに追加する必要があります。
keystone.ts

import { config, list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { password, text, relationship, timestamp, select } from '@keystone-6/core/fields';
import { withAuth, session } from './auth';

...

export default config(
  withAuth({
  db: {
    provider: 'sqlite',
    url: 'file:./keystone.db',
  },
  lists,
  session,
    ui: {
      isAccessAllowed: (context) => !!context.session?.data,
    },
  })
);

初期化項目の追加

新しい設定では、管理UIにロックアウトされます!さらに、まだデータベースにユーザーがいない場合、または新しい人がプロジェクトをクローンした場合、管理UIにアクセスできません。幸いなことに、Keystoneには、既存のユーザーが存在しない場合、最初にAdmin UIを起動するときに1人のユーザーを作成できる initFirstItem 機能があります。これは auth パッケージにあります。
auth.ts

...
const { withAuth } = createAuth({
  listKey: 'User',
  identityField: 'email',
  sessionData: 'name',
  secretField: 'password',
  initFirstItem: {
    fields: ['name', 'email', 'password'],
  },
});
...

認証とセッション

auth.ts

import { createAuth } from '@keystone-6/auth';
import { statelessSessions } from '@keystone-6/core/session' ;

const { withAuth } = createAuth({
  listKey: 'User',
  identityField: 'email',
  sessionData: 'name',
  secretField: 'password',
  initFirstItem: {
    fields: ['name', 'email', 'password'],
  },
});

let sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --';
let sessionMaxAge = 60 * 60 * 24 * 30; // 30 days

const session = statelessSessions({
  maxAge: sessionMaxAge,
  secret: sessionSecret,
});

export { withAuth, session }

keystone.ts

import { config, list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { password, text, relationship, timestamp, select } from '@keystone-6/core/fields';
import { withAuth, session } from './auth';

const lists = {
  User: list({
    access: allowAll,
    fields: {
      name: text({ validation: { isRequired: true } }),
      email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),
      posts: relationship({ ref: 'Post.author', many: true }),
      password: password({ validation: { isRequired: true } })
    },
  }),
  Post: list({
    access: allowAll,
    fields: {
      title: text(),
      publishedAt: timestamp(),
      author: relationship({
        ref: 'User.posts',
        ui: {
          displayMode: 'cards',
          cardFields: ['name', 'email'],
          inlineEdit: { fields: ['name', 'email'] },
          linkToItem: true,
          inlineCreate: { fields: ['name', 'email'] },
        },
      }),
      status: select({
        options: [
          { label: 'Published', value: 'published' },
          { label: 'Draft', value: 'draft' },
        ],
        defaultValue: 'draft',
        ui: { displayMode: 'segmented-control' },
      }),
    },
  }),
};

export default config(
  withAuth({
  db: {
    provider: 'sqlite',
    url: 'file:./keystone.db',
  },
  lists,
  session,
    ui: {
      isAccessAllowed: (context) => !!context.session?.data,
    },
  })
);