BREAKING!: update libraries
This commit is contained in:
parent
4669e1c0d4
commit
84c0f7243e
@ -1,5 +1,6 @@
|
|||||||
import { atom, useAtomValue, setAtomValue } from "../lib/atom.ts";
|
import { atom, useAtomValue, setAtomValue } from "../lib/atom.ts";
|
||||||
import { makeApiUrl } from "../hook/fetcher.ts";
|
import { makeApiUrl } from "../hook/fetcher.ts";
|
||||||
|
import { LoginRequest } from "dbtype/mod.ts";
|
||||||
|
|
||||||
type LoginLocalStorage = {
|
type LoginLocalStorage = {
|
||||||
username: string;
|
username: string;
|
||||||
@ -77,10 +78,7 @@ export const doLogout = async () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const doLogin = async (userLoginInfo: {
|
export const doLogin = async (userLoginInfo: LoginRequest): Promise<string | LoginLocalStorage> => {
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}): Promise<string | LoginLocalStorage> => {
|
|
||||||
const u = makeApiUrl("/api/user/login");
|
const u = makeApiUrl("/api/user/login");
|
||||||
const res = await fetch(u, {
|
const res = await fetch(u, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -99,6 +97,20 @@ export const doLogin = async (userLoginInfo: {
|
|||||||
return b;
|
return b;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const doResetPassword = async (username: string, oldpassword: string, newpassword: string) => {
|
||||||
|
const u = makeApiUrl("/api/user/reset");
|
||||||
|
const res = await fetch(u, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ username, oldpassword, newpassword }),
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const b = await res.json();
|
||||||
|
if (res.status !== 200) {
|
||||||
|
return b.detail as string;
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getInitialValue() {
|
export async function getInitialValue() {
|
||||||
const user = getUserSessions();
|
const user = getUserSessions();
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import type { JSONMap } from './jsonmap';
|
|
||||||
|
|
||||||
export interface DocumentBody {
|
|
||||||
title: string;
|
|
||||||
content_type: string;
|
|
||||||
basepath: string;
|
|
||||||
filename: string;
|
|
||||||
modified_at: number;
|
|
||||||
content_hash: string | null;
|
|
||||||
additional: JSONMap;
|
|
||||||
tags: string[]; // eager loading
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Document extends DocumentBody {
|
|
||||||
readonly id: number;
|
|
||||||
readonly created_at: number;
|
|
||||||
readonly deleted_at: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type QueryListOption = {
|
|
||||||
/**
|
|
||||||
* search word
|
|
||||||
*/
|
|
||||||
word?: string;
|
|
||||||
allow_tag?: string[];
|
|
||||||
/**
|
|
||||||
* limit of list
|
|
||||||
* @default 20
|
|
||||||
*/
|
|
||||||
limit?: number;
|
|
||||||
/**
|
|
||||||
* use offset if true, otherwise
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
use_offset?: boolean;
|
|
||||||
/**
|
|
||||||
* cursor of documents
|
|
||||||
*/
|
|
||||||
cursor?: number;
|
|
||||||
/**
|
|
||||||
* offset of documents
|
|
||||||
*/
|
|
||||||
offset?: number;
|
|
||||||
/**
|
|
||||||
* tag eager loading
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
eager_loading?: boolean;
|
|
||||||
/**
|
|
||||||
* content type
|
|
||||||
*/
|
|
||||||
content_type?: string;
|
|
||||||
};
|
|
2
packages/dbtype/mod.ts
Normal file
2
packages/dbtype/mod.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./src/api.ts";
|
||||||
|
export * as db from "./src/types.ts";
|
@ -2,7 +2,7 @@
|
|||||||
"name": "dbtype",
|
"name": "dbtype",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "mod.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
@ -13,6 +13,11 @@
|
|||||||
"@types/better-sqlite3": "^7.6.9",
|
"@types/better-sqlite3": "^7.6.9",
|
||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.4.3",
|
||||||
"kysely": "^0.27.3",
|
"kysely": "^0.27.3",
|
||||||
"kysely-codegen": "^0.14.1"
|
"kysely-codegen": "^0.14.1",
|
||||||
|
"typescript": "^5.4.3"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
102
packages/dbtype/src/api.ts
Normal file
102
packages/dbtype/src/api.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
export { ZodError } from "zod";
|
||||||
|
|
||||||
|
export const DocumentBodySchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
content_type: z.string(),
|
||||||
|
basepath: z.string(),
|
||||||
|
filename: z.string(),
|
||||||
|
modified_at: z.number(),
|
||||||
|
content_hash: z.string(),
|
||||||
|
additional: z.record(z.unknown()),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
pagenum: z.number().int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DocumentBody = z.infer<typeof DocumentBodySchema>;
|
||||||
|
|
||||||
|
export const DocumentSchema = DocumentBodySchema.extend({
|
||||||
|
id: z.number(),
|
||||||
|
created_at: z.number(),
|
||||||
|
deleted_at: z.number().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Document = z.infer<typeof DocumentSchema>;
|
||||||
|
|
||||||
|
export const TagSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Tag = z.infer<typeof TagSchema>;
|
||||||
|
|
||||||
|
export const TagRelationSchema = z.object({
|
||||||
|
doc_id: z.number(),
|
||||||
|
tag_name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TagRelation = z.infer<typeof TagRelationSchema>;
|
||||||
|
|
||||||
|
export const PermissionSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Permission = z.infer<typeof PermissionSchema>;
|
||||||
|
|
||||||
|
export const UserSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
password_hash: z.string(),
|
||||||
|
password_salt: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type User = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
|
export const SchemaMigrationSchema = z.object({
|
||||||
|
version: z.string().nullable(),
|
||||||
|
dirty: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SchemaMigration = z.infer<typeof SchemaMigrationSchema>;
|
||||||
|
|
||||||
|
const WorkStatusEnum = z.enum(["pending", "done", "error"]);
|
||||||
|
|
||||||
|
export const WorkSchema = z.object({
|
||||||
|
uuid: z.string(),
|
||||||
|
type: z.literal("rehash"),
|
||||||
|
status: WorkStatusEnum,
|
||||||
|
detail: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Work = z.infer<typeof WorkSchema>;
|
||||||
|
|
||||||
|
export const QueryListOptionSchema = z.object({
|
||||||
|
/**
|
||||||
|
* search word
|
||||||
|
*/
|
||||||
|
word: z.string().optional(),
|
||||||
|
allow_tag: z.array(z.string()).default([]),
|
||||||
|
limit: z.number().default(20),
|
||||||
|
use_offset: z.boolean().default(false),
|
||||||
|
cursor: z.number().optional(),
|
||||||
|
offset: z.number().optional(),
|
||||||
|
eager_loading: z.boolean().default(true),
|
||||||
|
content_type: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type QueryListOption = z.infer<typeof QueryListOptionSchema>;
|
||||||
|
|
||||||
|
export const LoginRequestSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoginRequest = z.infer<typeof LoginRequestSchema>;
|
||||||
|
|
||||||
|
export const LoginResetRequestSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
oldpassword: z.string(),
|
||||||
|
newpassword: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoginResetRequest = z.infer<typeof LoginResetRequestSchema>;
|
@ -12,7 +12,7 @@ export interface DocTagRelation {
|
|||||||
export interface Document {
|
export interface Document {
|
||||||
additional: string | null;
|
additional: string | null;
|
||||||
basepath: string;
|
basepath: string;
|
||||||
content_hash: string | null;
|
content_hash: string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
deleted_at: number | null;
|
deleted_at: number | null;
|
||||||
@ -20,6 +20,7 @@ export interface Document {
|
|||||||
id: Generated<number>;
|
id: Generated<number>;
|
||||||
modified_at: number;
|
modified_at: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
pagenum: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Permissions {
|
export interface Permissions {
|
110
packages/dbtype/tsconfig.json
Normal file
110
packages/dbtype/tsconfig.json
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "NodeNext", /* Specify what module code is generated. */
|
||||||
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
"allowImportingTsExtensions": true, /* Allow importing TypeScript files with a '.ts' extension. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
// "outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
"noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,10 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "build/app.js",
|
"main": "build/app.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"compile": "swc src --out-dir compile",
|
"dev": "tsx watch src/app.ts",
|
||||||
"dev": "nodemon -r @swc-node/register --enable-source-maps --exec node app.ts",
|
"start": "tsx src/app.ts"
|
||||||
"start": "node compile/src/app.js"
|
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -14,8 +14,9 @@
|
|||||||
"@zip.js/zip.js": "^2.7.40",
|
"@zip.js/zip.js": "^2.7.40",
|
||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.4.3",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
|
"dbtype": "workspace:dbtype",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jose": "^5.9.3",
|
||||||
"koa": "^2.15.2",
|
"koa": "^2.15.2",
|
||||||
"koa-bodyparser": "^4.4.1",
|
"koa-bodyparser": "^4.4.1",
|
||||||
"koa-compose": "^4.1.0",
|
"koa-compose": "^4.1.0",
|
||||||
@ -25,9 +26,6 @@
|
|||||||
"tiny-async-pool": "^1.3.0"
|
"tiny-async-pool": "^1.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc-node/register": "^1.9.0",
|
|
||||||
"@swc/cli": "^0.3.10",
|
|
||||||
"@swc/core": "^1.4.11",
|
|
||||||
"@types/better-sqlite3": "^7.6.9",
|
"@types/better-sqlite3": "^7.6.9",
|
||||||
"@types/jsonwebtoken": "^8.5.9",
|
"@types/jsonwebtoken": "^8.5.9",
|
||||||
"@types/koa": "^2.15.0",
|
"@types/koa": "^2.15.0",
|
||||||
@ -36,7 +34,8 @@
|
|||||||
"@types/koa-router": "^7.4.8",
|
"@types/koa-router": "^7.4.8",
|
||||||
"@types/node": ">=20.0.0",
|
"@types/node": ">=20.0.0",
|
||||||
"@types/tiny-async-pool": "^1.0.5",
|
"@types/tiny-async-pool": "^1.0.5",
|
||||||
"dbtype": "workspace:^",
|
"nodemon": "^3.1.0",
|
||||||
"nodemon": "^3.1.0"
|
"tsx": "^4.19.1",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { create_server } from "./src/server";
|
import { create_server } from "./server.ts";
|
||||||
|
|
||||||
create_server().then((server) => {
|
create_server().then((server) => {
|
||||||
server.start_server();
|
server.start_server();
|
@ -1,22 +0,0 @@
|
|||||||
import type { Knex as k } from "knex";
|
|
||||||
|
|
||||||
export namespace Knex {
|
|
||||||
export const config: {
|
|
||||||
development: k.Config;
|
|
||||||
production: k.Config;
|
|
||||||
} = {
|
|
||||||
development: {
|
|
||||||
client: "sqlite3",
|
|
||||||
connection: {
|
|
||||||
filename: "./devdb.sqlite3",
|
|
||||||
},
|
|
||||||
debug: true,
|
|
||||||
},
|
|
||||||
production: {
|
|
||||||
client: "sqlite3",
|
|
||||||
connection: {
|
|
||||||
filename: "./db.sqlite3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import { extname } from "node:path";
|
import { extname } from "node:path";
|
||||||
import type { DocumentBody } from "dbtype/api";
|
import type { DocumentBody } from "dbtype";
|
||||||
import { readZip } from "../util/zipwrap";
|
import { readZip } from "../util/zipwrap.ts";
|
||||||
import { type ContentConstructOption, createDefaultClass, registerContentReferrer } from "./file";
|
import { type ContentConstructOption, createDefaultClass, registerContentReferrer } from "./file.ts";
|
||||||
import { TextWriter } from "@zip.js/zip.js";
|
import { TextWriter } from "@zip.js/zip.js";
|
||||||
|
|
||||||
type ComicType = "doujinshi" | "artist cg" | "manga" | "western";
|
type ComicType = "doujinshi" | "artist cg" | "manga" | "western";
|
||||||
@ -63,6 +63,7 @@ export class ComicReferrer extends createDefaultClass("comic") {
|
|||||||
additional: {
|
additional: {
|
||||||
page: this.pagenum,
|
page: this.pagenum,
|
||||||
},
|
},
|
||||||
|
pagenum: this.pagenum,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { promises, type Stats } from "node:fs";
|
import { promises, type Stats } from "node:fs";
|
||||||
import path, { extname } from "node:path";
|
import path, { extname } from "node:path";
|
||||||
import type { DocumentBody } from "dbtype/api";
|
import type { DocumentBody } from "dbtype";
|
||||||
/**
|
/**
|
||||||
* content file or directory referrer
|
* content file or directory referrer
|
||||||
*/
|
*/
|
||||||
@ -11,6 +11,7 @@ export interface ContentFile {
|
|||||||
readonly path: string;
|
readonly path: string;
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContentConstructOption = {
|
export type ContentConstructOption = {
|
||||||
hash: string;
|
hash: string;
|
||||||
};
|
};
|
||||||
@ -50,6 +51,7 @@ export const createDefaultClass = (type: string): ContentFileConstructor => {
|
|||||||
tags: [],
|
tags: [],
|
||||||
content_hash: await this.getHash(),
|
content_hash: await this.getHash(),
|
||||||
modified_at: await this.getMtime(),
|
modified_at: await this.getMtime(),
|
||||||
|
pagenum: 0,
|
||||||
} as DocumentBody;
|
} as DocumentBody;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
import "./comic";
|
import "./comic";
|
||||||
import "./video";
|
import "./video";
|
||||||
export { ContentFile, createContentFile } from "./file";
|
export { createContentFile } from "./file.ts";
|
||||||
|
export type { ContentFile } from "./file.ts";
|
@ -1,5 +1,5 @@
|
|||||||
import { registerContentReferrer } from "./file";
|
import { registerContentReferrer } from "./file.ts";
|
||||||
import { createDefaultClass } from "./file";
|
import { createDefaultClass } from "./file.ts";
|
||||||
|
|
||||||
export class VideoReferrer extends createDefaultClass("video") {
|
export class VideoReferrer extends createDefaultClass("video") {
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { getKysely } from "./kysely";
|
import { getKysely } from "./kysely.ts";
|
||||||
import { jsonArrayFrom } from "kysely/helpers/sqlite";
|
import { jsonArrayFrom } from "kysely/helpers/sqlite";
|
||||||
import type { DocumentAccessor } from "../model/doc";
|
import type { DocumentAccessor } from "../model/doc.ts";
|
||||||
import type {
|
import type {
|
||||||
Document,
|
Document,
|
||||||
QueryListOption,
|
QueryListOption,
|
||||||
|
db,
|
||||||
DocumentBody
|
DocumentBody
|
||||||
} from "dbtype/api";
|
} from "dbtype";
|
||||||
import type { NotNull } from "kysely";
|
import type { NotNull } from "kysely";
|
||||||
import { MyParseJSONResultsPlugin } from "./plugin";
|
import { MyParseJSONResultsPlugin } from "./plugin.ts";
|
||||||
|
|
||||||
|
type DBDocument = db.Document;
|
||||||
|
|
||||||
export type DBTagContentRelation = {
|
export type DBTagContentRelation = {
|
||||||
doc_id: number;
|
doc_id: number;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from "kysely";
|
import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from "kysely";
|
||||||
import SqliteDatabase from "better-sqlite3";
|
import SqliteDatabase from "better-sqlite3";
|
||||||
import type { DB } from "dbtype/types";
|
import type { db } from "dbtype";
|
||||||
|
|
||||||
|
type DB = db.DB;
|
||||||
|
|
||||||
export function createSqliteDialect() {
|
export function createSqliteDialect() {
|
||||||
const url = process.env.DATABASE_URL;
|
const url = process.env.DATABASE_URL;
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export * from "./doc";
|
export * from "./doc.ts";
|
||||||
export * from "./tag";
|
export * from "./tag.ts";
|
||||||
export * from "./user";
|
export * from "./user.ts";
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { getKysely } from "./kysely";
|
import { getKysely } from "./kysely.ts";
|
||||||
import { jsonArrayFrom } from "kysely/helpers/sqlite";
|
import type { Tag, TagAccessor, TagCount } from "../model/tag.ts";
|
||||||
import type { Tag, TagAccessor, TagCount } from "../model/tag";
|
|
||||||
import type { DBTagContentRelation } from "./doc";
|
|
||||||
|
|
||||||
class SqliteTagAccessor implements TagAccessor {
|
class SqliteTagAccessor implements TagAccessor {
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getKysely } from "./kysely";
|
import { getKysely } from "./kysely.ts";
|
||||||
import { type IUser, Password, type UserAccessor, type UserCreateInput } from "../model/user";
|
import { type IUser, Password, type UserAccessor, type UserCreateInput } from "../model/user.ts";
|
||||||
|
|
||||||
class SqliteUser implements IUser {
|
class SqliteUser implements IUser {
|
||||||
readonly username: string;
|
readonly username: string;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { basename, dirname, join as pathjoin } from "node:path";
|
import { basename, dirname } from "node:path";
|
||||||
import { ContentFile, createContentFile } from "../content/mod";
|
import { createContentFile } from "../content/mod.ts";
|
||||||
import type { Document, DocumentAccessor } from "../model/mod";
|
import type { Document, DocumentAccessor } from "../model/mod.ts";
|
||||||
import { ContentList } from "./content_list";
|
import { ContentList } from "./content_list.ts";
|
||||||
import type { IDiffWatcher } from "./watcher";
|
import type { IDiffWatcher } from "./watcher.ts";
|
||||||
|
|
||||||
// refactoring needed.
|
// refactoring needed.
|
||||||
export class ContentDiffHandler {
|
export class ContentDiffHandler {
|
||||||
@ -22,6 +22,11 @@ export class ContentDiffHandler {
|
|||||||
async setup() {
|
async setup() {
|
||||||
const deleted = await this.doc_cntr.findDeleted(this.content_type);
|
const deleted = await this.doc_cntr.findDeleted(this.content_type);
|
||||||
for (const it of deleted) {
|
for (const it of deleted) {
|
||||||
|
if (it.deleted_at === null) {
|
||||||
|
// it should not be happened. but when it happens, ignore it.
|
||||||
|
console.error("It should not happen");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
this.tombstone.set(it.content_hash, it);
|
this.tombstone.set(it.content_hash, it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,7 +75,7 @@ export class ContentDiffHandler {
|
|||||||
id: dbc[0].id,
|
id: dbc[0].id,
|
||||||
deleted_at: Date.now(),
|
deleted_at: Date.now(),
|
||||||
});
|
});
|
||||||
this.tombstone.set(dbc[0].content_hash, dbc[0]);
|
this.tombstone.set(content_hash, dbc[0]);
|
||||||
}
|
}
|
||||||
private async OnCreated(cpath: string) {
|
private async OnCreated(cpath: string) {
|
||||||
const basepath = dirname(cpath);
|
const basepath = dirname(cpath);
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export * from "./diff";
|
export * from "./diff.ts";
|
||||||
export * from "./router";
|
export * from "./router.ts";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ConfigManager } from "../../util/configRW";
|
import { ConfigManager } from "../../util/configRW.ts";
|
||||||
import ComicSchema from "./ComicConfig.schema.json";
|
import ComicSchema from "./ComicConfig.schema.json";
|
||||||
export interface ComicConfig {
|
export interface ComicConfig {
|
||||||
watch: string[];
|
watch: string[];
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ComicConfig } from "./ComicConfig";
|
import { ComicConfig } from "./ComicConfig.ts";
|
||||||
import { WatcherCompositer } from "./compositer";
|
import { WatcherCompositer } from "./compositer.ts";
|
||||||
import { RecursiveWatcher } from "./recursive_watcher";
|
import { RecursiveWatcher } from "./recursive_watcher.ts";
|
||||||
import { WatcherFilter } from "./watcher_filter";
|
import { WatcherFilter } from "./watcher_filter.ts";
|
||||||
|
|
||||||
const createComicWatcherBase = (path: string) => {
|
const createComicWatcherBase = (path: string) => {
|
||||||
return new WatcherFilter(new RecursiveWatcher(path), (x) => x.endsWith(".zip"));
|
return new WatcherFilter(new RecursiveWatcher(path), (x) => x.endsWith(".zip"));
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import event from "node:events";
|
import event from "node:events";
|
||||||
import { type FSWatcher, promises, watch } from "node:fs";
|
import { type FSWatcher, promises, watch } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { DocumentAccessor } from "../../model/doc";
|
import type { DocumentAccessor } from "../../model/doc.ts";
|
||||||
import type { DiffWatcherEvent, IDiffWatcher } from "../watcher";
|
import type { DiffWatcherEvent, IDiffWatcher } from "../watcher.ts";
|
||||||
import { setupHelp } from "./util";
|
import { setupHelp } from "./util.ts";
|
||||||
|
|
||||||
const { readdir } = promises;
|
const { readdir } = promises;
|
||||||
|
|
||||||
@ -20,17 +20,20 @@ export class CommonDiffWatcher extends event.EventEmitter implements IDiffWatche
|
|||||||
constructor(path: string) {
|
constructor(path: string) {
|
||||||
super();
|
super();
|
||||||
this._path = path;
|
this._path = path;
|
||||||
this._watcher = watch(this._path, { persistent: true, recursive: false }, async (eventType, filename) => {
|
this._watcher = watch(this._path, { persistent: true, recursive: false },
|
||||||
if (eventType === "rename") {
|
async (eventType, filename) => {
|
||||||
const cur = await readdir(this._path);
|
// if filename is null, it means the path is not exist
|
||||||
// add
|
if (filename === null) return;
|
||||||
if (cur.includes(filename)) {
|
if (eventType === "rename") {
|
||||||
this.emit("create", join(this.path, filename));
|
const cur = await readdir(this._path);
|
||||||
} else {
|
// add
|
||||||
this.emit("delete", join(this.path, filename));
|
if (cur.includes(filename)) {
|
||||||
|
this.emit("create", join(this.path, filename));
|
||||||
|
} else {
|
||||||
|
this.emit("delete", join(this.path, filename));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
async setup(cntr: DocumentAccessor): Promise<void> {
|
async setup(cntr: DocumentAccessor): Promise<void> {
|
||||||
await setupHelp(this, this.path, cntr);
|
await setupHelp(this, this.path, cntr);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import type { DocumentAccessor } from "../../model/doc";
|
import type { DocumentAccessor } from "../../model/doc.ts";
|
||||||
import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher";
|
import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher.ts";
|
||||||
|
|
||||||
export class WatcherCompositer extends EventEmitter implements IDiffWatcher {
|
export class WatcherCompositer extends EventEmitter implements IDiffWatcher {
|
||||||
refWatchers: IDiffWatcher[];
|
refWatchers: IDiffWatcher[];
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { type FSWatcher, watch } from "chokidar";
|
import { type FSWatcher, watch } from "chokidar";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import type { DocumentAccessor } from "../../model/doc";
|
import type { DocumentAccessor } from "../../model/doc.ts";
|
||||||
import type { DiffWatcherEvent, IDiffWatcher } from "../watcher";
|
import type { DiffWatcherEvent, IDiffWatcher } from "../watcher.ts";
|
||||||
import { setupRecursive } from "./util";
|
import { setupRecursive } from "./util.ts";
|
||||||
|
|
||||||
type RecursiveWatcherOption = {
|
type RecursiveWatcherOption = {
|
||||||
/** @default true */
|
/** @default true */
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { promises } from "node:fs";
|
import { promises } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
const { readdir } = promises;
|
const { readdir } = promises;
|
||||||
import type { DocumentAccessor } from "../../model/doc";
|
import type { DocumentAccessor } from "../../model/doc.ts";
|
||||||
import type { IDiffWatcher } from "../watcher";
|
import type { IDiffWatcher } from "../watcher.ts";
|
||||||
|
|
||||||
function setupCommon(watcher: IDiffWatcher, basepath: string, initial_filenames: string[], cur: string[]) {
|
function setupCommon(watcher: IDiffWatcher, basepath: string, initial_filenames: string[], cur: string[]) {
|
||||||
// Todo : reduce O(nm) to O(n+m) using hash map.
|
// Todo : reduce O(nm) to O(n+m) using hash map.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import type { DocumentAccessor } from "../../model/doc";
|
import type { DocumentAccessor } from "../../model/doc.ts";
|
||||||
import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher";
|
import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher.ts";
|
||||||
|
|
||||||
export class WatcherFilter extends EventEmitter implements IDiffWatcher {
|
export class WatcherFilter extends EventEmitter implements IDiffWatcher {
|
||||||
refWatcher: IDiffWatcher;
|
refWatcher: IDiffWatcher;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { sign, TokenExpiredError, verify } from "jsonwebtoken";
|
import { SignJWT, jwtVerify, errors } from "jose";
|
||||||
import type Koa from "koa";
|
import type Koa from "koa";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import type { IUser, UserAccessor } from "./model/mod";
|
import type { IUser, UserAccessor } from "./model/mod.ts";
|
||||||
import { sendError } from "./route/error_handler";
|
import { sendError } from "./route/error_handler.ts";
|
||||||
import { get_setting } from "./SettingConfig";
|
import { get_setting } from "./SettingConfig.ts";
|
||||||
|
import { LoginRequestSchema, LoginResetRequestSchema } from "dbtype";
|
||||||
|
|
||||||
type PayloadInfo = {
|
type PayloadInfo = {
|
||||||
username: string;
|
username: string;
|
||||||
@ -18,40 +19,50 @@ const isUserState = (obj: object | string): obj is PayloadInfo => {
|
|||||||
if (typeof obj === "string") return false;
|
if (typeof obj === "string") return false;
|
||||||
return "username" in obj && "permission" in obj && Array.isArray((obj as { permission: unknown }).permission);
|
return "username" in obj && "permission" in obj && Array.isArray((obj as { permission: unknown }).permission);
|
||||||
};
|
};
|
||||||
|
|
||||||
type RefreshPayloadInfo = { username: string };
|
type RefreshPayloadInfo = { username: string };
|
||||||
const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => {
|
const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => {
|
||||||
if (typeof obj === "string") return false;
|
if (typeof obj === "string") return false;
|
||||||
return "username" in obj && typeof (obj as { username: unknown }).username === "string";
|
return "username" in obj && typeof (obj as { username: unknown }).username === "string";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const accessExpiredTime = 60 * 60 * 2; // 2 hour
|
||||||
|
async function createAccessToken(payload: PayloadInfo, secret: string) {
|
||||||
|
return await new SignJWT(payload)
|
||||||
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
|
.setExpirationTime("2h")
|
||||||
|
.sign(new TextEncoder().encode(secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 day;
|
||||||
|
async function createRefreshToken(payload: RefreshPayloadInfo, secret: string) {
|
||||||
|
return await new SignJWT(payload)
|
||||||
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
|
.setExpirationTime("14d")
|
||||||
|
.sign(new TextEncoder().encode(secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
class TokenExpiredError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("Token expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyToken(token: string, secret: string) {
|
||||||
|
try {
|
||||||
|
const { payload } = await jwtVerify(token, new TextEncoder().encode(secret));
|
||||||
|
return payload as PayloadInfo;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof errors.JWTExpired) {
|
||||||
|
throw new TokenExpiredError();
|
||||||
|
}
|
||||||
|
throw new Error("Invalid token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const accessTokenName = "access_token";
|
export const accessTokenName = "access_token";
|
||||||
export const refreshTokenName = "refresh_token";
|
export const refreshTokenName = "refresh_token";
|
||||||
const accessExpiredTime = 60 * 60; // 1 hour
|
|
||||||
const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 day;
|
|
||||||
|
|
||||||
export const getAdminAccessTokenValue = () => {
|
|
||||||
const { jwt_secretkey } = get_setting();
|
|
||||||
return publishAccessToken(jwt_secretkey, "admin", [], accessExpiredTime);
|
|
||||||
};
|
|
||||||
export const getAdminRefreshTokenValue = () => {
|
|
||||||
const { jwt_secretkey } = get_setting();
|
|
||||||
return publishRefreshToken(jwt_secretkey, "admin", refreshExpiredTime);
|
|
||||||
};
|
|
||||||
const publishAccessToken = (secretKey: string, username: string, permission: string[], expiredtime: number) => {
|
|
||||||
const payload = sign(
|
|
||||||
{
|
|
||||||
username: username,
|
|
||||||
permission: permission,
|
|
||||||
},
|
|
||||||
secretKey,
|
|
||||||
{ expiresIn: expiredtime },
|
|
||||||
);
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
const publishRefreshToken = (secretKey: string, username: string, expiredtime: number) => {
|
|
||||||
const payload = sign({ username: username }, secretKey, { expiresIn: expiredtime });
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
function setToken(ctx: Koa.Context, token_name: string, token_payload: string | null, expiredtime: number) {
|
function setToken(ctx: Koa.Context, token_name: string, token_payload: string | null, expiredtime: number) {
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
if (token_payload === null && !ctx.cookies.get(token_name)) {
|
if (token_payload === null && !ctx.cookies.get(token_name)) {
|
||||||
@ -64,20 +75,15 @@ function setToken(ctx: Koa.Context, token_name: string, token_payload: string |
|
|||||||
expires: new Date(Date.now() + expiredtime * 1000),
|
expires: new Date(Date.now() + expiredtime * 1000),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLoginMiddleware = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => {
|
export const createLoginMiddleware = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => {
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
const secretKey = setting.jwt_secretkey;
|
const secretKey = setting.jwt_secretkey;
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
// check format
|
const {
|
||||||
if (typeof body === "string" || !("username" in body) || !("password" in body)) {
|
username,
|
||||||
return sendError(400, "invalid form : username or password is not found in query.");
|
password,
|
||||||
}
|
} = LoginRequestSchema.parse(body);
|
||||||
const username = body.username;
|
|
||||||
const password = body.password;
|
|
||||||
// check type
|
|
||||||
if (typeof username !== "string" || typeof password !== "string") {
|
|
||||||
return sendError(400, "invalid form : username or password is not string");
|
|
||||||
}
|
|
||||||
// if admin login is forbidden?
|
// if admin login is forbidden?
|
||||||
if (username === "admin" && setting.forbid_remote_admin_login) {
|
if (username === "admin" && setting.forbid_remote_admin_login) {
|
||||||
return sendError(403, "forbidden remote admin login");
|
return sendError(403, "forbidden remote admin login");
|
||||||
@ -91,8 +97,13 @@ export const createLoginMiddleware = (userController: UserAccessor) => async (ct
|
|||||||
}
|
}
|
||||||
// create token
|
// create token
|
||||||
const userPermission = await user.get_permissions();
|
const userPermission = await user.get_permissions();
|
||||||
const payload = publishAccessToken(secretKey, user.username, userPermission, accessExpiredTime);
|
const payload = await createAccessToken({
|
||||||
const payload2 = publishRefreshToken(secretKey, user.username, refreshExpiredTime);
|
username: user.username,
|
||||||
|
permission: userPermission,
|
||||||
|
}, secretKey);
|
||||||
|
const payload2 = await createRefreshToken({
|
||||||
|
username: user.username,
|
||||||
|
}, secretKey);
|
||||||
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
||||||
setToken(ctx, refreshTokenName, payload2, refreshExpiredTime);
|
setToken(ctx, refreshTokenName, payload2, refreshExpiredTime);
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
@ -115,6 +126,7 @@ export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
|
|||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createUserMiddleWare =
|
export const createUserMiddleWare =
|
||||||
(userController: UserAccessor) => async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
|
(userController: UserAccessor) => async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
|
||||||
const refreshToken = refreshTokenHandler(userController);
|
const refreshToken = refreshTokenHandler(userController);
|
||||||
@ -127,50 +139,63 @@ export const createUserMiddleWare =
|
|||||||
};
|
};
|
||||||
return await refreshToken(ctx, setGuest, next);
|
return await refreshToken(ctx, setGuest, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
|
const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
|
||||||
const accessPayload = ctx.cookies.get(accessTokenName);
|
const accessPayload = ctx.cookies.get(accessTokenName);
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
const secretKey = setting.jwt_secretkey;
|
const secretKey = setting.jwt_secretkey;
|
||||||
if (accessPayload === undefined) {
|
|
||||||
|
if (!accessPayload) {
|
||||||
return await checkRefreshAndUpdate();
|
return await checkRefreshAndUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const o = verify(accessPayload, secretKey);
|
const payload = await verifyToken(accessPayload, secretKey);
|
||||||
if (isUserState(o)) {
|
if (isUserState(payload)) {
|
||||||
ctx.state.user = o;
|
ctx.state.user = payload;
|
||||||
return await next();
|
return await next();
|
||||||
}
|
}
|
||||||
console.error("invalid token detected");
|
console.error("Invalid token detected");
|
||||||
throw new Error("token form invalid");
|
throw new Error("Token form invalid");
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
if (e instanceof TokenExpiredError) {
|
if (error instanceof TokenExpiredError) {
|
||||||
return await checkRefreshAndUpdate();
|
return await checkRefreshAndUpdate();
|
||||||
}throw e;
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkRefreshAndUpdate() {
|
async function checkRefreshAndUpdate() {
|
||||||
const refreshPayload = ctx.cookies.get(refreshTokenName);
|
const refreshPayload = ctx.cookies.get(refreshTokenName);
|
||||||
if (refreshPayload === undefined) {
|
if (!refreshPayload) {
|
||||||
return await fail(); // refresh token doesn't exist
|
return await fail(); // Refresh token doesn't exist
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const o = verify(refreshPayload, secretKey);
|
try {
|
||||||
if (isRefreshToken(o)) {
|
const payload = await verifyToken(refreshPayload, secretKey);
|
||||||
const user = await cntr.findUser(o.username);
|
if (isRefreshToken(payload)) {
|
||||||
if (user === undefined) return await fail(); // already non-existence user
|
const user = await cntr.findUser(payload.username);
|
||||||
const perm = await user.get_permissions();
|
if (!user) return await fail(); // User does not exist
|
||||||
const payload = publishAccessToken(secretKey, user.username, perm, accessExpiredTime);
|
|
||||||
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
const permissions = await user.get_permissions();
|
||||||
ctx.state.user = { username: o.username, permission: perm };
|
const newAccessToken = await createAccessToken({
|
||||||
} else {
|
username: user.username,
|
||||||
console.error("invalid token detected");
|
permission: permissions,
|
||||||
throw new Error("token form invalid");
|
}, secretKey);
|
||||||
}
|
|
||||||
} catch (e) {
|
setToken(ctx, accessTokenName, newAccessToken, accessExpiredTime);
|
||||||
if (e instanceof TokenExpiredError) {
|
ctx.state.user = { username: payload.username, permission: permissions };
|
||||||
// refresh token is expired.
|
} else {
|
||||||
return await fail();
|
console.error("Invalid token detected");
|
||||||
}throw e;
|
throw new Error("Token form invalid");
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
// Refresh token is expired
|
||||||
|
return await fail();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
return await next();
|
return await next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -195,25 +220,24 @@ export const createRefreshTokenMiddleware = (cntr: UserAccessor) => async (ctx:
|
|||||||
ctx.type = "json";
|
ctx.type = "json";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => {
|
export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (typeof body !== "object" || !("username" in body) || !("oldpassword" in body) || !("newpassword" in body)) {
|
const {
|
||||||
return sendError(400, "request body is invalid format");
|
username,
|
||||||
}
|
oldpassword,
|
||||||
const username = body.username;
|
newpassword,
|
||||||
const oldpw = body.oldpassword;
|
} = LoginResetRequestSchema.parse(body);
|
||||||
const newpw = body.newpassword;
|
|
||||||
if (typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string") {
|
|
||||||
return sendError(400, "request body is invalid format");
|
|
||||||
}
|
|
||||||
const user = await cntr.findUser(username);
|
const user = await cntr.findUser(username);
|
||||||
if (user === undefined) {
|
if (user === undefined) {
|
||||||
return sendError(403, "not authorized");
|
return sendError(403, "not authorized");
|
||||||
}
|
}
|
||||||
if (!user.password.check_password(oldpw)) {
|
if (!user.password.check_password(oldpassword)) {
|
||||||
return sendError(403, "not authorized");
|
return sendError(403, "not authorized");
|
||||||
}
|
}
|
||||||
user.reset_password(newpw);
|
user.reset_password(newpassword);
|
||||||
ctx.body = { ok: true };
|
ctx.body = { ok: true };
|
||||||
ctx.type = "json";
|
ctx.type = "json";
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import { check_type } from "../util/type_check";
|
import { check_type } from "../util/type_check.ts";
|
||||||
import type {
|
import type {
|
||||||
DocumentBody,
|
DocumentBody,
|
||||||
|
QueryListOption,
|
||||||
Document,
|
Document,
|
||||||
QueryListOption
|
db
|
||||||
} from "dbtype/api";
|
} from "dbtype";
|
||||||
|
type DBDocument = db.Document;
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Document,
|
||||||
|
DBDocument
|
||||||
|
};
|
||||||
|
|
||||||
export const MetaContentBody = {
|
export const MetaContentBody = {
|
||||||
title: "string",
|
title: "string",
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export * from "./doc";
|
export * from "./doc.ts";
|
||||||
export * from "./tag";
|
export * from "./tag.ts";
|
||||||
export * from "./user";
|
export * from "./user.ts";
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createHmac, randomBytes } from "node:crypto";
|
import { createHmac, randomBytes } from "node:crypto";
|
||||||
|
|
||||||
function hashForPassword(salt: string, password: string) {
|
function hashForPassword(salt: string, password: string) {
|
||||||
|
// TODO: use pbkdf2 or argon2
|
||||||
return createHmac("sha256", salt).update(password).digest("hex");
|
return createHmac("sha256", salt).update(password).digest("hex");
|
||||||
}
|
}
|
||||||
function createPasswordHashAndSalt(password: string): { salt: string; hash: string } {
|
function createPasswordHashAndSalt(password: string): { salt: string; hash: string } {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { DefaultContext, Middleware, Next, ParameterizedContext } from "koa";
|
import type { DefaultContext, Middleware, Next, ParameterizedContext } from "koa";
|
||||||
import compose from "koa-compose";
|
import compose from "koa-compose";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import ComicRouter from "./comic";
|
import ComicRouter from "./comic.ts";
|
||||||
import type { ContentContext } from "./context";
|
import type { ContentContext } from "./context.ts";
|
||||||
import VideoRouter from "./video";
|
import VideoRouter from "./video.ts";
|
||||||
|
|
||||||
const table: { [s: string]: Router | undefined } = {
|
const table: { [s: string]: Router | undefined } = {
|
||||||
comic: new ComicRouter(),
|
comic: new ComicRouter(),
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { Context } from "koa";
|
import type { Context } from "koa";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap";
|
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap.ts";
|
||||||
import type { ContentContext } from "./context";
|
import type { ContentContext } from "./context.ts";
|
||||||
import { since_last_modified } from "./util";
|
import { since_last_modified } from "./util.ts";
|
||||||
import type { ZipReader } from "@zip.js/zip.js";
|
import type { ZipReader } from "@zip.js/zip.js";
|
||||||
import type { FileHandle } from "node:fs/promises";
|
import type { FileHandle } from "node:fs/promises";
|
||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
|
@ -4,17 +4,17 @@ import { join } from "node:path";
|
|||||||
import type {
|
import type {
|
||||||
Document,
|
Document,
|
||||||
QueryListOption,
|
QueryListOption,
|
||||||
} from "dbtype/api";
|
} from "dbtype";
|
||||||
import type { DocumentAccessor } from "../model/doc";
|
import type { DocumentAccessor } from "../model/doc.ts";
|
||||||
import {
|
import {
|
||||||
AdminOnlyMiddleware as AdminOnly,
|
AdminOnlyMiddleware as AdminOnly,
|
||||||
createPermissionCheckMiddleware as PerCheck,
|
createPermissionCheckMiddleware as PerCheck,
|
||||||
Permission as Per,
|
Permission as Per,
|
||||||
} from "../permission/permission";
|
} from "../permission/permission.ts";
|
||||||
import { AllContentRouter } from "./all";
|
import { AllContentRouter } from "./all.ts";
|
||||||
import type { ContentLocation } from "./context";
|
import type { ContentLocation } from "./context.ts";
|
||||||
import { sendError } from "./error_handler";
|
import { sendError } from "./error_handler.ts";
|
||||||
import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util";
|
import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util.ts";
|
||||||
|
|
||||||
const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const num = Number.parseInt(ctx.params.num);
|
const num = Number.parseInt(ctx.params.num);
|
||||||
@ -35,14 +35,21 @@ const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Contex
|
|||||||
ctx.type = "json";
|
ctx.type = "json";
|
||||||
};
|
};
|
||||||
const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
|
|
||||||
const query_limit = ctx.query.limit;
|
const query_limit = ctx.query.limit;
|
||||||
const query_cursor = ctx.query.cursor;
|
const query_cursor = ctx.query.cursor;
|
||||||
const query_word = ctx.query.word;
|
const query_word = ctx.query.word;
|
||||||
const query_content_type = ctx.query.content_type;
|
const query_content_type = ctx.query.content_type;
|
||||||
const query_offset = ctx.query.offset;
|
const query_offset = ctx.query.offset;
|
||||||
const query_use_offset = ctx.query.use_offset;
|
const query_use_offset = ctx.query.use_offset;
|
||||||
if (Array.isArray(query_limit) ||Array.isArray(query_cursor) ||Array.isArray(query_word) ||Array.isArray(query_content_type) ||Array.isArray(query_offset) ||Array.isArray(query_use_offset)
|
if ([
|
||||||
) {
|
query_limit,
|
||||||
|
query_cursor,
|
||||||
|
query_word,
|
||||||
|
query_content_type,
|
||||||
|
query_offset,
|
||||||
|
query_use_offset,
|
||||||
|
].some((x) => Array.isArray(x))) {
|
||||||
return sendError(400, "paramter can not be array");
|
return sendError(400, "paramter can not be array");
|
||||||
}
|
}
|
||||||
const limit = Math.min(ParseQueryNumber(query_limit) ?? 20, 100);
|
const limit = Math.min(ParseQueryNumber(query_limit) ?? 20, 100);
|
||||||
@ -65,7 +72,7 @@ const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Contex
|
|||||||
cursor: cursor,
|
cursor: cursor,
|
||||||
eager_loading: true,
|
eager_loading: true,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
use_offset: use_offset,
|
use_offset: use_offset ?? false,
|
||||||
content_type: content_type,
|
content_type: content_type,
|
||||||
};
|
};
|
||||||
const document = await controller.findList(option);
|
const document = await controller.findList(option);
|
||||||
@ -105,6 +112,7 @@ const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, nex
|
|||||||
ctx.body = JSON.stringify(r);
|
ctx.body = JSON.stringify(r);
|
||||||
ctx.type = "json";
|
ctx.type = "json";
|
||||||
};
|
};
|
||||||
|
|
||||||
const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
let tag_name = ctx.params.tag;
|
let tag_name = ctx.params.tag;
|
||||||
const num = Number.parseInt(ctx.params.num);
|
const num = Number.parseInt(ctx.params.num);
|
||||||
@ -120,12 +128,14 @@ const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, nex
|
|||||||
ctx.body = JSON.stringify(r);
|
ctx.body = JSON.stringify(r);
|
||||||
ctx.type = "json";
|
ctx.type = "json";
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const num = Number.parseInt(ctx.params.num);
|
const num = Number.parseInt(ctx.params.num);
|
||||||
const r = await controller.del(num);
|
const r = await controller.del(num);
|
||||||
ctx.body = JSON.stringify(r);
|
ctx.body = JSON.stringify(r);
|
||||||
ctx.type = "json";
|
ctx.type = "json";
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const num = Number.parseInt(ctx.params.num);
|
const num = Number.parseInt(ctx.params.num);
|
||||||
const document = await controller.findById(num, true);
|
const document = await controller.findById(num, true);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ZodError } from "dbtype";
|
||||||
import type { Context, Next } from "koa";
|
import type { Context, Next } from "koa";
|
||||||
|
|
||||||
export interface ErrorFormat {
|
export interface ErrorFormat {
|
||||||
@ -36,7 +37,17 @@ export const error_handler = async (ctx: Context, next: Next) => {
|
|||||||
};
|
};
|
||||||
ctx.status = err.code;
|
ctx.status = err.code;
|
||||||
ctx.body = body;
|
ctx.body = body;
|
||||||
} else {
|
}
|
||||||
|
else if (err instanceof ZodError) {
|
||||||
|
const body: ErrorFormat = {
|
||||||
|
code: 400,
|
||||||
|
message: "BadRequest",
|
||||||
|
detail: err.errors.map((x) => x.message).join(", "),
|
||||||
|
};
|
||||||
|
ctx.status = 400;
|
||||||
|
ctx.body = body;
|
||||||
|
}
|
||||||
|
else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { type Context, Next } from "koa";
|
import { type Context, Next } from "koa";
|
||||||
import Router, { type RouterContext } from "koa-router";
|
import Router, { type RouterContext } from "koa-router";
|
||||||
import type { TagAccessor } from "../model/tag";
|
import type { TagAccessor } from "../model/tag.ts";
|
||||||
import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission";
|
import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission.ts";
|
||||||
import { sendError } from "./error_handler";
|
import { sendError } from "./error_handler.ts";
|
||||||
|
|
||||||
export function getTagRounter(tagController: TagAccessor) {
|
export function getTagRounter(tagController: TagAccessor) {
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createReadStream, promises } from "node:fs";
|
import { createReadStream, promises } from "node:fs";
|
||||||
import type { Context } from "koa";
|
import type { Context } from "koa";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import type { ContentContext } from "./context";
|
import type { ContentContext } from "./context.ts";
|
||||||
|
|
||||||
export async function renderVideo(ctx: Context, path: string) {
|
export async function renderVideo(ctx: Context, path: string) {
|
||||||
const ext = path.trim().split(".").pop();
|
const ext = path.trim().split(".").pop();
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import Koa from "koa";
|
import Koa from "koa";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
|
|
||||||
import { connectDB } from "./database";
|
import { connectDB } from "./database.ts";
|
||||||
import { createDiffRouter, DiffManager } from "./diff/mod";
|
import { createDiffRouter, DiffManager } from "./diff/mod.ts";
|
||||||
import { get_setting, SettingConfig } from "./SettingConfig";
|
import { get_setting, SettingConfig } from "./SettingConfig.ts";
|
||||||
|
|
||||||
import { createReadStream, readFileSync } from "node:fs";
|
import { createReadStream, readFileSync } from "node:fs";
|
||||||
import bodyparser from "koa-bodyparser";
|
import bodyparser from "koa-bodyparser";
|
||||||
import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod";
|
import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod.ts";
|
||||||
import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login";
|
import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login.ts";
|
||||||
import getContentRouter from "./route/contents";
|
import getContentRouter from "./route/contents.ts";
|
||||||
import { error_handler } from "./route/error_handler";
|
import { error_handler } from "./route/error_handler.ts";
|
||||||
|
|
||||||
import { createInterface as createReadlineInterface } from "node:readline";
|
import { createInterface as createReadlineInterface } from "node:readline";
|
||||||
import { createComicWatcher } from "./diff/watcher/comic_watcher";
|
import { createComicWatcher } from "./diff/watcher/comic_watcher.ts";
|
||||||
import type { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod";
|
import type { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod.ts";
|
||||||
import { getTagRounter } from "./route/tags";
|
import { getTagRounter } from "./route/tags.ts";
|
||||||
|
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import { extname, join } from "node:path";
|
import { extname, join } from "node:path";
|
||||||
@ -150,7 +150,7 @@ class ServerApplication {
|
|||||||
meta = createOgTagContent(
|
meta = createOgTagContent(
|
||||||
doc.title,
|
doc.title,
|
||||||
doc.tags.join(", "),
|
doc.tags.join(", "),
|
||||||
`https://aeolian.prelude.duckdns.org/api/doc/${docId}/comic/thumbnail`,
|
`https://aeolian.monoid.top/api/doc/${docId}/comic/thumbnail`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const html = makeMetaTagInjectedHTML(this.index_html, meta);
|
const html = makeMetaTagInjectedHTML(this.index_html, meta);
|
||||||
|
44
packages/server/src/util/oshash.ts
Normal file
44
packages/server/src/util/oshash.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { open } from "fs/promises";
|
||||||
|
/**
|
||||||
|
* get hash of file or directory.
|
||||||
|
*
|
||||||
|
* Contains a hashing method that matches the hashing method described
|
||||||
|
* here: [https://pypi.org/project/oshash/](https://pypi.org/project/oshash/)
|
||||||
|
* This hashing method is particularly useful when you don't want to read
|
||||||
|
* an entire file's bytes to generate a hash, provided you trust that any
|
||||||
|
* changes to the file will cause byte differences in the first and last
|
||||||
|
* bytes of the file, or a change to its file size.
|
||||||
|
*/
|
||||||
|
export async function oshash(
|
||||||
|
path: string,
|
||||||
|
){
|
||||||
|
const chunkSize = 4096;
|
||||||
|
const minFileSize = chunkSize * 2;
|
||||||
|
|
||||||
|
const fd = await open(path, "r");
|
||||||
|
const st = await fd.stat();
|
||||||
|
let hash = BigInt(st.size);
|
||||||
|
|
||||||
|
if (st.size < minFileSize){
|
||||||
|
throw new Error("File is too small to hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
// read first and last chunk
|
||||||
|
const firstChunk = Buffer.alloc(chunkSize);
|
||||||
|
await fd.read(firstChunk, 0, chunkSize, 0);
|
||||||
|
const lastChunk = Buffer.alloc(chunkSize);
|
||||||
|
await fd.read(lastChunk, 0, chunkSize, st.size - chunkSize);
|
||||||
|
// iterate over first and last chunk.
|
||||||
|
// for each uint64_t, add it to the hash.
|
||||||
|
for (let i = 0; i < chunkSize; i += 8){
|
||||||
|
hash += firstChunk.readBigUInt64LE(i);
|
||||||
|
// prevent overflow
|
||||||
|
hash = (hash & 0xFFFFFFFFFFFFFFFFn);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < chunkSize; i += 8){
|
||||||
|
hash += lastChunk.readBigUInt64LE(i);
|
||||||
|
// prevent overflow
|
||||||
|
hash = (hash & 0xFFFFFFFFFFFFFFFFn);
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
@ -5,7 +5,7 @@
|
|||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"module": "NodeNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
"lib": ["DOM", "ESNext"],
|
"lib": ["DOM", "ESNext"],
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
@ -14,15 +14,16 @@
|
|||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
"outDir": "./build", /* Redirect output structure to the directory. */
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
// "composite": true, /* Enable project compilation */
|
// "composite": true, /* Enable project compilation */
|
||||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
"noEmit": true, /* Do not emit outputs. */
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
/* Strict Type-Checking Options */
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
@ -41,9 +42,12 @@
|
|||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
"moduleResolution": "NodeNext", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
/* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "paths": {
|
||||||
|
// "dbtype/*": ["node_modules/dbtype/*"],
|
||||||
|
// },
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
@ -67,6 +71,6 @@
|
|||||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
},
|
},
|
||||||
"include": ["./"],
|
"include": ["src"],
|
||||||
"exclude": ["src/client", "app", "seeds"]
|
"exclude": ["app", "seeds", "node_modules"]
|
||||||
}
|
}
|
||||||
|
7202
pnpm-lock.yaml
7202
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user