feat: implement JWT token provider with access and refresh token generation

This commit is contained in:
2026-03-11 19:01:23 +07:00
parent dc06412f79
commit 9276603a70
19 changed files with 3749 additions and 42 deletions

View File

@@ -7,12 +7,12 @@ import { registerManifestRoutes } from './server/routes/manifest';
import { registerMergeRoutes } from './server/routes/merge';
import { registerSSRRoutes } from './server/routes/ssr';
import { registerWellKnownRoutes } from './server/routes/wellKnown';
import { setupServices } from './server/services/grpcClient';
const app = new Hono();
// Global middlewares
setupMiddlewares(app);
setupServices(app);
// API proxy middleware (handles /r/*)
app.use(apiProxyMiddleware);
// Routes

View File

@@ -1,17 +1,36 @@
import { contextStorage } from 'hono/context-storage';
import { cors } from 'hono/cors';
import isMobile from 'is-mobile';
import type { Hono } from 'hono';
import { languageDetector } from 'hono/language';
import { RedisClient } from "bun";
import type { Hono } from "hono";
import { contextStorage } from "hono/context-storage";
import { cors } from "hono/cors";
import { languageDetector } from "hono/language";
import isMobile from "is-mobile";
type AppFetch = (
input: string | Request | URL,
requestInit?: RequestInit
) => Response | Promise<Response>;
declare module "hono" {
interface ContextVariableMap {
fetch: AppFetch;
isMobile: boolean;
redis: RedisClient;
}
}
const client = new RedisClient("redis://:pass123@47.84.62.226:6379/3");
export function setupMiddlewares(app: Hono) {
app.use('*', languageDetector({
supportedLanguages: ['vi', 'en'],
fallbackLanguage: 'en',
lookupCookie: 'i18next',
lookupFromHeaderKey: 'accept-language',
order: ['cookie', 'header'],
}) ,contextStorage());
app.use(
"*",
languageDetector({
supportedLanguages: ["vi", "en"],
fallbackLanguage: "en",
lookupCookie: "i18next",
lookupFromHeaderKey: "accept-language",
order: ["cookie", "header"],
}),
contextStorage()
);
app.use(cors(), async (c, next) => {
c.set("fetch", app.request.bind(app));
@@ -24,4 +43,15 @@ export function setupMiddlewares(app: Hono) {
c.set("isMobile", isMobile({ ua }));
await next();
});
app.use(async (c, next) => {
client
.connect()
.then(() => {
c.set("redis", client);
return next();
})
.catch((e) => {
console.error("Failed to connect to Redis", e);
});
});
}

79
src/server/routes/auth.ts Normal file
View File

@@ -0,0 +1,79 @@
import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
import z from 'zod';
import { getUserServiceClient } from '../services/grpcClient';
// authGroup := r.Group("/auth")
// {
// authGroup.POST("/login", authHandler.Login)
// authGroup.POST("/register", authHandler.Register)
// authGroup.POST("/forgot-password", authHandler.ForgotPassword)
// authGroup.POST("/reset-password", authHandler.ResetPassword)
// authGroup.GET("/google/login", authHandler.LoginGoogle)
// 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);
if (!user) {
return c.json({ error: 'Invalid email or password' }, 401);
}
if (user.password !== data.password) {
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' });
});
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);
// }
// });
}

View File

