Slide 1

Slide 1 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. T O R A N O A N A L a b  Fresh(Deno)を  プラグインで拡張しよう! 2023/12/07 虎の穴ラボ 奥谷 一陽

Slide 2

Slide 2 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 自己紹介 奥谷 一陽 所属:虎の穴ラボ株式会社 担当:Fantia、とらコインなどの開発 興味:Deno、TypeScript 最近買ったもの:ちょっといいまくら Twitter:@okutann88 github:Octo8080X

Slide 3

Slide 3 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 今回のLTは、先にブログで公開しています 参照:https://toranoana-lab.hatenablog.com/entry/2023/12/06/100000

Slide 4

Slide 4 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. Fresh 1.6 が12月1日に公開されました 参照:https://deno.com/blog/fresh-1.6

Slide 5

Slide 5 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. ピックアップ - Tailwind CSSプラグイン - Partials がFormに対応 - プラグインAPIの機能強化 - and more

Slide 6

Slide 6 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. プラグインでできること - routesの拡張 - middlewareの拡張 - islandsの拡張(NEW) - リンク要素の追加(NEW) - and more 今回は、routes と middleware をまとめて拡張するプラグインを取り扱います プラグインの作り方の一例としてみていただければ幸い // blob/main/src/server/types.ts export interface Plugin> { name: string; entrypoints?: Record; render?(ctx: PluginRenderContext): PluginRenderResult; renderAsync?(ctx: PluginAsyncRenderContext): Promise; buildStart?(config: ResolvedFreshConfig): Promise | void; buildEnd?(): Promise | void; configResolved?(config: ResolvedFreshConfig): Promise | void; routes?: PluginRoute[]; middlewares?: PluginMiddleware[]; islands?: PluginIslands; }

Slide 7

Slide 7 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. routes と middleware をまとめて拡張するプラグイン例 例えば次のようなプラグインがある。 - / に一致するパスでは、 **samplePlugin middleware** とコンソールに表示する - /sample に一致するパスでは
SAMPLE PLUGIN
を返す import { FreshContext, Plugin } from "$fresh/server.ts"; async function sampleHandler( _req: Request, ctx: FreshContext, ): Promise { console.log("**samplePlugin middleware**"); return await ctx.next(); } export const samplePlugin: Plugin = { name: "samplePlugin", middlewares: [ { path: "/", middleware: { handler: sampleHandler, }, }, ], routes: [ { path: "/sample", component: () =>
SAMPLE PLUGIN
, }, ], };

Slide 8

Slide 8 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. routes と middleware をまとめて拡張するプラグイン例 プラグインをconfigに設定すると /sample にアクセスすると **samplePlugin middleware** と出力し
SAMPLE PLUGIN
を返す // fresh.config.ts import { defineConfig } from "$fresh/server.ts"; import tailwind from "$fresh/plugins/tailwind.ts"; import { samplePlugin } from "./sample_plugint.tsx"; export default defineConfig({ plugins: [tailwind(), samplePlugin], });

Slide 9

Slide 9 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. このように、routes と middleware を
 まとめて拡張できる


Slide 10

Slide 10 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 1. 作るFreshのプラグイン - 認証機能提供するnpmパッケージluciaを使ってのログイン機能 - luciaのStarter guidesベースで進めますが、 csrf対策不足なのでそのまま本番に適用せずよく確認してください。 - 認証機能のために実装するのは主に2つ - アカウントの作成、ログイン、ログアウトをするroutes - ログイン情報を事前処理するmiddaleware - なるべくFresh本体のroutes以下に手を入れずに 「プラグインだけ入れれば、認証機能が追加できる」プラグインを作 る。 - 一旦はFreshに直接実装してから、プラグインにします。

Slide 11

Slide 11 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 「プラグインを入れるだけでログイン機能が使える」を実現したい 1. 作るFreshのプラグイン

Slide 12

