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
    虎の穴ラボ 奥谷 一陽

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. 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,
    },
    ],
    };

    View full-size slide

  8. 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],
    });

    View full-size slide

  9. Copyright (C) 2023 Toranoana Lab Inc. All Rights Reserved.
    このように、routes と middleware を

    まとめて拡張できる


    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. 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,
    };
    },
    });

    View full-size slide

  13. 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,
    });
    }
    // …
    // 以下省略

    View full-size slide

  14. 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 本体に寄せた構成

    View full-size slide

  15. 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 || "/" },
    });
    }
    },
    };
    }

    View full-size slide

  16. 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,
    },
    ],
    };
    }

    View full-size slide

  17. 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/" // <= 追記
    }
    }

    View full-size slide

  18. 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)
    ],
    });

    View full-size slide

  19. 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



    );
    }

    View full-size slide

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

    View full-size slide

  21. 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以下に並ぶ
    拡張機能がプラグインの中に閉じている
    => 配布しやすい

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide