2023-11-12

RemixJS 使用 Jotai 取代 Context

RemixJS 使用 Jotai 取代 Context

把之前的專案,用 Jotai 取代 Context 做登入狀態的管理。原本的登入狀態是使用 cookie-session 處理的。

基本上,改用 Jotai 程式碼反而變多了,思路比較徧向以前端的方式解決問題。

以下是遇到的狀況和問題:

  1. 要將 loader 取得的資料放到 store,應該要使用 useHydrateAtoms hook。

  2. 使用 store.get()store.set() 時,似乎不會觸發 re-render;而必須使用 useAtom,應該是訂閱功能做在 useAtom 身上吧。

  3. 使用 Context 時,有些功能用後端功能處理就可以了;但 Jotai 必須使用前端的功能解決。例如登出時,使用 Context 只要在 logout 頁面的 loader() 處理即可。使用 Jotai 必須登出後,使用 useAtom 去變更 store 狀態。使用 google 登入時,有相同的問題。

  4. 經常遇到 Warning: Text content did not match 的問題。解決的方式是在使用 useAtom 的 setter 後,再使用 useNavigate 轉到別的頁面,讓頁面重新 render。

RemixJS, Cloudflare Pages 專案使用 Google Sign-in

RemixJS, Cloudflare Pages 專案使用 Google Sign-in

承上篇的環境設定,要使用 Google sign-in 但不使用官方套件 google-auth-library

可以使用 worker-auth-providers,它雖然還在 beta 版,但簡單的 oauth 登入是可行的。

GCP 基本設定

Google Cloud Platform (GCP) 設定並下載憑證資料:

  1. 到「API 程式庫」>「公開」>「社交」啟用 Google People API。

  2. 到「API和服務 > 憑證」,點選「+建立憑證」>「OAuth 用戶 ID」。

  3. 「應用程式類型」選「網頁應用程式」

  4. 「已授權的 JavaScript 來源」加入正式環境及測試環境的 URI。

  5. 「已授權的重新導向 URI」加入接收 Query String 參數的頁面 URI,同樣包含測試及正式環境。

  6. 建立完成後,「下載 OAuth 用戶端」的 JSON 檔。

  7. 到「OAuth 同意畫面」,設定應用程式名稱,及測試帳號等。

登入頁面

按了「Google 登入」的按鈕轉到 google-signin.tsx 頁面

// app/routes/google-signin.tsx import { type LoaderFunctionArgs, redirect } from "@remix-run/cloudflare"; import { google } from "worker-auth-providers"; import { getMySession } from "~/utils/sessions"; export async function loader({ request, context, params }: LoaderFunctionArgs) { const session = await getMySession(request); const userId = session.get("userId"); if (userId) { return redirect("/"); } const env: any = context.env; const googleLoginUrl = await google.redirect({ options: { clientId: env.GOOGLE_CLIENT_ID, redirectTo: env.GOOGLE_REDIRECT_URI, }, }); return redirect(googleLoginUrl); }

用戶登入後轉向的頁面

// import { type LoaderFunctionArgs, redirect } from "@remix-run/cloudflare"; import { getMySession, getHeadersWithSetCookie } from "~/utils/sessions"; import { google } from "worker-auth-providers"; import getSupabase from "~/utils/supabase-client"; export async function loader({ request, context, params }: LoaderFunctionArgs) { const env: any = context.env; const { user, tokens } = await google.users({ options: { clientSecret: env.GOOGLE_CLIENT_SECRET, clientId: env.GOOGLE_CLIENT_ID, redirectUrl: env.GOOGLE_REDIRECT_URI, }, request, }); // "user": { // "id": "111******", // "email": "shinder.lin@gmail.com", // "verified_email": true, // "name": "Shinder Lin", // "given_name": "Shinder", // "family_name": "Lin", // "picture": "...", // "locale": "zh-TW" // }, // console.log({ user, tokens }); // tokens: { // access_token: 'ya29...', // expires_in: 3599, // scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid', // token_type: 'Bearer', // id_token: 'eyJh...' // } if (!user?.id) { return redirect("/login"); } const supabase = getSupabase(context); const { data: data1, error: error1 } = await supabase .from("g_users") .select() .eq("gid", user.id); // 是否已經登入過 if (data1.length) { const row: any = data1[0]; if (row.blocked) { return { msg: "您的帳號已被停權" }; } else { await supabase .from("g_users") .update({ updated_at: new Date() }) .eq("gid", row.gid); } } else { const { data: data2, error: error2 } = await supabase .from("g_users") .insert({ gid: user.id, email: user.email, name: user.name }) .select(); if (!data2.length) { return { msg: "DB 無法新增資料" }; } } const session = await getMySession(request); session.set("gid", user.id); session.set("userId", user.email); session.set("nickname", user.name); const headers = await getHeadersWithSetCookie(session); return redirect('/', { headers }); }

