2025-03-10

使用 homebrew 安裝 apache2 和 php 環境

使用 homebrew 安裝 apache2 和 php 環境

XAMPP 的 mac 版本現在都比較老舊,而且在 macOS 15 有無法啟動 apache 的問題。 為了教學需求,在此記錄 mac 的 php 環境的建立方式。假設 homebrew 的環境已經安裝完成。 如果要使用 ubuntu vm 可以參考上一篇 https://qops.blogspot.com/2025/03/orbstack-ubuntu-224-php-56-apache2.html 的作法。

安裝 apache2

# 先查看是否已安裝 brew info apache2 # 前幾行若出現 Not installed 表示沒有安裝,後面也有設定的說明
# 安裝 brew install apache2 # 安裝後,注意預先的設定 # -- DocumentRoot is /opt/homebrew/var/www # -- 主設定檔位置 /opt/homebrew/etc/httpd/httpd.conf # -- ssl 設定檔位置 /opt/homebrew/etc/httpd/extra/httpd-ssl.conf # -- 預設的 ports 為 8080 和 8443 # 啟動服務 brew services start httpd # 在瀏覽器上查看 http://localhost:8080 # 重新啟動服務 brew services restart httpd # 停止服務 brew services stop httpd

安裝 php

這裡是以 php 8.3 的版本為例說明,當然你可以自行決定要安裝的版本。

# 先查看要安裝的版本資訊 brew info php@8.3 # 安裝 brew install php@8.3
# 在 apache 的設定檔,放入下式 LoadModule php_module /opt/homebrew/opt/php@8.3/lib/httpd/modules/libphp.so <FilesMatch \.php$> SetHandler application/x-httpd-php </FilesMatch> # 設定索引檔 DirectoryIndex index.php index.html

PHP 的設定檔位於 /opt/homebrew/etc/php/8.3/ 裡面。

# 若要使用 php 指令,可以將下兩行放入用戶設定檔 (.zshrc) 裡 export PATH="/opt/homebrew/opt/php@8.3/bin:$PATH" export PATH="/opt/homebrew/opt/php@8.3/sbin:$PATH"

在根目錄 /opt/homebrew/var/www 寫一支 phpinfo() 檔測試看看。

mysql 8.0

# 先查看要安裝的版本資訊 brew info mysql@8.0 # 安裝 brew install mysql@8.0 # 啟動服務 brew services start mysql@8.0

2025-03-03

OrbStack 安裝 Ubuntu 22.4, php 5.6 及 apache2

OrbStack 安裝 Ubuntu 22.4, php 5.6 及 apache2

某個專案還在老舊的 php 5.6 環境,macOS 15 Sequoia 目前已無法正常執行 XAMPP。在不小心升級 macOS 後,測試 php 5.6 程式是讓人困擾的問題。

原本想要使用 OrbStack 的 docker containers 功能,然而 container 需要不少設定,反而是一種麻煩的做法。後來還是以 VM 的方式來處理。OrbStack 的安裝就不贅述。

安裝 ubuntu vm

# *** 安裝 ubuntu vm *** # orb create --help # orb create [flags] DISTRO[:VERSION] [MACHINE_NAME] orb create ubuntu:jammy # 預設的 machine name 為 ubuntu
# *** 從 mac ssh 連線 vm *** # ssh [用戶名]@[虛擬主機名]@orb ssh root@ubuntu@orb
# *** 使用 vm 的 domain *** # ping [虛擬主機名].orb.local ping ubuntu.orb.local # mac host 的 ip 可以直接使用區域網路 ip

在 vm 中可以透過 /mnt/mac 路徑找到 mac 的對應檔案。

安裝 php 5.6

以 ssh 登入 vm 後,依下列步驟安裝 php 5.6

# *** 1. 更新系統 repo 資料 apt update # *** 2. 安裝 software-properties-common 以管理 PPA (Personal Package Archives) apt install software-properties-common # *** 3. php 5.6 已經不是官方維護的版本,需依賴 PPA add-apt-repository ppa:ondrej/php # *** 4. 再更新系統 repo 資料一次 apt update # *** 5. 安裝 php 5.6 apt install php5.6 # *** 6. 安裝 php 常用套件 apt install php5.6-mysql php5.6-curl php5.6-json php5.6-cgi php5.6-xml php5.6-mbstring php5.6-gd # *** 7. 確認 php 版本 php -v # *** 8. 啟用 apache2 模組 # 在安裝 apache2 之後才可以執行 # a2dismod php8.3 # 若有需要先關閉新模組 a2enmod php5.6

安裝 apache2