@@ -0,0 +1,60 @@
import { ChannelCredentials, credentials } from "@grpc/grpc-js";
import { tryGetContext } from "hono/context-storage";
import { Hono } from "node_modules/hono/dist/types/hono";
import { PromisifiedClient, promisifyClient } from "../utils/grpcHelper";
import { UserServiceClient } from "../utils/proto/v1/user";
declare module "hono" {
interface ContextVariableMap {
userServiceClient: PromisifiedClient<UserServiceClient>;
}
}
const DEFAULT_GRPC_ADDRESS = '127.0.0.1:9000';
const grpcAddress = () => process.env.STREAM_API_GRPC_ADDR || DEFAULT_GRPC_ADDRESS;
let sharedCredentials: ChannelCredentials | undefined;
const getCredentials = () => {
if (!sharedCredentials) {
sharedCredentials = credentials.createInsecure();
}
return sharedCredentials;
};
export const getUserServiceClient = () => {
const context = tryGetContext();
if (context) {
return context.get("userServiceClient");
}
throw new Error("No context available to get UserServiceClient");
};
// (method) UserServiceClient.getUserByEmail(request: GetUserByEmailRequest, callback: (error: ServiceError | null, response: GetUserResponse) => void): ClientUnaryCall (+2 overloads)
// const unaryCall = <TResponse>(
// executor: (
// metadata: Metadata,
// options: Partial<CallOptions>,
// callback: (error: ServiceError | null, response: TResponse) => void,
// ) => { metadata?: Metadata; trailer?: Metadata },
// ): Promise<TResponse> => {
// // const { metadata } = createMetadataFromContext();
// return new Promise<TResponse>((resolve, reject) => {
// executor({
// deadline: Date.now() + 10_000,
// }, (error, response) => {
// if (error) {
// reject(normalizeGrpcError(error));
// return;
// }
// // appendSetCookiesToResponse(call.metadata?.get('set-cookie') ?? []);
// resolve(response);
// });
// });
// };
export const setupServices = (app: Hono) => {
app.use("*", async (c, next) => {
c.set("userServiceClient", promisifyClient(new UserServiceClient(grpcAddress(), getCredentials())));
await next();
});
}

View File

@@ -0,0 +1,109 @@
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;
type RequestOf<T> = T extends (
req: infer TReq,
callback: UnaryCallback<any>
) => ClientUnaryCall
? TReq
: never;
type ResponseOf<T> = T extends (
req: any,
callback: UnaryCallback<infer TRes>
) => ClientUnaryCall
? TRes
: never;
/**
* Lấy ra overload đúng dạng (req, callback) => ClientUnaryCall
*/
type ExtractUnaryOverload<T> = Extract<T, UnaryLike<any, any>>;
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]>>>;
};
const grpcCodeToHttpStatus = (code?: number) => {
switch (code) {
case status.INVALID_ARGUMENT:
return 400;
case status.UNAUTHENTICATED:
return 401;
case status.PERMISSION_DENIED:
return 403;
case status.NOT_FOUND:
return 404;
default:
return 500;
}
};
const normalizeGrpcError = (error: ServiceError) => {
const normalized = new Error(error.details || error.message) as Error & {
status?: number;
code?: number;
body?: { code?: number; message?: string; data?: unknown };
};
normalized.code = error.code;
normalized.status = grpcCodeToHttpStatus(error.code);
const trailerBody = error.metadata?.get('x-error-body')?.[0];
if (typeof trailerBody === 'string' && trailerBody) {
try {
normalized.body = JSON.parse(trailerBody) as { code?: number; message?: string; data?: unknown };
if (normalized.body?.message) {
normalized.message = normalized.body.message;
}
if (typeof normalized.body?.code === 'number') {
normalized.status = normalized.body.code;
}
} catch {
// ignore malformed structured error payloads
}
}
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>;
}

38
src/server/utils/index.ts Normal file
View File

@@ -0,0 +1,38 @@
import { RedisClient } from "bun";
import { tryGetContext } from "hono/context-storage";
import {
setCookie
} from 'hono/cookie';
import { JWTProvider } from "./token";
export const redisClient = (): RedisClient => {
const context = tryGetContext<any>();
const redis = context?.get("redis") as RedisClient | undefined;
if (!redis) {
throw new Error("Redis client not found in context");
}
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, {
expires: new Date(td.atExpires * 1000),
httpOnly: true,
secure: false,
path: "/",
});
setCookie(context, "refresh_token", td.refreshToken, {
expires: new Date(td.rtExpires * 1000),
httpOnly: true,
secure: false,
path: "/",
});
}
}).catch((e) => {
console.error("Error generating tokens", e);
});
}

View File