Slide 12 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 2. 一旦Freshに直接実装(設定) アカウント作成、認証、ログアウトに 必要な、Luciaの共通部分は 切り出しておきます。 // utils/lucia_auth.ts import { lucia } from "npm:lucia"; import { web } from "npm:lucia/middleware"; import { mysql2 } from "npm:@lucia-auth/adapter-mysql"; import mysql from "npm:mysql2/promise"; import "https://deno.land/[email protected]/dotenv/load.ts"; const connectionPool = mysql.createPool({ database: Deno.env.get("MYSQL_DATABASE") || "", host: Deno.env.get("MYSQL_HOST") || "", user: Deno.env.get("MYSQL_USER") || "", port: parseInt(Deno.env.get("MYSQL_PORT") || ""), }); export const luciaAuth = lucia({ adapter: mysql2(connectionPool, { user: "user", key: "user_key", session: "user_session", }), env: "DEV", middleware: web(), sessionCookie: { expires: false }, getUserAttributes: (databaseUser) => { return { username: databaseUser.username, }; }, });

Slide 13

Slide 13 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 2. 一旦Freshに直接実装(routes) routes/_middleware.ts  認証状況の検証/情報付与/リダイレクト routes/create_account.ts  アカウント作成 routes/login.ts  ログイン routes/logout.ts  ログアウト これらを直接実装します。 // routes/create_account.tsx import type { HandlerContext, Handlers, } from "https://deno.land/x/[email protected]/server.ts"; import { LuciaError } from "npm:lucia"; import { luciaAuth } from "../utils/lucia_auth.ts"; export const handler: Handlers = { async POST(req: Request, ctx: HandlerContext) { const formData = await req.formData(); const rawUsername = formData.get("username"); const rawPassword = formData.get("password"); const createAccountValidateResult = createAccountValidate( rawUsername, rawPassword, ); if (!createAccountValidateResult.success) { return ctx.render({ errors: createAccountValidateResult.errors, rawUsername, }); } // … // 以下省略

Slide 14

Slide 14 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 3. プラグイン化する プラグインのディレクトリ構成は、 Fresh本体のようなディレクトリ構成 にしました。 ディレクトリ構成 plugins └─lucia_plugin ├─mod.ts ├─plugin.ts ├─deps.ts ├─routes │ ├─create_account.tsx │ ├─login.tsx │ └─logout.tsx └─middlewares └─session_middleware.ts Fresh 本体に寄せた構成

Slide 15

Slide 15 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 3. プラグイン化する(handler) - パラメータを引数にとり、 handlerを返す関数を作る // plugins/lucia_plugin/routes/logout.tsx import type { Auth, HandlerContext, Handlers } from "../deps.ts"; export function getLogoutHandler(luciaAuth: Auth):Handlers { // LuciaAuth は外から渡す形に変更 return { async GET(req: Request, _ctx: HandlerContext) { const authRequest = luciaAuth.handleRequest(req); const session = await authRequest.validate(); if (session) { luciaAuth.invalidateSession(session.sessionId); return new Response("", { status: 302, headers: { "Location": "/login" }, }); } else { return new Response("", { status: 302, headers: { "Location": req.referrer || "/" }, }); } }, }; }

Slide 16

Slide 16 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 3. プラグイン化する(プラグイン) - 引数を元に プラグインを返す関数を作る // plugins/lucia_plugin/plugin.ts(一部省略) import type { Auth, ComponentType, PageProps, Plugin } from "./deps.ts"; import { getLuciaSessionMiddleware } from "./middlewares/lucia_middleware.ts"; import Login, { getLoginHandler } from "./routes/create_account.tsx"; import { getLogoutHandler } from "./routes/logout.tsx"; export function getLuciaPlugin( luciaAuth: Auth, ): Plugin { return { name: "LuciaPlugin", middlewares: [{ middleware: { handler: getLuciaSessionMiddleware(luciaAuth), }, path: "/" }], routes: [ { path: "/create_account", handler: getCreateAccountHandler(luciaAuth), component: CreateAccount as ComponentType, }, ], }; }

Slide 17