apt install apache2
# 啟用 rewrite module a2enmod rewrite
# /etc/apache2/mods-enabled 裡啟用的模組參考 access_compat.load -> ../mods-available/access_compat.load alias.conf -> ../mods-available/alias.conf alias.load -> ../mods-available/alias.load auth_basic.load -> ../mods-available/auth_basic.load authn_core.load -> ../mods-available/authn_core.load authn_file.load -> ../mods-available/authn_file.load authz_core.load -> ../mods-available/authz_core.load authz_host.load -> ../mods-available/authz_host.load authz_user.load -> ../mods-available/authz_user.load autoindex.conf -> ../mods-available/autoindex.conf autoindex.load -> ../mods-available/autoindex.load cache.load -> ../mods-available/cache.load cache_disk.conf -> ../mods-available/cache_disk.conf cache_disk.load -> ../mods-available/cache_disk.load deflate.conf -> ../mods-available/deflate.conf deflate.load -> ../mods-available/deflate.load dir.conf -> ../mods-available/dir.conf dir.load -> ../mods-available/dir.load env.load -> ../mods-available/env.load expires.load -> ../mods-available/expires.load file_cache.load -> ../mods-available/file_cache.load filter.load -> ../mods-available/filter.load headers.load -> ../mods-available/headers.load mime.conf -> ../mods-available/mime.conf mime.load -> ../mods-available/mime.load mpm_prefork.conf -> ../mods-available/mpm_prefork.conf mpm_prefork.load -> ../mods-available/mpm_prefork.load negotiation.conf -> ../mods-available/negotiation.conf negotiation.load -> ../mods-available/negotiation.load php5.6.conf -> ../mods-available/php5.6.conf php5.6.load -> ../mods-available/php5.6.load reqtimeout.conf -> ../mods-available/reqtimeout.conf reqtimeout.load -> ../mods-available/reqtimeout.load rewrite.load -> ../mods-available/rewrite.load setenvif.conf -> ../mods-available/setenvif.conf setenvif.load -> ../mods-available/setenvif.load socache_shmcb.load -> ../mods-available/socache_shmcb.load ssl.conf -> ../mods-available/ssl.conf ssl.load -> ../mods-available/ssl.load status.conf -> ../mods-available/status.conf status.load -> ../mods-available/status.load

安裝 Mariadb 10

# *** 1. 更新套件資訊 apt update # *** 2. 查看可以安裝的子版本 apt-cache policy mariadb-server # *** 3. 安裝 apt install mariadb-server
# 以 mysql client 使用 root 登入 mysql -uroot # 變更 root 密碼 ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密碼'; # 建立管理用戶給 phpmyadmin 使用 CREATE USER 'pmauser'@'localhost' IDENTIFIED BY 'pmauser'; GRANT ALL PRIVILEGES ON *.* TO 'pmauser'@'localhost' WITH GRANT OPTION; FLUSH PRIVILEGES; # 查看使用者 SELECT User, Host FROM mysql.user;

2024-08-24

使用前端 JS 接收 Ollama 回應的 Ndjson 格式

使用前端 JS 接收 Ollama 回應的 Ndjson 格式

基本上就是使用 fetch API 來處理串流資料,利用 async/await 就可以使用無窮迴圈來處理。

<input type="text" id="chat" value="台灣位於哪裡?" /> <button onclick="startAsk()">開始</button> <div id="result_div"></div>
const chat = document.querySelector("#chat"); const result_div = document.querySelector("#result_div"); const model = "mistral-nemo"; // ollama 使用的 LLM 模型名稱 const startAsk = async () => { const response = await fetch(`http://localhost:11434/api/generate`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ model, prompt: chat.value }), }); if (!response.ok) { console.log("回應發生錯誤!!!"); return; } // ['content-type', 'application/x-ndjson'] console.log(...response.headers); const reader = response.body.getReader(); // 讀取器 const decoder = new TextDecoder("utf8"); // 解碼器 while (true) { const { value, done } = await reader.read(); const json = decoder.decode(value); // 基本上每一行都是 json console.log(json); try { const obj = JSON.parse(json); result_div.innerText += obj.response; } catch (ex) { console.log(ex); console.log("不是 JSON:", `---${json}---`); } if (done) { result_div.innerText += "\n\n"; break; } } };

2024-07-15

在 M1 macOS上執行 tensorflow 的環境

在 M1 macOS上執行 tensorflow 的環境

目前 M3 的晶片都出好一陣子了,想說 M1 跑 tensorflow 應該沒問題。 結果不管是 python 3.11 或 3.12 都無法正常執行使用到 tensorflow 的專案。

爬文都只能爬到 python 3.8 的環境,3.8 也被評為執行 tensorflow 最為穩定的版本。 後來只好安裝 3.8 再使用 3.8 建立虛擬環境。

參考 Apple 官方文件 https://developer.apple.com/metal/tensorflow-plugin/

# 安裝 python 3.8 brew install python@3.8 mkdir proj-dir cd proj-dir # 建立虛擬環境 python3.8 -m venv venv # 啟動虛擬環境 source ./venv/bin/activate # 安裝 tensorflow 2.12 的版本 pip install tensorflow-macos # 安裝 tensorflow-metal pip install tensorflow-metal