@@ -0,0 +1,231 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.4
// protoc unknown
// source: google/protobuf/timestamp.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
export const protobufPackage = "google.protobuf";
/**
* A Timestamp represents a point in time independent of any time zone or local
* calendar, encoded as a count of seconds and fractions of seconds at
* nanosecond resolution. The count is relative to an epoch at UTC midnight on
* January 1, 1970, in the proleptic Gregorian calendar which extends the
* Gregorian calendar backwards to year one.
*
* All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
* second table is needed for interpretation, using a [24-hour linear
* smear](https://developers.google.com/time/smear).
*
* The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
* restricting to that range, we ensure that we can convert to and from [RFC
* 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
*
* # Examples
*
* Example 1: Compute Timestamp from POSIX `time()`.
*
* Timestamp timestamp;
* timestamp.set_seconds(time(NULL));
* timestamp.set_nanos(0);
*
* Example 2: Compute Timestamp from POSIX `gettimeofday()`.
*
* struct timeval tv;
* gettimeofday(&tv, NULL);
*
* Timestamp timestamp;
* timestamp.set_seconds(tv.tv_sec);
* timestamp.set_nanos(tv.tv_usec * 1000);
*
* Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
*
* FILETIME ft;
* GetSystemTimeAsFileTime(&ft);
* UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
*
* // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
* // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
* Timestamp timestamp;
* timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
* timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
*
* Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
*
* long millis = System.currentTimeMillis();
*
* Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
* .setNanos((int) ((millis % 1000) * 1000000)).build();
*
* Example 5: Compute Timestamp from Java `Instant.now()`.
*
* Instant now = Instant.now();
*
* Timestamp timestamp =
* Timestamp.newBuilder().setSeconds(now.getEpochSecond())
* .setNanos(now.getNano()).build();
*
* Example 6: Compute Timestamp from current time in Python.
*
* timestamp = Timestamp()
* timestamp.GetCurrentTime()
*
* # JSON Mapping
*
* In JSON format, the Timestamp type is encoded as a string in the
* [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
* format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
* where {year} is always expressed using four digits while {month}, {day},
* {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
* seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
* are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
* is required. A proto3 JSON serializer should always use UTC (as indicated by
* "Z") when printing the Timestamp type and a proto3 JSON parser should be
* able to accept both UTC and other timezones (as indicated by an offset).
*
* For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
* 01:30 UTC on January 15, 2017.
*
* In JavaScript, one can convert a Date object to this format using the
* standard
* [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
* method. In Python, a standard `datetime.datetime` object can be converted
* to this format using
* [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
* the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
* the Joda Time's [`ISODateTimeFormat.dateTime()`](
* http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()
* ) to obtain a formatter capable of generating timestamps in this format.
*/
export interface Timestamp {
/**
* Represents seconds of UTC time since Unix epoch
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
* 9999-12-31T23:59:59Z inclusive.
*/
seconds?:
| number
| undefined;
/**
* Non-negative fractions of a second at nanosecond resolution. Negative
* second values with fractions must still have non-negative nanos values
* that count forward in time. Must be from 0 to 999,999,999
* inclusive.
*/
nanos?: number | undefined;
}
function createBaseTimestamp(): Timestamp {
return { seconds: 0, nanos: 0 };
}
export const Timestamp: MessageFns<Timestamp> = {
encode(message: Timestamp, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.seconds !== undefined && message.seconds !== 0) {
writer.uint32(8).int64(message.seconds);
}
if (message.nanos !== undefined && message.nanos !== 0) {
writer.uint32(16).int32(message.nanos);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): Timestamp {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseTimestamp();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break;
}
message.seconds = longToNumber(reader.int64());
continue;
}
case 2: {
if (tag !== 16) {
break;
}
message.nanos = reader.int32();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): Timestamp {
return {
seconds: isSet(object.seconds) ? globalThis.Number(object.seconds) : 0,
nanos: isSet(object.nanos) ? globalThis.Number(object.nanos) : 0,
};
},
toJSON(message: Timestamp): unknown {
const obj: any = {};
if (message.seconds !== undefined && message.seconds !== 0) {
obj.seconds = Math.round(message.seconds);
}
if (message.nanos !== undefined && message.nanos !== 0) {
obj.nanos = Math.round(message.nanos);
}
return obj;
},
create<I extends Exact<DeepPartial<Timestamp>, I>>(base?: I): Timestamp {
return Timestamp.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<Timestamp>, I>>(object: I): Timestamp {
const message = createBaseTimestamp();
message.seconds = object.seconds ?? 0;
message.nanos = object.nanos ?? 0;
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 };
function longToNumber(int64: { toString(): string }): number {
const num = globalThis.Number(int64.toString());
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
}
return num;
}
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
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;
}

View File

