vợ bảo okeoke

This commit is contained in:
2026-01-02 22:00:50 +07:00
parent 16f64c5e4b
commit 2b0f88fa16
19 changed files with 415 additions and 87 deletions

View File

@@ -0,0 +1,57 @@
import { TinyRpcClientAdapter, TinyRpcError } from "@hiogawa/tiny-rpc";
import { Result } from "@hiogawa/utils";
const GET_PAYLOAD_PARAM = "payload";
export function httpClientAdapter(opts: {
url: string;
pathsForGET?: string[];
}): TinyRpcClientAdapter {
return {
send: async (data) => {
const url = [opts.url, data.path].join("/");
const payload = JSON.stringify(data.args);
const method = opts.pathsForGET?.includes(data.path)
? "GET"
: "POST";
let req: Request;
if (method === "GET") {
req = new Request(
url +
"?" +
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload })
);
} else {
req = new Request(url, {
method: "POST",
body: payload,
headers: {
"content-type": "application/json; charset=utf-8",
},
credentials: "include",
});
}
let res: Response;
res = await fetch(req);
if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`);
throw new Error(
JSON.stringify({
status: res.status,
statusText: res.statusText,
data: { message: await res.text() },
internal: true,
})
);
// throw TinyRpcError.deserialize(res.status);
}
const result: Result<unknown, unknown> = JSON.parse(
await res.text()
);
if (!result.ok) {
throw TinyRpcError.deserialize(result.value);
}
return result.value;
},
};
}

View File

@@ -0,0 +1,69 @@
import { TinyRpcClientAdapter, TinyRpcError } from "@hiogawa/tiny-rpc";
import { Result } from "@hiogawa/utils";
import { tryGetContext } from "hono/context-storage";
const GET_PAYLOAD_PARAM = "payload";
export function httpClientAdapter(opts: {
url: string;
pathsForGET?: string[];
}): TinyRpcClientAdapter {
return {
send: async (data) => {
const url = [opts.url, data.path].join("/");
const payload = JSON.stringify(data.args);
const method = opts.pathsForGET?.includes(data.path)
? "GET"
: "POST";
let req: Request;
if (method === "GET") {
req = new Request(
url +
"?" +
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload })
);
} else {
req = new Request(url, {
method: "POST",
body: payload,
headers: {
"content-type": "application/json; charset=utf-8",
},
credentials: "include",
});
}
let res: Response;
if (import.meta.env.SSR) {
const c = tryGetContext<any>();
if (!c) {
throw new Error("Hono context not found in SSR");
}
Object.entries(c.req.header()).forEach(([k, v]) => {
req.headers.append(k, v);
});
res = await c.get("fetch")(req);
} else {
res = await fetch(req);
}
if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`);
throw new Error(
JSON.stringify({
status: res.status,
statusText: res.statusText,
data: { message: await res.text() },
internal: true,
})
);
// throw TinyRpcError.deserialize(res.status);
}
const result: Result<unknown, unknown> = JSON.parse(
await res.text()
);
if (!result.ok) {
throw TinyRpcError.deserialize(result.value);
}
return result.value;
},
};
}

View File