測試官方的範例,有出現 Warning 和 Notice 不過可以正常執行。

2024-02-04

Apache HTTPS 服務間歇停止的問題

Apache HTTPS 服務間歇停止的問題

2024-02-01 開始客戶的網站就發生服務中斷的情況(其實這狀況當時應該發生好幾天了)。

主機架在 DigitalOcean 的 SGP1 上,由於 SGP1 網路設備曾有停擺快 2 天的黑記錄;所以一開始就懷疑是不是網路設備不穩。

以往的經驗,通常停止十分鐘左右就會恢復正常,但這次卻不是想像中單純。

服務大概停擺十分鐘後,就恢復正常,但是每隔一到兩小時就發生一次服務停擺,實著讓人頭疼。

一開始是毫無頭緒到底是什麼問題,也發 ticket 給 DO,DO 回應希望進一步提供資料。

懷疑過:網路設備問題、程式沒寫好、SSL 憑證問題、MySQL 資料庫問題等,大伙搞了兩天也沒離清問題點,只發現 HTTPS 受影響,但 HTTP 似乎沒受影響。可是 HTTPS 中斷服務時, HTTP 也變慢了。

大家的想法都一樣,HTTPS 和 HTTP 都是 Apache 伺服器的服務;沒道理 HTTPS 停擺,而 HTTP 正常。所以焦點又被轉移到 SSL 憑證上。

第三天開始,我終於有比較多的時間,從調 MySQL 開始搞了半天,沒什麼進展。 後來在一堆 Apache error log 中,某個停擺時點找到一條:

[Sat Feb 03 23:54:11.819500 2024] [mpm_prefork:error] [pid 8964] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting

很少在搞設定的我,只好查一下資料 mpm_prefork 是什麼模組,然後網路上也沒查到上面的錯誤會造成什麼問題。 後來在 stackoverflow 找到這篇 AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting

硬著頭皮依照建議設定 mpm_prefork.conf,心想應該就是這個問題。 一覺起來,果然有改善,再依 log 做些調整。

# prefork MPM # StartServers: number of server processes to start # MinSpareServers: minimum number of server processes which are kept spare # MaxSpareServers: maximum number of server processes which are kept spare # MaxRequestWorkers: maximum number of server processes allowed to start # MaxConnectionsPerChild: maximum number of requests a server process serves <IfModule mpm_prefork_module> StartServers 10 MinSpareServers 10 MaxSpareServers 20 ServerLimit 2000 MaxRequestWorkers 1536 MaxConnectionsPerChild 10000 </IfModule>

幾天的心情低落就因為一個設定。

另一個有趣的事情,就是在 stackoverflow 找資料時,看到一篇症狀和我們遇到的差不多狀況。 但傻眼的是,他自己解答自己的問題,答案是「公司找到會設定 Apache 的人才,他弄一下設定就好了」,沒有提到任何的模組或設定...

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); }

2023-10-26

RemixJS 2 保有登入狀態

RemixJS 2 保有登入狀態

呈上篇 RemixJS 2 以 session 實作登入機制,session 的資料存放在 cookie 裡,那要如何在頁面內判斷及保有登入的狀態?

在前端可以用原本 react 的處理方式,以 context 來保有狀態。在後端必須每個 request 都要檢查 cookie。

get-auth.ts 是將解讀 cookie 的功能寫成模組,用來判斷是否為登入狀態。

// app/modules/get-auth.ts import { getMySession } from "~/modules/sessions"; async function getAuth(request: Request) { const session = await getMySession(request); const userId = session.get("userId") || ""; const nickname = session.get("nickname") || ""; return { userId, nickname, auth: !!userId }; // auth 屬性用來判斷是否登入 } export default getAuth;

AuthContextProvider

// app/contexts/AuthContext.tsx import React, { createContext } from "react"; export type AuthDataType = { userId: string | undefined; nickname: string | undefined; }; export const AuthContext = createContext<AuthDataType>({ userId: "", nickname: "", }); type PropsType = { userId: string | undefined; nickname: string | undefined; children: React.ReactNode; }; export function AuthContextProvider({ userId, nickname, children }: PropsType) { return ( <AuthContext.Provider value={{ userId, nickname }}> {children} </AuthContext.Provider> ); }

<AuthContextProvider> 包住 <Outlet />

// app/root.tsx 部份內容 export async function loader({ request, params }: LoaderFunctionArgs) { console.log("App loader"); return await getAuth(request); } export default function App() { const loaderData = useLoaderData<typeof loader>() return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> <AuthContextProvider userId={loaderData.userId} nickname={loaderData.nickname}> <Outlet /> </AuthContextProvider> <ScrollRestoration /> <Scripts /> <LiveReload /> </body> </html> ); }

