Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fresh(Deno)をプラグインで拡張しよう!

 Fresh(Deno)をプラグインで拡張しよう!

FreshのプラグインAPIを使用し、認証機能の導入を導入するプラグインの開発について
段階を踏んだ実装の紹介

https://toranoana-lab.hatenablog.com/entry/2023/12/06/100000
https://yumenosora.connpass.com/event/301110/
https://www.youtube.com/watch?v=0MX1ZwBwPCg

虎の穴ラボ株式会社

December 07, 2023
Tweet

More Decks by 虎の穴ラボ株式会社

Other Decks in Technology

Transcript

  1. 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 虎の穴ラボ 奥谷 一陽
  2. Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 自己紹介

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

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

    - Tailwind CSSプラグイン - Partials がFormに対応 - プラグインAPIの機能強化 - and more
  5. 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<State = Record<string, unknown>> { name: string; entrypoints?: Record<string, string>; render?(ctx: PluginRenderContext): PluginRenderResult; renderAsync?(ctx: PluginAsyncRenderContext): Promise<PluginRenderResult>; buildStart?(config: ResolvedFreshConfig): Promise<void> | void; buildEnd?(): Promise<void> | void; configResolved?(config: ResolvedFreshConfig): Promise<void> | void; routes?: PluginRoute[]; middlewares?: PluginMiddleware<State>[]; islands?: PluginIslands; }
  6. Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. routes

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

    と middleware をまとめて拡張するプラグイン例 プラグインをconfigに設定すると /sample にアクセスすると **samplePlugin middleware** と出力し <div>SAMPLE PLUGIN</div> を返す // 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], });
  8. Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. 1.

    作るFreshのプラグイン - 認証機能提供するnpmパッケージluciaを使ってのログイン機能 - luciaのStarter guidesベースで進めますが、 csrf対策不足なのでそのまま本番に適用せずよく確認してください。 - 認証機能のために実装するのは主に2つ - アカウントの作成、ログイン、ログアウトをするroutes - ログイン情報を事前処理するmiddaleware - なるべくFresh本体のroutes以下に手を入れずに 「プラグインだけ入れれば、認証機能が追加できる」プラグインを作 る。 - 一旦はFreshに直接実装してから、プラグインにします。
  9. 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, }; }, });
  10. 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, }); } // … // 以下省略
  11. 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 本体に寄せた構成
  12. 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 || "/" }, }); } }, }; }
  13. 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<PageProps>, }, ], }; }
  14. 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/" // <= 追記 } }
  15. 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) ], });
  16. 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 ( <div className="px-4 py-8 mx-auto bg-[#86efac]"> <div className="max-w-screen-md mx-auto flex flex-col items-center justify-center"> <p> Login user: {state.session ? state.session.user?.username : "NO LOGIN"} </p> <a href="/logout" className="my-4"> LOGOUT </a> </div> </div> ); }
  17. 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以下に並ぶ 拡張機能がプラグインの中に閉じている => 配布しやすい
  18. Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved. まとめ

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

    https://github.com/Octo8080X/fresh_lucia_plugin 各種オプション増強版を公開中