Slide 17 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 3. プラグイン化する - mod.ts に取りまとめる プラグインの機能は 必ずここから呼び出す。 - Fresh本体のように、 deno.json にローカルのパスを 記述して運用する =>具体的パスが   ソースコードから消えて、   便利です。 // plugins/lucia_plugin/mod.ts export type {Auth, Session, HandlerContext} from "./deps.ts" export * from "./plugin.ts" // deno.json(抜粋) { "imports": { "$fresh/": "https://deno.land/x/[email protected]/", "preact": "https://esm.sh/[email protected]", "preact/": "https://esm.sh/[email protected]/", "preact-render-to-string": "https://esm.sh/*[email protected]", "@preact/signals": "https://esm.sh/*@preact/[email protected]", "@preact/signals-core": "https://esm.sh/*@preact/[email protected]", "twind": "https://esm.sh/[email protected]", "twind/": "https://esm.sh/[email protected]/", "$std/": "https://deno.land/[email protected]/", "lucia_plugin/": "./plugins/lucia_plugin/" // <= 追記 } }

Slide 18

Slide 18 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 3. プラグインを使用する - utils/lucia_auth.ts に分離した設定をプラグインに 渡すだけ。 - これだけで、 routesと middlewareを まとめて拡張できる。 // fresh.config.ts import { defineConfig } from "$fresh/server.ts"; import twindPlugin from "$fresh/plugins/twind.ts"; import twindConfig from "./twind.config.ts"; import {luciaAuth} from "./utils/lucia_auth.ts"; // deno.jsonのインポートマップ部分に追記したことで `./plugins/`は 不要に import {getLuciaPlugin} from "lucia_plugin/mod.ts"; export default defineConfig({ plugins: [ twindPlugin(twindConfig), // Luciaのインスタンスを引数にpluginを取得して割り当てる getLuciaPlugin(luciaAuth) ], });

Slide 19

Slide 19 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 3. プラグインの機能を使用するroutes プラグインから 一部定義を参照し、使用する。 型情報自体も、 プラグインから提供されるのが、 ベストだと思います。 // routes/index.tsx import {HandlerContext, Session } from "lucia_plugin/mod.ts"; export type WithSession = { state: { session?: Session } }; export type WithSessionHandlerContext = HandlerContext & WithSession; export default function Home({ state }: WithSessionHandlerContext) { return (

Login user: {state.session ? state.session.user?.username : "NO LOGIN"}

LOGOUT
); }

Slide 20

Slide 20 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 4. 改めて動作の様子

Slide 21

Slide 21 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 5. プラグイン化の前後比較 # ディレクトリ構成(プラグイン化後一部抜粋) ├─deno.json ├─dev.ts ├─fresh.config.ts ├─fresh.gen.ts ├─main.ts ├─twind.config.ts ├─components │ └─Button.tsx ├─islands │ └─Counter.tsx ├─plugins │ └─lucia_plugin # 今回実装したlucia_plugin本体 ├─routes │ ├─index.tsx │ ├─_404.tsx │ ├─_app.tsx │ ├─api │ │ └─joke.ts │ └─greet │ └─[name].tsx ├─static │ ├─favicon.ico │ └─logo.svg └─utils └─lucia_auth.ts # ディレクトリ構成(プラグイン化後一部抜粋) ├─deno.json ├─dev.ts ├─fresh.config.ts ├─fresh.gen.ts ├─main.ts ├─twind.config.ts ├─components │ └─Button.tsx ├─islands │ └─Counter.tsx ├─routes │ ├─index.tsx │ ├─_404.tsx │ ├─_app.tsx │ ├─_middleware.ts │ ├─create_account.tsx │ ├─login.tsx │ └─logout.tsx │ ├─api │ │ └─joke.ts │ └─greet │ └─[name].tsx ├─static │ ├─favicon.ico │ └─logo.svg └─utils └─lucia_auth.ts 拡張機能が、routes以下に並ぶ 拡張機能がプラグインの中に閉じている => 配布しやすい

Slide 22

Slide 22 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. まとめ - Freshのプラグインで、routesとmiddlewareをまとめて拡張できる - プラグインの中に閉じることで、一部見通しがよくなるとともに 機能の切り分けがはっきりする。 ログイン機能を用意したが、routes と middleware (と islands)の 結びつきが強い機能を作る(配布する)ならかなり有効と考えています。 例えば?、 管理画面(routes)+アクセスログ収集(middleware)+リアルタイムログ表示 (islands) をまとめて提供するプラグイン

Slide 23

Slide 23 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. おまけ https://github.com/Octo8080X/fresh_lucia_plugin 各種オプション増強版を公開中

Slide 24

Slide 24 text

Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. ありがとうございました