develop-updateui #1
@@ -9,7 +9,7 @@ plugins:
|
||||
# opt:
|
||||
# - paths=source_relative
|
||||
- remote: buf.build/community/stephenh-ts-proto
|
||||
out: ../stream-ui/src/server/utils/proto
|
||||
out: ./src/server/utils/proto
|
||||
opt:
|
||||
- env=node
|
||||
- esModuleInterop=true
|
||||
|
||||
3
bun.lock
3
bun.lock
@@ -8,6 +8,7 @@
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@hattip/adapter-node": "^0.0.49",
|
||||
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
|
||||
"@hono/node-server": "^1.19.11",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@pinia/colada": "^0.21.7",
|
||||
@@ -190,6 +191,8 @@
|
||||
|
||||
"@hattip/walk": ["@hattip/walk@0.0.49", "", { "dependencies": { "@hattip/headers": "0.0.49", "cac": "^6.7.14", "mime-types": "^2.1.35" }, "bin": { "hattip-walk": "cli.js" } }, "sha512-AgJgKLooZyQnzMfoFg5Mo/aHM+HGBC9ExpXIjNqGimYTRgNbL/K7X5EM1kR2JY90BNKk9lo6Usq1T/nWFdT7TQ=="],
|
||||
|
||||
"@hiogawa/tiny-rpc": ["@hiogawa/tiny-rpc@0.2.3-pre.18", "", {}, "sha512-BiNHrutG9G9yV622QvkxZxF+PhkaH2Aspp4/X1KYTfnaQTcg4fFUTBWf5Kf533swon2SuVJwi6U6H1LQbhVOQQ=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||
|
||||
"@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="],
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@hattip/adapter-node": "^0.0.49",
|
||||
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
|
||||
"@hono/node-server": "^1.19.11",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@pinia/colada": "^0.21.7",
|
||||
|
||||
@@ -3,7 +3,7 @@ syntax = "proto3";
|
||||
package stream.User.v1;
|
||||
|
||||
option go_package = "stream/proto/gen/go/User/v1;Userv1";
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
service UserService {
|
||||
@@ -14,6 +14,7 @@ service UserService {
|
||||
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
|
||||
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
|
||||
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
|
||||
rpc UpdateUserPassword(UpdateUserPasswordRequest) returns (google.protobuf.Empty);
|
||||
|
||||
// Preferences
|
||||
rpc GetPreferences(GetPreferencesRequest) returns (GetPreferencesResponse);
|
||||
@@ -21,7 +22,10 @@ service UserService {
|
||||
}
|
||||
|
||||
// ─── User Messages ───────────────────────────────────────────────────────────
|
||||
|
||||
message UpdateUserPasswordRequest {
|
||||
string id = 1;
|
||||
string new_password = 2;
|
||||
}
|
||||
message GetUserRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
1962
src/api/client.ts
1962
src/api/client.ts
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,65 @@
|
||||
export const customFetch: typeof fetch = (input, init) => {
|
||||
return fetch(input, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
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[];
|
||||
headers?: () => Promise<Record<string, string>> | Record<string, 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";
|
||||
|
||||
const extraHeaders = opts.headers ? await opts.headers() : {};
|
||||
|
||||
let req: Request;
|
||||
if (method === "GET") {
|
||||
req = new Request(
|
||||
url +
|
||||
"?" +
|
||||
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload }),
|
||||
{
|
||||
headers: extraHeaders
|
||||
}
|
||||
);
|
||||
} else {
|
||||
req = new Request(url, {
|
||||
method: "POST",
|
||||
body: payload,
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
...extraHeaders
|
||||
},
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,125 +1,70 @@
|
||||
import { tryGetContext } from 'hono/context-storage';
|
||||
import { TinyRpcClientAdapter, TinyRpcError } from "@hiogawa/tiny-rpc";
|
||||
import { Result } from "@hiogawa/utils";
|
||||
import { tryGetContext } from "hono/context-storage";
|
||||
|
||||
// export const baseAPIURL = 'https://api.pipic.fun';
|
||||
export const baseAPIURL = 'http://localhost:8080';
|
||||
const GET_PAYLOAD_PARAM = "payload";
|
||||
|
||||
type RequestOptions = RequestInit | { raw: Request };
|
||||
|
||||
const isRequest = (input: URL | RequestInfo): input is Request =>
|
||||
typeof Request !== 'undefined' && input instanceof Request;
|
||||
|
||||
const isRequestLikeOptions = (options: RequestOptions): options is { raw: Request } =>
|
||||
typeof options === 'object' && options !== null && 'raw' in options && options.raw instanceof Request;
|
||||
|
||||
const resolveInputUrl = (input: URL | RequestInfo, currentRequestUrl: string) => {
|
||||
if (input instanceof URL) return new URL(input.toString());
|
||||
if (isRequest(input)) return new URL(input.url);
|
||||
|
||||
const baseUrl = new URL(currentRequestUrl);
|
||||
baseUrl.pathname = '/';
|
||||
baseUrl.search = '';
|
||||
baseUrl.hash = '';
|
||||
|
||||
return new URL(input, baseUrl);
|
||||
};
|
||||
|
||||
const resolveApiUrl = (input: URL | RequestInfo, currentRequestUrl: string) => {
|
||||
const inputUrl = resolveInputUrl(input, currentRequestUrl);
|
||||
const apiUrl = new URL(baseAPIURL);
|
||||
|
||||
apiUrl.pathname = inputUrl.pathname.replace(/^\/?r(?=\/|$)/, '') || '/';
|
||||
apiUrl.search = inputUrl.search;
|
||||
apiUrl.hash = inputUrl.hash;
|
||||
|
||||
return apiUrl;
|
||||
};
|
||||
|
||||
const getOptionHeaders = (options: RequestOptions) =>
|
||||
isRequestLikeOptions(options) ? options.raw.headers : options.headers;
|
||||
|
||||
const getOptionMethod = (options: RequestOptions) =>
|
||||
isRequestLikeOptions(options) ? options.raw.method : options.method;
|
||||
|
||||
const getOptionBody = (options: RequestOptions) =>
|
||||
isRequestLikeOptions(options) ? options.raw.body : options.body;
|
||||
|
||||
const getOptionSignal = (options: RequestOptions) =>
|
||||
isRequestLikeOptions(options) ? options.raw.signal : options.signal;
|
||||
|
||||
const getOptionCredentials = (options: RequestOptions) =>
|
||||
isRequestLikeOptions(options) ? undefined : options.credentials;
|
||||
|
||||
const mergeHeaders = (input: URL | RequestInfo, options: RequestOptions) => {
|
||||
const c = tryGetContext<any>();
|
||||
const mergedHeaders = new Headers(c?.req.raw.headers ?? undefined);
|
||||
const inputHeaders = isRequest(input) ? input.headers : undefined;
|
||||
const optionHeaders = getOptionHeaders(options);
|
||||
|
||||
new Headers(inputHeaders).forEach((value, key) => {
|
||||
mergedHeaders.set(key, value);
|
||||
export function httpClientAdapter(opts: {
|
||||
url: string;
|
||||
pathsForGET?: string[];
|
||||
headers?: () => Promise<Record<string, string>> | Record<string, 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",
|
||||
});
|
||||
|
||||
new Headers(optionHeaders).forEach((value, key) => {
|
||||
mergedHeaders.set(key, value);
|
||||
});
|
||||
|
||||
mergedHeaders.delete('host');
|
||||
mergedHeaders.delete('connection');
|
||||
mergedHeaders.delete('content-length');
|
||||
mergedHeaders.delete('transfer-encoding');
|
||||
|
||||
return mergedHeaders;
|
||||
};
|
||||
|
||||
const resolveMethod = (input: URL | RequestInfo, options: RequestOptions) => {
|
||||
const method = getOptionMethod(options);
|
||||
if (method) return method;
|
||||
if (isRequest(input)) return input.method;
|
||||
return 'GET';
|
||||
};
|
||||
|
||||
const resolveBody = (input: URL | RequestInfo, options: RequestOptions, method: string) => {
|
||||
if (method === 'GET' || method === 'HEAD') return undefined;
|
||||
|
||||
const body = getOptionBody(options);
|
||||
if (typeof body !== 'undefined') return body;
|
||||
if (isRequest(input)) return input.body;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const customFetch = (input: URL | RequestInfo, options: RequestOptions = {}) => {
|
||||
}
|
||||
let res: Response;
|
||||
if (import.meta.env.SSR) {
|
||||
const c = tryGetContext<any>();
|
||||
if (!c) {
|
||||
throw new Error('Hono context not found in SSR');
|
||||
throw new Error("Hono context not found in SSR");
|
||||
}
|
||||
|
||||
const apiUrl = resolveApiUrl(input, c.req.url);
|
||||
const method = resolveMethod(input, options);
|
||||
const body = resolveBody(input, options, method.toUpperCase());
|
||||
const requestOptions: RequestInit & { duplex?: 'half' } = {
|
||||
...(isRequestLikeOptions(options) ? {} : options),
|
||||
method,
|
||||
headers: mergeHeaders(input, options),
|
||||
body,
|
||||
credentials: getOptionCredentials(options) ?? 'include',
|
||||
signal: getOptionSignal(options) ?? (isRequest(input) ? input.signal : undefined),
|
||||
};
|
||||
|
||||
if (body) {
|
||||
requestOptions.duplex = 'half';
|
||||
}
|
||||
|
||||
return fetch(apiUrl, requestOptions).then((response) => {
|
||||
const setCookies = typeof response.headers.getSetCookie === 'function'
|
||||
? response.headers.getSetCookie()
|
||||
: response.headers.get('set-cookie')
|
||||
? [response.headers.get('set-cookie')!]
|
||||
: [];
|
||||
|
||||
for (const cookie of setCookies) {
|
||||
c.header('Set-Cookie', cookie, { append: true });
|
||||
}
|
||||
|
||||
return response;
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
19
src/api/rpcclient.ts
Normal file
19
src/api/rpcclient.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
proxyTinyRpc,
|
||||
TinyRpcClientAdapter,
|
||||
TinyRpcError,
|
||||
} from "@hiogawa/tiny-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" : "";
|
||||
import { type RpcRoutes } from "@/server/routes/rpc";
|
||||
|
||||
export const client = proxyTinyRpc<RpcRoutes>({
|
||||
adapter: httpClientAdapter({
|
||||
url: url + endpoint,
|
||||
pathsForGET: [],
|
||||
}),
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { apiProxyMiddleware } from './server/middlewares/apiProxy';
|
||||
import { setupMiddlewares } from './server/middlewares/setup';
|
||||
import { registerDisplayRoutes } from './server/routes/display';
|
||||
import { registerManifestRoutes } from './server/routes/manifest';
|
||||
@@ -8,15 +7,15 @@ import { registerMergeRoutes } from './server/routes/merge';
|
||||
import { registerSSRRoutes } from './server/routes/ssr';
|
||||
import { registerWellKnownRoutes } from './server/routes/wellKnown';
|
||||
import { setupServices } from './server/services/grpcClient';
|
||||
import { registerRpcRoutes } from './server/routes/rpc';
|
||||
const app = new Hono();
|
||||
|
||||
// Global middlewares
|
||||
setupMiddlewares(app);
|
||||
setupServices(app);
|
||||
// API proxy middleware (handles /r/*)
|
||||
app.use(apiProxyMiddleware);
|
||||
// Routes
|
||||
registerWellKnownRoutes(app);
|
||||
registerRpcRoutes(app);
|
||||
registerMergeRoutes(app);
|
||||
registerDisplayRoutes(app);
|
||||
registerManifestRoutes(app);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { customFetch } from '@httpClientAdapter';
|
||||
import type { Context, Next } from 'hono';
|
||||
|
||||
export async function apiProxyMiddleware(c: Context, next: Next) {
|
||||
const path = c.req.path;
|
||||
|
||||
if (path !== '/r' && !path.startsWith('/r/')) {
|
||||
return await next();
|
||||
}
|
||||
return customFetch(c.req.url, c.req)
|
||||
}
|
||||
59
src/server/middlewares/authenticate.ts
Normal file
59
src/server/middlewares/authenticate.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { MiddlewareHandler } from "hono";
|
||||
import { getCookie } from "hono/cookie";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { getUserServiceClient } from "../services/grpcClient";
|
||||
import { generateAndSetTokens } from "../utils";
|
||||
export const authenticate: MiddlewareHandler = async (ctx, next) => {
|
||||
let payload
|
||||
let cause
|
||||
const jwtProvider = ctx.get("jwtProvider");
|
||||
const token = getCookie(ctx, "access_token");
|
||||
if (!token) {
|
||||
throw new HTTPException(401, {
|
||||
message: 'Unauthorized',
|
||||
})
|
||||
}
|
||||
try {
|
||||
payload = await jwtProvider.parseToken(token);
|
||||
} catch (e) {
|
||||
cause = e
|
||||
}
|
||||
if (!payload) {
|
||||
const refreshToken = getCookie(ctx, "refresh_token");
|
||||
if (!refreshToken) {
|
||||
throw new HTTPException(401, {
|
||||
message: 'Unauthorized',
|
||||
})
|
||||
}
|
||||
const refreshPayload = await jwtProvider.parseToken(refreshToken);
|
||||
if (!refreshPayload) {
|
||||
throw new HTTPException(401)
|
||||
}
|
||||
const redis = ctx.get("redis");
|
||||
const refreshUuid = refreshPayload["refresh_uuid"];
|
||||
const userId = await redis.get("refresh_uuid:" + refreshUuid);
|
||||
if (!userId) {
|
||||
throw new HTTPException(401)
|
||||
}
|
||||
const userData = await getUserServiceClient().getUser({ id: userId });
|
||||
// userData.user
|
||||
redis.del("refresh_uuid:" + refreshUuid);
|
||||
const tokenPair = await generateAndSetTokens(ctx, userData.user!);
|
||||
payload = {
|
||||
user_id: userId,
|
||||
email: userData.user!.email,
|
||||
role: userData.user!.role,
|
||||
token_id: tokenPair.accessUUID,
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload.user_id || !payload.role) {
|
||||
throw new HTTPException(401, {
|
||||
message: 'Unauthorized',
|
||||
})
|
||||
}
|
||||
ctx.set('jwtPayload', payload)
|
||||
ctx.set('userId', payload.user_id)
|
||||
ctx.set("role", payload.role)
|
||||
await next();
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { contextStorage } from "hono/context-storage";
|
||||
import { cors } from "hono/cors";
|
||||
import { languageDetector } from "hono/language";
|
||||
import isMobile from "is-mobile";
|
||||
import { JwtProvider } from "../utils/token";
|
||||
type AppFetch = (
|
||||
input: string | Request | URL,
|
||||
requestInit?: RequestInit
|
||||
@@ -14,6 +15,9 @@ declare module "hono" {
|
||||
fetch: AppFetch;
|
||||
isMobile: boolean;
|
||||
redis: RedisClient;
|
||||
jwtProvider: JwtProvider;
|
||||
userId: string;
|
||||
role: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +33,11 @@ export function setupMiddlewares(app: Hono) {
|
||||
lookupFromHeaderKey: "accept-language",
|
||||
order: ["cookie", "header"],
|
||||
}),
|
||||
contextStorage()
|
||||
contextStorage(),
|
||||
async (c, next) => {
|
||||
c.set("jwtProvider", JwtProvider.newJWTProvider("your-secret-key"));
|
||||
await next();
|
||||
}
|
||||
);
|
||||
|
||||
app.use(cors(), async (c, next) => {
|
||||
@@ -44,7 +52,7 @@ export function setupMiddlewares(app: Hono) {
|
||||
await next();
|
||||
});
|
||||
app.use(async (c, next) => {
|
||||
client
|
||||
return await client
|
||||
.connect()
|
||||
.then(() => {
|
||||
c.set("redis", client);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
import z from 'zod';
|
||||
import { getUserServiceClient } from '../services/grpcClient';
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { Hono } from "hono";
|
||||
import z, { success } from "zod";
|
||||
import { getUserServiceClient } from "../services/grpcClient";
|
||||
import { generateAndSetTokens } from "../utils";
|
||||
import { getCookie, setCookie } from "hono/cookie";
|
||||
import { jwt } from "hono/jwt";
|
||||
import { authenticate } from "../middlewares/authenticate";
|
||||
// authGroup := r.Group("/auth")
|
||||
// {
|
||||
// authGroup.POST("/login", authHandler.Login)
|
||||
@@ -12,68 +16,124 @@ import { getUserServiceClient } from '../services/grpcClient';
|
||||
// authGroup.GET("/google/callback", authHandler.GoogleCallback)
|
||||
// }
|
||||
const authRoute = new Hono();
|
||||
authRoute.post('/login', zValidator('json', z.object({ email: z.email(), password: z.string().min(6) })), async (c) => {
|
||||
const data = c.req.valid("json")
|
||||
const user = await getUserServiceClient().getUserByEmail(data);
|
||||
authRoute.post(
|
||||
"/login",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
email: z.email("Invalid email or password"),
|
||||
password: z.string().min(6, "Invalid email or password"),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { email, password } = c.req.valid("json");
|
||||
const user = await getUserServiceClient().getUserByEmail({ email });
|
||||
if (!user) {
|
||||
return c.json({ error: 'Invalid email or password' }, 401);
|
||||
return c.json({ error: "Invalid email or password" }, 401);
|
||||
}
|
||||
if (user.password !== data.password) {
|
||||
return c.json({ error: 'Invalid email or password' }, 401);
|
||||
const isMatch = Bun.password.verifySync(password, user.user!.password!, "bcrypt");
|
||||
if (!isMatch) {
|
||||
return c.json({ error: "Invalid email or password" }, 401);
|
||||
}
|
||||
// const user = await getUserServiceClient().getUserByEmail({ email }, (err, response) => {
|
||||
// if (err) {
|
||||
// console.error("Error fetching user by email", err);
|
||||
// return null;
|
||||
// }
|
||||
// return response;
|
||||
// });
|
||||
// return c.json({ message: 'Login endpoint' });
|
||||
});
|
||||
authRoute.post('/register', zValidator('json', z.object({ email: z.email(), password: z.string().min(6) })), async (c) => {
|
||||
return c.json({ message: 'Register endpoint' });
|
||||
});
|
||||
authRoute.post('/forgot-password', zValidator('json', z.object({ email: z.email() })), async (c) => {
|
||||
return c.json({ message: 'Forgot Password endpoint' });
|
||||
});
|
||||
authRoute.post('/reset-password', zValidator('json', z.object({ token: z.string(), password: z.string().min(6) })), async (c) => {
|
||||
return c.json({ message: 'Reset Password endpoint' });
|
||||
});
|
||||
authRoute.get('/google/login', zValidator('query', z.object({ redirect_uri: z.string().url() })), async (c) => {
|
||||
return c.json({ message: 'Google Login endpoint' });
|
||||
});
|
||||
authRoute.get('/google/callback', zValidator('query', z.object({ code: z.string(), state: z.string() })), async (c) => {
|
||||
return c.json({ message: 'Google Callback endpoint' });
|
||||
});
|
||||
await generateAndSetTokens(c, user.user!);
|
||||
return c.json({ message: "Login successful" });
|
||||
},
|
||||
);
|
||||
authRoute.post(
|
||||
"/register",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
email: z.email("Invalid email"),
|
||||
username: z.string().min(3, "Username must be at least 3 characters"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters"),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { email, username, password } = c.req.valid("json");
|
||||
const user = await getUserServiceClient().createUser({
|
||||
email,
|
||||
username,
|
||||
password: Bun.password.hashSync(password, { algorithm: "bcrypt", cost: 12 }),
|
||||
});
|
||||
delete user.user?.password;
|
||||
return c.json({ success: true, user: user.user });
|
||||
},
|
||||
);
|
||||
authRoute.post(
|
||||
"/forgot-password",
|
||||
zValidator("json", z.object({ email: z.email("Invalid email") })),
|
||||
async (c) => {
|
||||
const { email } = c.req.valid("json");
|
||||
const user = await getUserServiceClient().getUserByEmail({ email });
|
||||
if (user) {
|
||||
const redis = c.get("redis");
|
||||
const resetToken = crypto.randomUUID();
|
||||
redis?.set("reset_pw:" + resetToken, user.user?.id || "", "EX", 15 * 60);
|
||||
//TODO: Connect to email service to send reset link with token
|
||||
}
|
||||
return c.json({ message: "If email exists, a reset link has been sent" });
|
||||
},
|
||||
);
|
||||
authRoute.post(
|
||||
"/reset-password",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({ token: z.string(), password: z.string().min(6) }),
|
||||
),
|
||||
async (c) => {
|
||||
const { token, password } = c.req.valid("json");
|
||||
const redis = c.get("redis");
|
||||
const userId = await redis?.get("reset_pw:" + token);
|
||||
if (userId) {
|
||||
// Update the user's password in the database
|
||||
await getUserServiceClient().updateUserPassword({
|
||||
id: userId,
|
||||
newPassword: Bun.password.hashSync(password, {
|
||||
algorithm: "bcrypt",
|
||||
cost: 12,
|
||||
}),
|
||||
});
|
||||
}
|
||||
return c.json({ message: "Reset Password endpoint" });
|
||||
},
|
||||
);
|
||||
authRoute.get(
|
||||
"/google/login",
|
||||
zValidator("query", z.object({ redirect_uri: z.string().url() })),
|
||||
async (c) => {
|
||||
//TODO: Implement Google OAuth flow
|
||||
return c.json({ message: "Google Login endpoint" });
|
||||
},
|
||||
);
|
||||
authRoute.get(
|
||||
"/google/callback",
|
||||
zValidator("query", z.object({ code: z.string(), state: z.string() })),
|
||||
async (c) => {
|
||||
//TODO: Implement Google OAuth flow
|
||||
return c.json({ message: "Google Callback endpoint" });
|
||||
},
|
||||
);
|
||||
export function registerAuthRoutes(app: Hono) {
|
||||
|
||||
// app.post('/merge', async (c) => {
|
||||
// try {
|
||||
// const body = await c.req.json();
|
||||
// const { filename, chunks, size } = body;
|
||||
|
||||
// if (!filename || !Array.isArray(chunks) || chunks.length === 0) {
|
||||
// return c.json({ error: 'invalid payload' }, 400);
|
||||
// }
|
||||
|
||||
// const hostError = validateChunkUrls(chunks);
|
||||
// if (hostError) return c.json({ error: hostError }, 400);
|
||||
|
||||
// const manifest = createManifest(filename, chunks, size);
|
||||
// await saveManifest(manifest);
|
||||
|
||||
// return c.json({
|
||||
// status: 'ok',
|
||||
// id: manifest.id,
|
||||
// filename: manifest.filename,
|
||||
// total_parts: manifest.total_parts,
|
||||
// size: manifest.size,
|
||||
// playback_url: `/display/${manifest.id}`,
|
||||
// play_url: `/play/index/${manifest.id}`,
|
||||
// manifest_url: `/manifest/${manifest.id}`,
|
||||
// });
|
||||
// } catch (e: any) {
|
||||
// return c.json({ error: e?.message ?? String(e) }, 500);
|
||||
// }
|
||||
// });
|
||||
app.route("/auth", authRoute);
|
||||
app.get(
|
||||
"/logout",
|
||||
authenticate,
|
||||
async (c) => {
|
||||
const payload = c.get("jwtPayload") as any;
|
||||
const redis = c.get("redis");
|
||||
redis.del("refresh_uuid:" + payload["refresh_uuid"]);
|
||||
setCookie(c, "access_token", "", {
|
||||
expires: new Date(0),
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
});
|
||||
setCookie(c, "refresh_token", "", {
|
||||
expires: new Date(0),
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
});
|
||||
return c.json({ message: "Logged out successfully" });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
42
src/server/routes/rpc/index.ts
Normal file
42
src/server/routes/rpc/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { authenticate } from "@/server/middlewares/authenticate";
|
||||
import {
|
||||
exposeTinyRpc,
|
||||
httpServerAdapter,
|
||||
validateFn,
|
||||
} from "@hiogawa/tiny-rpc";
|
||||
import { tinyassert } from "@hiogawa/utils";
|
||||
import { Hono } from "hono";
|
||||
import { getContext } from "hono/context-storage";
|
||||
import { jwt } from "hono/jwt";
|
||||
import { z } from "zod";
|
||||
import { meMethods } from "./me";
|
||||
|
||||
const routes = {
|
||||
// define as a bare function
|
||||
checkId: (id: string) => {
|
||||
const context = getContext();
|
||||
console.log(context.req.raw.headers);
|
||||
return id === "good";
|
||||
},
|
||||
...meMethods
|
||||
};
|
||||
export type RpcRoutes = typeof routes;
|
||||
export const endpoint = "/rpc";
|
||||
export const pathsForGET: (keyof typeof routes)[] = ["checkId"];
|
||||
|
||||
export function registerRpcRoutes(app: Hono) {
|
||||
app.use(endpoint, authenticate, async (c, next) => {
|
||||
if (c.req.path !== endpoint && !c.req.path.startsWith(endpoint + "/")) {
|
||||
return await next();
|
||||
}
|
||||
const handler = exposeTinyRpc({
|
||||
routes,
|
||||
adapter: httpServerAdapter({ endpoint }),
|
||||
});
|
||||
const res = await handler({ request: c.req.raw });
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
}
|
||||
56
src/server/routes/rpc/me.ts
Normal file
56
src/server/routes/rpc/me.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { validateFn } from "@hiogawa/tiny-rpc";
|
||||
import { getContext } from "hono/context-storage";
|
||||
import z from "zod";
|
||||
|
||||
export const meMethods = {
|
||||
getMe: async () => {
|
||||
const context = getContext();
|
||||
const userServiceClient = context.get("userServiceClient");
|
||||
const user = await userServiceClient.getUser({ id: context.get("userId") });
|
||||
const userPreferences = await userServiceClient.getPreferences({ userId: context.get("userId") });
|
||||
delete user.user?.password
|
||||
return {
|
||||
...user.user,
|
||||
...userPreferences.preferences
|
||||
};
|
||||
},
|
||||
updateMe: validateFn(z.object({
|
||||
username: z.string().min(3).optional(),
|
||||
avatar: z.url().optional(),
|
||||
role: z.string().optional(),
|
||||
planId: z.string().optional(),
|
||||
}))(
|
||||
async (data) => {
|
||||
const context = getContext();
|
||||
const user = await context.get("userServiceClient").updateUser({
|
||||
id: context.get("userId"),
|
||||
username: data.username,
|
||||
avatar: data.avatar,
|
||||
role: data.role,
|
||||
planId: data.planId,
|
||||
});
|
||||
delete user.user?.password
|
||||
return user.user;
|
||||
}
|
||||
),
|
||||
ChangePassword: validateFn(z.object({
|
||||
oldPassword: z.string().min(6),
|
||||
newPassword: z.string().min(6),
|
||||
}))(
|
||||
async (data) => {
|
||||
const context = getContext();
|
||||
const user = await context.get("userServiceClient").getUser({ id: context.get("userId") });
|
||||
if (!user.user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const isMatch = Bun.password.verifySync(data.oldPassword, user.user!.password!, "bcrypt");
|
||||
if (!isMatch) {
|
||||
throw new Error("Invalid password");
|
||||
}
|
||||
await context.get("userServiceClient").updateUserPassword({
|
||||
id: context.get("userId"),
|
||||
newPassword: Bun.password.hashSync(data.newPassword, { algorithm: "bcrypt", cost: 12 }),
|
||||
});
|
||||
}
|
||||
)
|
||||
};
|
||||
@@ -4,4 +4,17 @@ export function registerWellKnownRoutes(app: Hono) {
|
||||
app.get("/.well-known/*", (c) => {
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
app.get("/health/live", (c) => {
|
||||
return c.json({ status: "alive" });
|
||||
});
|
||||
app.get("/health/ready", (c) => {
|
||||
return c.json({ status: "ready" });
|
||||
});
|
||||
app.get("/health/detailed", (c) => {
|
||||
return c.json({ status: "detailed", uptime: process.uptime() });
|
||||
});
|
||||
// app.get("/metrics", (c) => {
|
||||
// //TODO: Implement metrics endpoint/ Prometheus integration
|
||||
// return c.json({ message: "Metrics endpoint" });
|
||||
// });
|
||||
}
|
||||
|
||||
@@ -55,6 +55,6 @@ export const getUserServiceClient = () => {
|
||||
export const setupServices = (app: Hono) => {
|
||||
app.use("*", async (c, next) => {
|
||||
c.set("userServiceClient", promisifyClient(new UserServiceClient(grpcAddress(), getCredentials())));
|
||||
await next();
|
||||
return await next();
|
||||
});
|
||||
}
|
||||
@@ -1,43 +1,78 @@
|
||||
import { ClientUnaryCall, ServiceError, status } from "@grpc/grpc-js";
|
||||
type UnaryCallback<TRes> = (
|
||||
error: ServiceError | null,
|
||||
response: TRes
|
||||
) => void;
|
||||
|
||||
type UnaryLike<TReq, TRes> = (
|
||||
req: TReq,
|
||||
callback: UnaryCallback<TRes>
|
||||
) => ClientUnaryCall;
|
||||
// 1. Định nghĩa lại UnaryCallback để bắt được kiểu TRes chính xác hơn
|
||||
type UnaryCallback<TRes> = (error: ServiceError | null, response: TRes) => void;
|
||||
|
||||
type RequestOf<T> = T extends (
|
||||
req: infer TReq,
|
||||
callback: UnaryCallback<any>
|
||||
) => ClientUnaryCall
|
||||
? TReq
|
||||
: never;
|
||||
// 2. Ép TypeScript tìm đúng Overload có Callback
|
||||
// Chúng ta sử dụng tham số thứ 2 của hàm (index 1) để lấy TRes
|
||||
type ResponseOf<T> = T extends {
|
||||
(req: any, callback: UnaryCallback<infer TRes>): ClientUnaryCall;
|
||||
(req: any, metadata: any, callback: UnaryCallback<infer TRes>): ClientUnaryCall;
|
||||
(req: any, metadata: any, options: any, callback: UnaryCallback<infer TRes>): ClientUnaryCall;
|
||||
} ? TRes : any;
|
||||
|
||||
type ResponseOf<T> = T extends (
|
||||
req: any,
|
||||
callback: UnaryCallback<infer TRes>
|
||||
) => ClientUnaryCall
|
||||
? TRes
|
||||
: never;
|
||||
type RequestOf<T> = T extends {
|
||||
(req: infer TReq, callback: UnaryCallback<any>): ClientUnaryCall;
|
||||
(req: infer TReq, metadata: any, callback: UnaryCallback<any>): ClientUnaryCall;
|
||||
} ? TReq : any;
|
||||
|
||||
/**
|
||||
* Lấy ra overload đúng dạng (req, callback) => ClientUnaryCall
|
||||
*/
|
||||
type ExtractUnaryOverload<T> = Extract<T, UnaryLike<any, any>>;
|
||||
// 3. Filter để chỉ lấy các Method thực sự là Unary
|
||||
type UnaryKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => ClientUnaryCall ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type PromisifiedClient<TClient> = {
|
||||
[K in keyof TClient as ExtractUnaryOverload<TClient[K]> extends never
|
||||
? never
|
||||
: K]: (
|
||||
req: RequestOf<ExtractUnaryOverload<TClient[K]>>
|
||||
) => Promise<ResponseOf<ExtractUnaryOverload<TClient[K]>>>;
|
||||
[K in UnaryKeys<TClient>]: (
|
||||
req: RequestOf<TClient[K]>
|
||||
) => Promise<ResponseOf<TClient[K]>>;
|
||||
};
|
||||
|
||||
// ... Các hàm normalizeGrpcError giữ nguyên ...
|
||||
|
||||
const grpcCodeToHttpStatus = (code?: number) => {
|
||||
export function promisifyClient<TClient extends object>(
|
||||
client: TClient
|
||||
): PromisifiedClient<TClient> {
|
||||
const result = {} as any;
|
||||
|
||||
// Thay vì quét Prototype, ta quét các key thực tế hiện có trên instance của client
|
||||
// gRPC dynamic clients thường định nghĩa method trực tiếp hoặc qua proxy
|
||||
const allKeys = new Set([
|
||||
...Object.getOwnPropertyNames(client),
|
||||
...Object.getOwnPropertyNames(Object.getPrototypeOf(client))
|
||||
]);
|
||||
|
||||
allKeys.forEach((key) => {
|
||||
if (key === "constructor") return;
|
||||
|
||||
const originalMethod = (client as any)[key];
|
||||
|
||||
// Chỉ xử lý nếu nó là function và không phải là các hàm tiện ích của gRPC (bắt đầu bằng $)
|
||||
if (typeof originalMethod === "function" && !key.startsWith('$')) {
|
||||
|
||||
result[key] = (req: any) =>
|
||||
new Promise((resolve, reject) => {
|
||||
// QUAN TRỌNG: Sử dụng .bind(client) hoặc .call(client, ...)
|
||||
// để tránh lỗi "No implementation found" do mất context 'this'
|
||||
originalMethod.call(
|
||||
client,
|
||||
req,
|
||||
(error: ServiceError | null, response: any) => {
|
||||
if (error) {
|
||||
reject(normalizeGrpcError(error));
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function grpcCodeToHttpStatus (code?: number) {
|
||||
switch (code) {
|
||||
case status.INVALID_ARGUMENT:
|
||||
return 400;
|
||||
@@ -51,7 +86,7 @@ const grpcCodeToHttpStatus = (code?: number) => {
|
||||
return 500;
|
||||
}
|
||||
};
|
||||
const normalizeGrpcError = (error: ServiceError) => {
|
||||
function normalizeGrpcError(error: ServiceError) {
|
||||
const normalized = new Error(error.details || error.message) as Error & {
|
||||
status?: number;
|
||||
code?: number;
|
||||
@@ -77,33 +112,3 @@ const normalizeGrpcError = (error: ServiceError) => {
|
||||
|
||||
return normalized;
|
||||
};
|
||||
export function promisifyClient<TClient extends object>(
|
||||
client: TClient
|
||||
): PromisifiedClient<TClient> {
|
||||
const proto = Object.getPrototypeOf(client);
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const key of Object.getOwnPropertyNames(proto)) {
|
||||
if (key === "constructor") continue;
|
||||
|
||||
const value = (client as Record<string, unknown>)[key];
|
||||
if (typeof value !== "function") continue;
|
||||
|
||||
result[key] = (req: unknown) =>
|
||||
new Promise((resolve, reject) => {
|
||||
(value as Function).call(
|
||||
client,
|
||||
req,
|
||||
(error: ServiceError | null, response: unknown) => {
|
||||
if (error) {
|
||||
reject(normalizeGrpcError(error));
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return result as PromisifiedClient<TClient>;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { RedisClient } from "bun";
|
||||
import { Context } from "hono";
|
||||
import { tryGetContext } from "hono/context-storage";
|
||||
import {
|
||||
setCookie
|
||||
} from 'hono/cookie';
|
||||
import { JWTProvider } from "./token";
|
||||
import { User } from "./proto/v1/user";
|
||||
export const redisClient = (): RedisClient => {
|
||||
const context = tryGetContext<any>();
|
||||
const redis = context?.get("redis") as RedisClient | undefined;
|
||||
@@ -13,26 +14,26 @@ export const redisClient = (): RedisClient => {
|
||||
return redis;
|
||||
};
|
||||
|
||||
export async function generateAndSetTokens(userID: string, email: string, role: string) {
|
||||
const redis = redisClient();
|
||||
const context = tryGetContext<any>();
|
||||
await JWTProvider("your-secret-key").generateTokenPair(userID, email, role).then((td) => {
|
||||
redis.set("refresh_uuid:" + td.refreshUUID, userID, "EX", td.rtExpires - Math.floor(Date.now() / 1000));
|
||||
if (context) {
|
||||
setCookie(context, "access_token", td.accessToken, {
|
||||
export async function generateAndSetTokens(c: Context, userData: User) {
|
||||
const redis = c.get("redis");
|
||||
const jwtProvider = c.get("jwtProvider");
|
||||
return await jwtProvider.generateTokenPair(userData.id!, userData.email!, userData.role!).then((td) => {
|
||||
redis.set("refresh_uuid:" + td.refreshUUID, userData.id!, "EX", td.rtExpires - Math.floor(Date.now() / 1000));
|
||||
setCookie(c, "access_token", td.accessToken, {
|
||||
expires: new Date(td.atExpires * 1000),
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
path: "/",
|
||||
});
|
||||
setCookie(context, "refresh_token", td.refreshToken, {
|
||||
setCookie(c, "refresh_token", td.refreshToken, {
|
||||
expires: new Date(td.rtExpires * 1000),
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
return td;
|
||||
}).catch((e) => {
|
||||
console.error("Error generating tokens", e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
86
src/server/utils/proto/google/protobuf/empty.ts
Normal file
86
src/server/utils/proto/google/protobuf/empty.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.11.4
|
||||
// protoc unknown
|
||||
// source: google/protobuf/empty.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
|
||||
export const protobufPackage = "google.protobuf";
|
||||
|
||||
/**
|
||||
* A generic empty message that you can re-use to avoid defining duplicated
|
||||
* empty messages in your APIs. A typical example is to use it as the request
|
||||
* or the response type of an API method. For instance:
|
||||
*
|
||||
* service Foo {
|
||||
* rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
* }
|
||||
*/
|
||||
export interface Empty {
|
||||
}
|
||||
|
||||
function createBaseEmpty(): Empty {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const Empty: MessageFns<Empty> = {
|
||||
encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Empty {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseEmpty();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(_: any): Empty {
|
||||
return {};
|
||||
},
|
||||
|
||||
toJSON(_: Empty): unknown {
|
||||
const obj: any = {};
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Empty>, I>>(base?: I): Empty {
|
||||
return Empty.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Empty>, I>>(_: I): Empty {
|
||||
const message = createBaseEmpty();
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -18,10 +18,17 @@ import {
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import { Empty } from "../google/protobuf/empty";
|
||||
import { Timestamp } from "../google/protobuf/timestamp";
|
||||
|
||||
export const protobufPackage = "stream.User.v1";
|
||||
|
||||
/** ─── User Messages ─────────────────────────────────────────────────────────── */
|
||||
export interface UpdateUserPasswordRequest {
|
||||
id?: string | undefined;
|
||||
newPassword?: string | undefined;
|
||||
}
|
||||
|
||||
export interface GetUserRequest {
|
||||
id?: string | undefined;
|
||||
}
|
||||
@@ -128,6 +135,86 @@ export interface Preferences {
|
||||
encrytionM3u8?: boolean | undefined;
|
||||
}
|
||||
|
||||
function createBaseUpdateUserPasswordRequest(): UpdateUserPasswordRequest {
|
||||
return { id: "", newPassword: "" };
|
||||
}
|
||||
|
||||
export const UpdateUserPasswordRequest: MessageFns<UpdateUserPasswordRequest> = {
|
||||
encode(message: UpdateUserPasswordRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.newPassword !== undefined && message.newPassword !== "") {
|
||||
writer.uint32(18).string(message.newPassword);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): UpdateUserPasswordRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseUpdateUserPasswordRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.newPassword = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): UpdateUserPasswordRequest {
|
||||
return {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
newPassword: isSet(object.newPassword)
|
||||
? globalThis.String(object.newPassword)
|
||||
: isSet(object.new_password)
|
||||
? globalThis.String(object.new_password)
|
||||
: "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: UpdateUserPasswordRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
if (message.newPassword !== undefined && message.newPassword !== "") {
|
||||
obj.newPassword = message.newPassword;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<UpdateUserPasswordRequest>, I>>(base?: I): UpdateUserPasswordRequest {
|
||||
return UpdateUserPasswordRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<UpdateUserPasswordRequest>, I>>(object: I): UpdateUserPasswordRequest {
|
||||
const message = createBaseUpdateUserPasswordRequest();
|
||||
message.id = object.id ?? "";
|
||||
message.newPassword = object.newPassword ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseGetUserRequest(): GetUserRequest {
|
||||
return { id: "" };
|
||||
}
|
||||
@@ -1849,6 +1936,16 @@ export const UserServiceService = {
|
||||
responseSerialize: (value: DeleteUserResponse): Buffer => Buffer.from(DeleteUserResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): DeleteUserResponse => DeleteUserResponse.decode(value),
|
||||
},
|
||||
updateUserPassword: {
|
||||
path: "/stream.User.v1.UserService/UpdateUserPassword",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: UpdateUserPasswordRequest): Buffer =>
|
||||
Buffer.from(UpdateUserPasswordRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): UpdateUserPasswordRequest => UpdateUserPasswordRequest.decode(value),
|
||||
responseSerialize: (value: Empty): Buffer => Buffer.from(Empty.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): Empty => Empty.decode(value),
|
||||
},
|
||||
/** Preferences */
|
||||
getPreferences: {
|
||||
path: "/stream.User.v1.UserService/GetPreferences",
|
||||
@@ -1882,6 +1979,7 @@ export interface UserServiceServer extends UntypedServiceImplementation {
|
||||
createUser: handleUnaryCall<CreateUserRequest, CreateUserResponse>;
|
||||
updateUser: handleUnaryCall<UpdateUserRequest, UpdateUserResponse>;
|
||||
deleteUser: handleUnaryCall<DeleteUserRequest, DeleteUserResponse>;
|
||||
updateUserPassword: handleUnaryCall<UpdateUserPasswordRequest, Empty>;
|
||||
/** Preferences */
|
||||
getPreferences: handleUnaryCall<GetPreferencesRequest, GetPreferencesResponse>;
|
||||
upsertPreferences: handleUnaryCall<UpsertPreferencesRequest, UpsertPreferencesResponse>;
|
||||
@@ -1979,6 +2077,21 @@ export interface UserServiceClient extends Client {
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: DeleteUserResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
updateUserPassword(
|
||||
request: UpdateUserPasswordRequest,
|
||||
callback: (error: ServiceError | null, response: Empty) => void,
|
||||
): ClientUnaryCall;
|
||||
updateUserPassword(
|
||||
request: UpdateUserPasswordRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: Empty) => void,
|
||||
): ClientUnaryCall;
|
||||
updateUserPassword(
|
||||
request: UpdateUserPasswordRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: Empty) => void,
|
||||
): ClientUnaryCall;
|
||||
/** Preferences */
|
||||
getPreferences(
|
||||
request: GetPreferencesRequest,
|
||||
|
||||
@@ -9,9 +9,7 @@ export interface Provider {
|
||||
role: string
|
||||
): Promise<TokenPair>
|
||||
|
||||
parseToken(token: string): Promise<JWTPayload>
|
||||
|
||||
parseMapToken(token: string): Promise<Record<string, any>>
|
||||
parseToken(token: string): Promise<JwtClaims>
|
||||
}
|
||||
|
||||
export interface TokenPair {
|
||||
@@ -30,7 +28,7 @@ export interface Claims {
|
||||
tokenID: string
|
||||
}
|
||||
|
||||
interface JwtClaims {
|
||||
export interface JwtClaims extends JWTPayload {
|
||||
user_id: string
|
||||
email: string
|
||||
role: string
|
||||
@@ -42,7 +40,7 @@ interface JwtClaims {
|
||||
export class JwtProvider implements Provider {
|
||||
constructor(private secret: string) {}
|
||||
|
||||
static newJWTProvider(secret: string): Provider {
|
||||
static newJWTProvider(secret: string): JwtProvider {
|
||||
return new JwtProvider(secret)
|
||||
}
|
||||
|
||||
@@ -86,26 +84,12 @@ export class JwtProvider implements Provider {
|
||||
return td
|
||||
}
|
||||
|
||||
async parseToken(token: string): Promise<JWTPayload> {
|
||||
const payload = (await verify(token, this.secret, "HS256"))
|
||||
async parseToken(token: string): Promise<JwtClaims> {
|
||||
const payload = await verify(token, this.secret, "HS256") as JwtClaims
|
||||
|
||||
if (!payload) {
|
||||
throw new Error("invalid token")
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
async parseMapToken(token: string): Promise<JWTPayload> {
|
||||
const payload = await verify(token, this.secret, "HS256")
|
||||
|
||||
if (!payload) {
|
||||
throw new Error("invalid token")
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
}
|
||||
|
||||
export function JWTProvider(secret: string): Provider {
|
||||
return new JwtProvider(secret)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { client, type AuthUserPayload, type ResponseResponse } from '@/api/client';
|
||||
// import { client, type AuthUserPayload, type ResponseResponse } from '@/api/client';
|
||||
import { client } from '@/api/rpcclient';
|
||||
import { TinyMqttClient } from '@/lib/liteMqtt';
|
||||
import type { User } from '@/server/utils/proto/v1/user';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, watch } from 'vue';
|
||||
@@ -11,23 +13,8 @@ type ProfileUpdatePayload = {
|
||||
locale?: string;
|
||||
};
|
||||
|
||||
type AuthResponseBody = ResponseResponse & {
|
||||
data?: AuthUserPayload | { user?: AuthUserPayload };
|
||||
};
|
||||
|
||||
const mqttBrokerUrl = 'wss://mqtt-dashboard.com:8884/mqtt';
|
||||
|
||||
const extractUser = (body?: AuthResponseBody | null): AuthUserPayload | null => {
|
||||
const data = body?.data;
|
||||
|
||||
if (!data) return null;
|
||||
if (typeof data === 'object' && 'user' in data && data.user) {
|
||||
return data.user;
|
||||
}
|
||||
|
||||
return data as AuthUserPayload;
|
||||
};
|
||||
|
||||
const getGoogleLoginPath = () => {
|
||||
const basePath = client.baseUrl.startsWith('/') ? client.baseUrl : `/${client.baseUrl}`;
|
||||
return `${basePath}/auth/google/login`;
|
||||
@@ -71,12 +58,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
});
|
||||
watch(() => user.value?.language, (lng) => i18next.changeLanguage(lng))
|
||||
async function fetchMe() {
|
||||
const response = await client.me.getMe({ baseUrl: '/r' });
|
||||
|
||||
const nextUser = extractUser(response.data as AuthResponseBody);
|
||||
user.value = nextUser;
|
||||
i18next.changeLanguage(nextUser?.language)
|
||||
return nextUser;
|
||||
const response = await client.getMe();
|
||||
user.value = await client.getMe();
|
||||
i18next.changeLanguage(response?.language || 'en');
|
||||
return response;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
|
||||
7
src/type.d.ts
vendored
7
src/type.d.ts
vendored
@@ -8,5 +8,10 @@ declare module '*.vue' {
|
||||
}
|
||||
|
||||
declare module "@httpClientAdapter" {
|
||||
export const customFetch: typeof fetch;
|
||||
import { TinyRpcClientAdapter } from "@hiogawa/tiny-rpc";
|
||||
export function httpClientAdapter(opts: {
|
||||
url: string;
|
||||
pathsForGET?: string[];
|
||||
headers?: () => Promise<{ Authorization?: undefined; } | { Authorization: string; }>
|
||||
}): TinyRpcClientAdapter;
|
||||
}
|
||||
Reference in New Issue
Block a user