@@ -164,13 +164,14 @@ const login = async (username: string, password: string) => {
};
async function checkAuth() {
console.log("Check auth called");
const context = getContext<HonoVarTypes>();
const token = getCookie(context, 'auth_token');
if (!token) {
return { authenticated: false, user: null };
}
try {
const payload = await verify(token, JWT_SECRET) as any;
@@ -178,10 +179,11 @@ async function checkAuth() {
const userRecord = Array.from(mockUsers.values()).find(
record => record.user.id === payload.sub
);
if (!userRecord) {
return { authenticated: false, user: null };
}
// console.log("Check auth called 2", userRecord);
return {
authenticated: true,

View File

@@ -5,6 +5,8 @@ import {
} from "@hiogawa/tiny-rpc";
import type { RpcRoutes } from "./rpc";
import { Result } from "@hiogawa/utils";
import {httpClientAdapter} from "@httpClientAdapter";
// console.log("httpClientAdapter module:", httpClientAdapter.toString());
declare let __host__: string;
const endpoint = "/rpc";
const url = import.meta.env.SSR ? "http://localhost" : "";
@@ -15,65 +17,3 @@ export const client = proxyTinyRpc<RpcRoutes>({
pathsForGET: [],
}),
});
const GET_PAYLOAD_PARAM = "payload";
function httpClientAdapter(opts: {
url: string;
pathsForGET?: string[];
}): TinyRpcClientAdapter {
return {
send: async (data) => {
const url = [opts.url, data.path].join("/");
const payload = JSON.stringify(data.args);
const method = opts.pathsForGET?.includes(data.path)
? "GET"
: "POST";
let req: Request;
if (method === "GET") {
req = new Request(
url +
"?" +
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload })
);
} else {
req = new Request(url, {
method: "POST",
body: payload,
headers: {
"content-type": "application/json; charset=utf-8",
},
credentials: "include",
});
}
let res: Response;
if (import.meta.env.SSR) {
const { getContext } = await import("hono/context-storage");
const c = getContext<any>();
Object.entries(c.req.header()).forEach(([k, v]) => {
req.headers.append(k, v);
});
res = await c.get("fetch")(req);
} else {
res = await fetch(req);
}
if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`);
throw new Error(
JSON.stringify({
status: res.status,
statusText: res.statusText,
data: { message: await res.text() },
internal: true,
})
);
// throw TinyRpcError.deserialize(res.status);
}
const result: Result<unknown, unknown> = JSON.parse(
await res.text()
);
if (!result.ok) {
throw TinyRpcError.deserialize(result.value);
}
return result.value;
},
};
}

View File

@@ -0,0 +1,15 @@
import { useHead, UseHeadInput, UseHeadOptions } from "@unhead/vue";
import { defineComponent, toRef } from "vue";
interface VueHeadProps {
input: UseHeadInput;
options?: UseHeadOptions;
}
export const VueHead = defineComponent<VueHeadProps>({
name: "VueHead",
props: ["input", "options"],
setup(props) {
useHead(toRef(props, "input") as any, props.options);
return () => null;
}
});
export default VueHead;

View File

@@ -8,11 +8,12 @@ import { contextStorage } from 'hono/context-storage';
import { cors } from "hono/cors";
import { jwtRpc, rpcServer } from './api/rpc';
import isMobile from 'is-mobile';
import { useAuthStore } from './stores/auth';
const app = new Hono()
// app.use(renderer)
app.use('*', contextStorage());
app.use(cors(), async (c, next) => {
c.set("fetch", app.request.bind(app));
const ua = c.req.header("User-Agent")
@@ -21,15 +22,20 @@ app.use(cors(), async (c, next) => {
};
c.set("isMobile", isMobile({ ua }));
await next();
}, contextStorage(), rpcServer);
}, rpcServer);
app.get("/.well-known/*", (c) => {
return c.json({ ok: true });
});
app.get("*", async (c) => {
const url = new URL(c.req.url);
const { app, router, head } = createApp();
router.push(url.pathname);
await router.isReady();
const { app, router, head, pinia } = createApp();
app.provide("honoContext", c);
await router.push(url.pathname);
await router.isReady().then(() => {
const auth = useAuthStore();
auth.initialized = false;
auth.init();
});
return streamText(c, async (stream) => {
c.header("Content-Type", "text/html; charset=utf-8");
c.header("Content-Encoding", "Identity");
@@ -43,8 +49,8 @@ app.get("*", async (c) => {
await stream.write(buildBootstrapScript());
await stream.write("</head><body class='font-sans bg-[#f9fafd] text-gray-800 antialiased flex flex-col min-h-screen'>");
await stream.pipe(appStream);
let json = htmlEscape(JSON.stringify(JSON.stringify(ctx)));
await stream.write(`<script>window.__SSR_STATE__ = JSON.parse(${json});</script>`);
await stream.write(`<script>window.__SSR_STATE__ = JSON.parse(${htmlEscape(JSON.stringify(JSON.stringify(ctx)))});</script>`);
await stream.write(`<script>window.__PINIA_STATE__ = JSON.parse(${htmlEscape(JSON.stringify(JSON.stringify(pinia.state.value)))});</script>`);
await stream.write("</body></html>");
});
// return c.body(renderToWebStream(app, {}));

View File

@@ -10,10 +10,10 @@ export function withErrorBoundary(WrappedComponent: any) {
<div class="p-8 space-y-lg max-w-lg w-full">
<p>
<b>500.&nbsp;</b>
<ins class="text-gray-500 decoration-none">Đã xảy ra lỗi.</ins>
<ins class="text-gray-500 decoration-none">Something went wrong.</ins>
</p>
<div class="font-thin">
<p>Máy chủ đang gặp sự cố tạm thời không thể xử yêu cầu của bạn. Vui lòng <a class="underline text-primary" href="/">thử lại</a> sau vài phút.</p>
<p>The server is currently experiencing temporary issues and cannot process your request. Please <a class="underline text-primary" href="/">try again</a> in a few minutes.</p>
</div>
</div>
</div>

View File

@@ -10,18 +10,21 @@ import Aura from '@primeuix/themes/aura';
import { createPinia } from "pinia";
import { useAuthStore } from './stores/auth';
const pinia = createPinia();
export function createApp() {
const pinia = createPinia();
const app = createSSRApp(withErrorBoundary(RouterView));
const head = import.meta.env.SSR ? SSRHead() : CSRHead();
app.use(head);
app.use(PrimeVue, {
// unstyled: true,
theme: {
preset: Aura,
options: {
darkModeSelector: '.my-app-dark',
cssLayer: false,
prefix: 'pv-',
}
}
});
@@ -42,5 +45,5 @@ export function createApp() {
});
}
return { app, router, head };
return { app, router, head, pinia };
}

12
src/routes/NotFound.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<vue-head :input="{title: '404 - Page Not Found'}"/>
<div class="mx-auto text-center mt-20 flex flex-col items-center gap-4">
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<router-link class="btn btn-primary" to="/">Go back to Home</router-link>
</div>
</template>
<script setup lang="ts">
import { VueHead } from "@/components/VueHead";
</script>

View File

@@ -75,6 +75,11 @@ const routes: RouteData[] = [
component: () => import("./add/Add.vue"),
},
],
},
{
path: "/:pathMatch(.*)*",
name: "not-found",
component: () => import("./NotFound.vue"),
}
],
},
@@ -88,6 +93,7 @@ const router = createRouter({
router.beforeEach((to, from, next) => {
const auth = useAuthStore();
console.log("Call on server:", Math.random());
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!auth.user) {
next({ name: "login" });

View File

@@ -20,10 +20,15 @@ export const useAuthStore = defineStore('auth', () => {
// Check auth status on init (reads from cookie)
async function init() {
if (initialized.value) return;
console.log("Auth store init called");
// if (initialized.value) return;
try {
const response = await client.checkAuth();
const response = await client.checkAuth().then((res) => {
console.log("call", res);
return res;
});
if (response.authenticated && response.user) {
user.value = response.user;
// Get CSRF token if authenticated

10
src/type.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/// <reference types="vite/client" />
/// <reference types="unplugin-vue-components/types/vue" />
declare module "@httpClientAdapter" {
import { TinyRpcClientAdapter } from "@hiogawa/tiny-rpc";
export function httpClientAdapter(opts: {
url: string;
pathsForGET?: string[];
}): TinyRpcClientAdapter;
}