2023-11-11

RemixJS, Supabase 專案發佈到 Cloudflare Pages

RemixJS, Supabase 專案發佈到 Cloudflare Pages

注意的事項

條列一下,Remix 從專案建立到發佈到 Cloudflare pages 該注意的事項:

  1. 建立專案時,直接使用 Cloudflare 的官方工具 npm create cloudflare,以方便發佈,參考Deploy a Remix site

  2. 原本由 @remix-run/node 匯入的類型 LinksFunction, LoaderFunctionArgs 或函式 json, redirect 改由 @remix-run/cloudflare 匯入。

  3. 和 NodeJS 相依的套件,儘量不要使用,例如 qs 套件。如果用到了,要在 remox.config.js 裡設定 polyfill。

  4. 開發時,環境變數設定在 .dev.vars 裡。在正式環境時,須到 Cloudflare pages 的專案裡,設定環境變數。

  5. 環境變數的取得,不是透過 process.env,是透過 context.env。而 context 物件是由 LoaderFunctionArgs 或 ActionFunctionArgs ({ request, context, params }) 而來。

在測試的專案用到 bcryptjs,而該套件使用到 Node 的 crypto,因此要如下設定:

// remox.config.js /** @type {import('@remix-run/dev').AppConfig} */ export default { /* 略 */ browserNodeBuiltinsPolyfill: { modules: { crypto: true } }, // bcryptjs 需要 };

Supabase 相關

連線 supabase

// app/utils/supabase-client.ts import { createClient } from "@supabase/supabase-js"; let supabase; export default function getSupabase(context={}){ if(! supabase) { supabase = createClient( context.env.SUPABASE_URL, context.env.SUPABASE_ANON_KEY ); } return supabase; }

新增

const result = await supabase .from("address_book") .insert({ name, email, mobile, birthday, address, creator_gid: myAuth.gid }) .select();

讀取

算數量

let totalRows = 0; const tmpSupabase = supabase .from("address_book") .select("*", { count: "exact", head: true }); if (searchStr) { const { count } = await tmpSupabase.or( `name.like.%${searchStr}%,mobile.like.%${searchStr}%` ); totalRows = count; } else { const { count } = await tmpSupabase; totalRows = count; }

條件搜尋

if (searchStr) { const { data, error } = await supabase .from("address_book") .select() .or(`name.like.%${searchStr}%,mobile.like.%${searchStr}%`) .order("sid", { ascending: false }) .range((page - 1) * perPage, page * perPage - 1); if (!error) { rows = data; } } else { const { data, error } = await supabase .from("address_book") .select() .order("sid", { ascending: false }) .range((page - 1) * perPage, page * perPage - 1); if (!error) { rows = data; } }

更新

let query = supabase .from("address_book") .update({ name, email, mobile, birthday, address }) .eq("sid", sid); let result; if (myAuth.gid === context.env.ADMIN_GID) { result = await query; } else { result = await query.eq("creator_gid", myAuth.gid); }

刪除

let result; let query = supabase.from("address_book").delete().eq("sid", sid); if (myAuth.gid === context.env.ADMIN_GID) { result = await query; } else { result = await query.eq("creator_gid", myAuth.gid); }

FB 留言