@@ -0,0 +1,719 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.4
// protoc unknown
// source: v1/common.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
import { Timestamp } from "../google/protobuf/timestamp";
export const protobufPackage = "stream.common.v1";
export interface RequestContext {
userId?: string | undefined;
email?: string | undefined;
role?: string | undefined;
requestId?: string | undefined;
source?: string | undefined;
}
export interface PaginationRequest {
page?: number | undefined;
pageSize?: number | undefined;
}
export interface PaginationResponse {
page?: number | undefined;
pageSize?: number | undefined;
total?: number | undefined;
}
export interface Money {
amount?: number | undefined;
currency?: string | undefined;
}
export interface Empty {
}
export interface IdRequest {
id?: string | undefined;
}
export interface DeleteResponse {
message?: string | undefined;
}
export interface TimestampRange {
from?: string | undefined;
to?: string | undefined;
}
function createBaseRequestContext(): RequestContext {
return { userId: "", email: "", role: "", requestId: "", source: "" };
}
export const RequestContext: MessageFns<RequestContext> = {
encode(message: RequestContext, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.userId !== undefined && message.userId !== "") {
writer.uint32(10).string(message.userId);
}
if (message.email !== undefined && message.email !== "") {
writer.uint32(18).string(message.email);
}
if (message.role !== undefined && message.role !== "") {
writer.uint32(26).string(message.role);
}
if (message.requestId !== undefined && message.requestId !== "") {
writer.uint32(34).string(message.requestId);
}
if (message.source !== undefined && message.source !== "") {
writer.uint32(42).string(message.source);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): RequestContext {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseRequestContext();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.userId = reader.string();
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.email = reader.string();
continue;
}
case 3: {
if (tag !== 26) {
break;
}
message.role = reader.string();
continue;
}
case 4: {
if (tag !== 34) {
break;
}
message.requestId = reader.string();
continue;
}
case 5: {
if (tag !== 42) {
break;
}
message.source = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): RequestContext {
return {
userId: isSet(object.userId)
? globalThis.String(object.userId)
: isSet(object.user_id)
? globalThis.String(object.user_id)
: "",
email: isSet(object.email) ? globalThis.String(object.email) : "",
role: isSet(object.role) ? globalThis.String(object.role) : "",
requestId: isSet(object.requestId)
? globalThis.String(object.requestId)
: isSet(object.request_id)
? globalThis.String(object.request_id)
: "",
source: isSet(object.source) ? globalThis.String(object.source) : "",
};
},
toJSON(message: RequestContext): unknown {
const obj: any = {};
if (message.userId !== undefined && message.userId !== "") {
obj.userId = message.userId;
}
if (message.email !== undefined && message.email !== "") {
obj.email = message.email;
}
if (message.role !== undefined && message.role !== "") {
obj.role = message.role;
}
if (message.requestId !== undefined && message.requestId !== "") {
obj.requestId = message.requestId;
}
if (message.source !== undefined && message.source !== "") {
obj.source = message.source;
}
return obj;
},
create<I extends Exact<DeepPartial<RequestContext>, I>>(base?: I): RequestContext {
return RequestContext.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<RequestContext>, I>>(object: I): RequestContext {
const message = createBaseRequestContext();
message.userId = object.userId ?? "";
message.email = object.email ?? "";
message.role = object.role ?? "";
message.requestId = object.requestId ?? "";
message.source = object.source ?? "";
return message;
},
};
function createBasePaginationRequest(): PaginationRequest {
return { page: 0, pageSize: 0 };
}
export const PaginationRequest: MessageFns<PaginationRequest> = {
encode(message: PaginationRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.page !== undefined && message.page !== 0) {
writer.uint32(8).int32(message.page);
}
if (message.pageSize !== undefined && message.pageSize !== 0) {
writer.uint32(16).int32(message.pageSize);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): PaginationRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBasePaginationRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break;
}
message.page = reader.int32();
continue;
}
case 2: {
if (tag !== 16) {
break;
}
message.pageSize = reader.int32();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): PaginationRequest {
return {
page: isSet(object.page) ? globalThis.Number(object.page) : 0,
pageSize: isSet(object.pageSize)
? globalThis.Number(object.pageSize)
: isSet(object.page_size)
? globalThis.Number(object.page_size)
: 0,
};
},
toJSON(message: PaginationRequest): unknown {
const obj: any = {};
if (message.page !== undefined && message.page !== 0) {
obj.page = Math.round(message.page);
}
if (message.pageSize !== undefined && message.pageSize !== 0) {
obj.pageSize = Math.round(message.pageSize);
}
return obj;
},
create<I extends Exact<DeepPartial<PaginationRequest>, I>>(base?: I): PaginationRequest {
return PaginationRequest.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<PaginationRequest>, I>>(object: I): PaginationRequest {
const message = createBasePaginationRequest();
message.page = object.page ?? 0;
message.pageSize = object.pageSize ?? 0;
return message;
},
};
function createBasePaginationResponse(): PaginationResponse {
return { page: 0, pageSize: 0, total: 0 };
}
export const PaginationResponse: MessageFns<PaginationResponse> = {
encode(message: PaginationResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.page !== undefined && message.page !== 0) {
writer.uint32(8).int32(message.page);
}
if (message.pageSize !== undefined && message.pageSize !== 0) {
writer.uint32(16).int32(message.pageSize);
}
if (message.total !== undefined && message.total !== 0) {
writer.uint32(24).int64(message.total);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): PaginationResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBasePaginationResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break;
}
message.page = reader.int32();
continue;
}
case 2: {
if (tag !== 16) {
break;
}
message.pageSize = reader.int32();
continue;
}
case 3: {
if (tag !== 24) {
break;
}
message.total = longToNumber(reader.int64());
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): PaginationResponse {
return {
page: isSet(object.page) ? globalThis.Number(object.page) : 0,
pageSize: isSet(object.pageSize)
? globalThis.Number(object.pageSize)
: isSet(object.page_size)
? globalThis.Number(object.page_size)
: 0,
total: isSet(object.total) ? globalThis.Number(object.total) : 0,
};
},
toJSON(message: PaginationResponse): unknown {
const obj: any = {};
if (message.page !== undefined && message.page !== 0) {
obj.page = Math.round(message.page);
}
if (message.pageSize !== undefined && message.pageSize !== 0) {
obj.pageSize = Math.round(message.pageSize);
}
if (message.total !== undefined && message.total !== 0) {
obj.total = Math.round(message.total);
}
return obj;
},
create<I extends Exact<DeepPartial<PaginationResponse>, I>>(base?: I): PaginationResponse {
return PaginationResponse.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<PaginationResponse>, I>>(object: I): PaginationResponse {
const message = createBasePaginationResponse();
message.page = object.page ?? 0;
message.pageSize = object.pageSize ?? 0;
message.total = object.total ?? 0;
return message;
},
};
function createBaseMoney(): Money {
return { amount: 0, currency: "" };
}
export const Money: MessageFns<Money> = {
encode(message: Money, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.amount !== undefined && message.amount !== 0) {
writer.uint32(9).double(message.amount);
}
if (message.currency !== undefined && message.currency !== "") {
writer.uint32(18).string(message.currency);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): Money {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseMoney();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 9) {
break;
}
message.amount = reader.double();
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.currency = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): Money {
return {
amount: isSet(object.amount) ? globalThis.Number(object.amount) : 0,
currency: isSet(object.currency) ? globalThis.String(object.currency) : "",
};
},
toJSON(message: Money): unknown {
const obj: any = {};
if (message.amount !== undefined && message.amount !== 0) {
obj.amount = message.amount;
}
if (message.currency !== undefined && message.currency !== "") {
obj.currency = message.currency;
}
return obj;
},
create<I extends Exact<DeepPartial<Money>, I>>(base?: I): Money {
return Money.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<Money>, I>>(object: I): Money {
const message = createBaseMoney();
message.amount = object.amount ?? 0;
message.currency = object.currency ?? "";
return message;
},
};
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;
},
};
function createBaseIdRequest(): IdRequest {
return { id: "" };
}
export const IdRequest: MessageFns<IdRequest> = {
encode(message: IdRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.id !== undefined && message.id !== "") {
writer.uint32(10).string(message.id);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): IdRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseIdRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.id = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): IdRequest {
return { id: isSet(object.id) ? globalThis.String(object.id) : "" };
},
toJSON(message: IdRequest): unknown {
const obj: any = {};
if (message.id !== undefined && message.id !== "") {
obj.id = message.id;
}
return obj;
},
create<I extends Exact<DeepPartial<IdRequest>, I>>(base?: I): IdRequest {
return IdRequest.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<IdRequest>, I>>(object: I): IdRequest {
const message = createBaseIdRequest();
message.id = object.id ?? "";
return message;
},
};
function createBaseDeleteResponse(): DeleteResponse {
return { message: "" };
}
export const DeleteResponse: MessageFns<DeleteResponse> = {
encode(message: DeleteResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.message !== undefined && message.message !== "") {
writer.uint32(10).string(message.message);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): DeleteResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.message = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): DeleteResponse {
return { message: isSet(object.message) ? globalThis.String(object.message) : "" };
},
toJSON(message: DeleteResponse): unknown {
const obj: any = {};
if (message.message !== undefined && message.message !== "") {
obj.message = message.message;
}
return obj;
},
create<I extends Exact<DeepPartial<DeleteResponse>, I>>(base?: I): DeleteResponse {
return DeleteResponse.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<DeleteResponse>, I>>(object: I): DeleteResponse {
const message = createBaseDeleteResponse();
message.message = object.message ?? "";
return message;
},
};
function createBaseTimestampRange(): TimestampRange {
return { from: undefined, to: undefined };
}
export const TimestampRange: MessageFns<TimestampRange> = {
encode(message: TimestampRange, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.from !== undefined) {
Timestamp.encode(toTimestamp(message.from), writer.uint32(10).fork()).join();
}
if (message.to !== undefined) {
Timestamp.encode(toTimestamp(message.to), writer.uint32(18).fork()).join();
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): TimestampRange {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseTimestampRange();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.from = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.to = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): TimestampRange {
return {
from: isSet(object.from) ? globalThis.String(object.from) : undefined,
to: isSet(object.to) ? globalThis.String(object.to) : undefined,
};
},
toJSON(message: TimestampRange): unknown {
const obj: any = {};
if (message.from !== undefined) {
obj.from = message.from;
}
if (message.to !== undefined) {
obj.to = message.to;
}
return obj;
},
create<I extends Exact<DeepPartial<TimestampRange>, I>>(base?: I): TimestampRange {
return TimestampRange.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<TimestampRange>, I>>(object: I): TimestampRange {
const message = createBaseTimestampRange();
message.from = object.from ?? undefined;
message.to = object.to ?? undefined;
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 };
function toTimestamp(dateStr: string): Timestamp {
const date = new globalThis.Date(dateStr);
const seconds = Math.trunc(date.getTime() / 1_000);
const nanos = (date.getTime() % 1_000) * 1_000_000;
return { seconds, nanos };
}
function fromTimestamp(t: Timestamp): string {
let millis = (t.seconds || 0) * 1_000;
millis += (t.nanos || 0) / 1_000_000;
return new globalThis.Date(millis).toISOString();
}
function longToNumber(int64: { toString(): string }): number {
const num = globalThis.Number(int64.toString());
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
}
return num;
}
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
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;
}