防止未授權而使用功能

這裡要分兩個部份 loader 和 action。loader 因為有階層關係,可以在 Layout 的 loader 做阻擋的動作。

// app/routes/address-book.tsx 的 loader 部份 export async function loader({ request, params }: LoaderFunctionArgs) { console.log("address-book loader"); const myAuth = await getAuth(request); if (!myAuth.auth) { // 沒有登入,轉到登入頁面 return redirect(`/login?u=${request.url}`); } return null; }

action 就必須在每個頁面處理。

export async function action({ request, params }: ActionFunctionArgs) { const myAuth = await getAuth(request); if (!myAuth.auth) { // 沒有登入,轉到登入頁面 return redirect(`/login?u=${request.url}`); } // 略 }

2023-10-23

RemixJS 2 以 session 實作登入機制

RemixJS 2 以 session 實作登入機制

一般是以 cookie 存放 session id,session 資料存放在後端,可以是記憶體、檔案或資料庫。 但如果考慮到分散式架構,記憶體和檔案都是不可行的。

以識別用戶而言,其實資料很少一個變數值即可,其餘想放的資料頂多三四個變數值。此時使用官方範例的 createCookieSessionStorage 是個不錯的作法,將資料直接放在 cookie 並加密即可,當然除了好的加密密碼,cookie 最好設定為 httpOnly。

session 工具

使用官方的範例,同時包裝兩個方法 getMySessiongetHeadersWithSetCookie 省點囉唆的動作。

// app/modules/sessions.ts import { Session, createCookieSessionStorage } from "@remix-run/node"; type SessionData = { userId: string; nickname: string; }; type SessionFlashData = { error: string; }; const { getSession, commitSession, destroySession } = createCookieSessionStorage<SessionData, SessionFlashData>({ cookie: { name: "__session", // domain: "remix.run", // expires: new Date(Date.now() + 60_000), httpOnly: true, // maxAge: 60, path: "/", sameSite: "lax", secrets: ["your_secret_key"], // secure: true, }, }); const getMySession = async (request: Request) => { return await getSession(request.headers.get("Cookie")); }; const getHeadersWithSetCookie = async (session: Session) => { return { "Set-Cookie": await commitSession(session), }; }; export { getSession, commitSession, destroySession, getMySession, getHeadersWithSetCookie, };

登入頁

關於登入頁幾點請注意:

  1. 進入登入頁前,先判斷用戶是否已經登入,若是則轉向別的頁面。
  2. 表單有兩個欄位 email 和 password,email 為 userId。
  3. 帳號和密碼直接寫在程式碼裡是不好的作法,這只是個 demo。
  4. 確定登入後,將兩個值寫入 cookie,分別是 userId 和 nickname。
// app/routes/login.tsx // 略 export async function loader({ request, params }: LoaderFunctionArgs) { const session = await getMySession(request); const userId = session.get("userId"); if (userId) { // 如果已經登入 const uStr = getQueryStringObject(request)["u"]; if (uStr) { return redirect(uStr); } else { return redirect("/"); } } return null; } export default function LoginPage() { // 略 return ( <> <Form method="post" encType="multipart/form-data"> {/* 略 */} <button type="submit" className="btn btn-primary"> 登入 </button> </Form> </> ); } type loginUser = { email: string; nickname: string; password: string }; export async function action({ request, params }: ActionFunctionArgs) { const users: loginUser[] = [ { email: "lsd0125@gmail.com", password: "12345678", nickname: "小嘟" }, { email: "shin@gmail.com", password: "345678", nickname: "肥肥" }, ]; const session = await getMySession(request); const body = await getBodyObject(request); const { email, password } = body; const theUser = users.find((u) => { return u.email === email && u.password === password; }); const output = { success: !!theUser, bodyData: body, }; if (theUser) { session.set("userId", email); session.set("nickname", theUser.nickname); const headers = await getHeadersWithSetCookie(session); return json(output, { headers }); } return output; }

登出功能

登出是直接把兩個值從 cookie 中移除。

// app/routes/logout.tsx import { type LoaderFunctionArgs, redirect } from "@remix-run/node"; import { getMySession, getHeadersWithSetCookie } from "~/modules/sessions"; import { getQueryStringObject } from "~/modules/handle-request-data"; export async function loader({ request, params }: LoaderFunctionArgs) { const session = await getMySession(request); session.unset("userId"); session.unset("nickname"); const headers = await getHeadersWithSetCookie(session); const uStr = getQueryStringObject(request)["u"]; if(uStr){ return redirect(uStr, { headers }); } else { return redirect("/", { headers }); } }

有 userId 後,其它要存放在後端的資料,就可以存到資料庫裡了,不要把太多資料放在 cookie 裡,通常不要超過 4K。

FB 留言