File diff suppressed because it is too large Load Diff

111
src/server/utils/token.ts Normal file
View File

@@ -0,0 +1,111 @@
import { randomUUID } from "crypto"
import { sign, verify } from "hono/jwt"
import { JWTPayload } from "hono/utils/jwt/types"
export interface Provider {
generateTokenPair(
userID: string,
email: string,
role: string
): Promise<TokenPair>
parseToken(token: string): Promise<JWTPayload>
parseMapToken(token: string): Promise<Record<string, any>>
}
export interface TokenPair {
accessToken: string
refreshToken: string
atExpires: number
rtExpires: number
accessUUID: string
refreshUUID: string
}
export interface Claims {
userID: string
email: string
role: string
tokenID: string
}
interface JwtClaims {
user_id: string
email: string
role: string
token_id: string
iss: string
exp: number
}
export class JwtProvider implements Provider {
constructor(private secret: string) {}
static newJWTProvider(secret: string): Provider {
return new JwtProvider(secret)
}
async generateTokenPair(
userID: string,
email: string,
role: string
): Promise<TokenPair> {
const now = Math.floor(Date.now() / 1000)
const td: TokenPair = {
accessToken: "",
refreshToken: "",
atExpires: now + 15 * 60,
rtExpires: now + 7 * 24 * 60 * 60,
accessUUID: randomUUID(),
refreshUUID: randomUUID(),
}
// ACCESS TOKEN
const accessPayload: JWTPayload = {
user_id: userID,
email,
role,
token_id: td.accessUUID,
iss: "stream.api",
exp: td.atExpires,
}
td.accessToken = await sign(accessPayload, this.secret)
// REFRESH TOKEN
const refreshPayload = {
refresh_uuid: td.refreshUUID,
user_id: userID,
exp: td.rtExpires,
}
td.refreshToken = await sign(refreshPayload, this.secret)
return td
}
async parseToken(token: string): Promise<JWTPayload> {
const payload = (await verify(token, this.secret, "HS256"))
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)
}