Browse Source

format

theme
npmrun 2 years ago
parent
commit
994c7d6013
  1. 367
      packages/hapi-router/src/index.ts
  2. 20
      packages/hapi-router/src/util/decorators.ts
  3. 135
      packages/hapi-router/src/util/index.ts
  4. 30
      public/js/common/main.js
  5. 2
      public/js/page/login.js
  6. 122
      public/style/common/normalize.css
  7. 32
      public/style/common/style.css
  8. 4
      route.txt
  9. 38
      source/auth/index.ts
  10. 68
      source/log4js_config.ts
  11. 30
      source/main.ts
  12. 12
      source/models/Color.ts
  13. 12
      source/models/Constant.ts
  14. 93
      source/models/User.ts
  15. 26
      source/models/ha/user.ts
  16. 42
      source/models/ha/user_info.ts
  17. 52
      source/plugins/file-plugin.ts
  18. 286
      source/plugins/index.ts
  19. 20
      source/route/api/index.ts
  20. 143
      source/route/api/v1/upload/_upload.ts
  21. 48
      source/route/api/v1/upload/index.ts
  22. 176
      source/route/api/v1/user/index.ts
  23. 2
      source/route/htmx/index.ts
  24. 164
      source/route/views/index.ts
  25. 165
      source/route/views/index/index.ts
  26. 8
      source/route/views/nav/index.ts
  27. 262
      source/run.ts
  28. 34
      source/schema/index.ts
  29. 16
      source/util/index.ts
  30. 44
      source/util/res-helper.ts
  31. 60
      source/util/util.ts
  32. 8
      template/404.pug
  33. 120
      template/helper/flush.pug
  34. 79
      template/ui/header.pug
  35. 6
      template/views/about.pug
  36. 12
      template/views/index.pug
  37. 20
      template/views/login.pug
  38. 14
      template/views/user.pug
  39. 25
      types/global.d.ts

367
packages/hapi-router/src/index.ts

@ -1,211 +1,190 @@
// @ts-nocheck // @ts-nocheck
import { walkDir, removeIndex, isIndexEnd } from "./util"; import { walkDir, removeIndex, isIndexEnd } from "./util"
import * as Joi from "joi"; import * as Joi from "joi"
const path = require("path"); const path = require("path")
const fs = require("fs"); const fs = require("fs")
class routePlugin { class routePlugin {
public name: string = "routePlugin"; public name: string = "routePlugin"
public version: string = "0.0.1"; public version: string = "0.0.1"
public register(server: any, opts: any) { public register(server: any, opts: any) {
const sourceDir = opts.sourceDir; const sourceDir = opts.sourceDir
const type = opts.type || "jwt"; const type = opts.type || "jwt"
const auth = opts.auth || []; const auth = opts.auth || []
let array = []; let array = []
for (let i = 0; i < sourceDir.length; i++) { for (let i = 0; i < sourceDir.length; i++) {
const dir = sourceDir[i]; const dir = sourceDir[i]
console.log(dir); console.log(dir)
array.push(dir.dir + "对应路径:"); array.push(dir.dir + "对应路径:")
array = array.concat( array = array.concat(this.registerRoute(server, dir.dir, dir.prefix || "", auth, type))
this.registerRoute(server, dir.dir, dir.prefix || "", auth, type) }
); fs.writeFileSync(path.resolve(process.cwd(), "route.txt"), array.join("\n"), {
encoding: "utf-8",
})
} }
fs.writeFileSync( registerRoute(server, sourceDir, prefix, auth, type) {
path.resolve(process.cwd(), "route.txt"), const files = walkDir(sourceDir)
array.join("\n"), const routes = []
{ files.forEach(file => {
encoding: "utf-8", let filename = file.relativeFileNoExt
} let array = filename.split(path.sep).slice(1)
); let fileNoExt = removeIndex("/" + array.join("/"))
} const moduleName = path.resolve(sourceDir, filename)
registerRoute(server, sourceDir, prefix, auth, type) { const obj = require(moduleName)
const files = walkDir(sourceDir); if (obj.default) {
const routes = []; const func = new (obj.default || obj)()
files.forEach((file) => { const prototype = Object.getPrototypeOf(func)
let filename = file.relativeFileNoExt; const keys = Reflect.ownKeys(prototype)
let array = filename.split(path.sep).slice(1); for (const key of keys) {
let fileNoExt = removeIndex("/" + array.join("/")); if (key !== "constructor") {
const moduleName = path.resolve(sourceDir, filename); let ff = func[key]
const obj = require(moduleName); let handler: () => void = undefined
if (obj.default) { // 默认方法
const func = new (obj.default || obj)(); const method = ff.$method || "GET"
const prototype = Object.getPrototypeOf(func); // 路由收集规则
const keys = Reflect.ownKeys(prototype); let route = ""
for (const key of keys) { if (ff.$route) {
if (key !== "constructor") { if (isIndexEnd(fileNoExt)) {
let ff = func[key]; route = ff.$route
let handler:()=>void = undefined } else {
// 默认方法 route = fileNoExt + ff.$route
const method = ff.$method || "GET"; }
// 路由收集规则 } else {
let route = ""; if (isIndexEnd(fileNoExt)) {
if (ff.$route) { route = fileNoExt + key.toString()
if (isIndexEnd(fileNoExt)) { } else {
route = ff.$route; route = fileNoExt + "/" + key.toString()
} else { }
route = fileNoExt + ff.$route;
}
} else {
if (isIndexEnd(fileNoExt)) {
route = fileNoExt + key.toString();
} else {
route = fileNoExt + "/" + key.toString();
}
}
route = removeIndex(route);
route = prefix ? route[0] + prefix + "/" + route.slice(1) : route;
// 配置规则
const options = ff.$options ? ff.$options : {};
if (!options.auth) {
if (ff.$auth == undefined) {
if (
auth &&
auth.length &&
auth.filter((v) => route.startsWith(v)).length
) {
options.auth = type;
} else {
options.auth = false;
}
} else if (ff.$auth) {
options.auth =
typeof ff.$auth === "boolean"
? type
: {
strategy: type,
mode: ff.$auth,
};
} else {
options.auth = false;
}
}
if (!options.validate) {
let validateObj = ff.$validate || {};
if (options.auth && type === "jwt") {
if (validateObj.headers) {
validateObj.headers = validateObj.headers.keys({
Authorization: Joi.string(),
});
} else {
validateObj.headers = Joi.object({
headers: Joi.object({
Authorization: Joi.string(),
}).unknown(), // 注意加上这个
});
}
}
if (validateObj&&!!Object.keys(validateObj).length) {
const failReason = validateObj.failReason
delete validateObj.failReason
if(validateObj.failAction === "log"){
if(!options.log) options.log = {}
options.log.collect = true
let errto = validateObj.$errto
handler = async function (...argus){
const request = argus[0]
const h = argus[1]
if(request.logs&&!!request.logs.length && errto){
// request.yar.flash('error', request.logs.map((v: any)=>v.error.message));
request.yar.flash('error', failReason);
return h.redirect(errto);
} }
return await ff.call(this, ...argus) route = removeIndex(route)
} route = prefix ? route[0] + prefix + "/" + route.slice(1) : route
} // 配置规则
if(validateObj.failAction === "function"){ const options = ff.$options ? ff.$options : {}
let errto = validateObj.$errto if (!options.auth) {
validateObj.failAction = async function(request, h, err){ if (ff.$auth == undefined) {
if(err.details){ if (auth && auth.length && auth.filter(v => route.startsWith(v)).length) {
request.$joi_error = err.details.map(v=>v.message) options.auth = type
} else {
options.auth = false
}
} else if (ff.$auth) {
options.auth =
typeof ff.$auth === "boolean"
? type
: {
strategy: type,
mode: ff.$auth,
}
} else {
options.auth = false
}
} }
return h.continue; if (!options.validate) {
} let validateObj = ff.$validate || {}
handler = async function (...argus){ if (options.auth && type === "jwt") {
const request = argus[0] if (validateObj.headers) {
const h = argus[1] validateObj.headers = validateObj.headers.keys({
if(request.$joi_error){ Authorization: Joi.string(),
loggerSite.debug('传输参数错误: ', request.$joi_error) })
request.yar.flash('error', failReason); } else {
delete request.$joi_error validateObj.headers = Joi.object({
return h.redirect(errto); headers: Joi.object({
Authorization: Joi.string(),
}).unknown(), // 注意加上这个
})
}
}
if (validateObj && !!Object.keys(validateObj).length) {
const failReason = validateObj.failReason
delete validateObj.failReason
if (validateObj.failAction === "log") {
if (!options.log) options.log = {}
options.log.collect = true
let errto = validateObj.$errto
handler = async function (...argus) {
const request = argus[0]
const h = argus[1]
if (request.logs && !!request.logs.length && errto) {
// request.yar.flash('error', request.logs.map((v: any)=>v.error.message));
request.yar.flash("error", failReason)
return h.redirect(errto)
}
return await ff.call(this, ...argus)
}
}
if (validateObj.failAction === "function") {
let errto = validateObj.$errto
validateObj.failAction = async function (request, h, err) {
if (err.details) {
request.$joi_error = err.details.map(v => v.message)
}
return h.continue
}
handler = async function (...argus) {
const request = argus[0]
const h = argus[1]
if (request.$joi_error) {
loggerSite.debug("传输参数错误: ", request.$joi_error)
request.yar.flash("error", failReason)
delete request.$joi_error
return h.redirect(errto)
}
return await ff.call(this, ...argus)
}
}
options.validate = validateObj
}
}
// && route.startsWith("/api")
if (ff.$swagger) {
options.description = ff.$swagger[0]
options.notes = ff.$swagger[1]
options.tags = ff.$swagger[2]
}
let str = route
if (
(typeof options.auth === "string" && options.auth) ||
(typeof options.auth === "object" && options.auth.mode === "required")
) {
str = " 需要权限 : " + " " + full(method) + " " + str
} else if (typeof options.auth === "object" && options.auth.mode === "optional") {
str = " 不需权限(提供即需验证): " + " " + full(method) + " " + str
} else if (typeof options.auth === "object" && options.auth.mode === "try") {
str = " 不需权限(提供无需验证): " + " " + full(method) + " " + str
} else {
str = " 不需权限 : " + " " + full(method) + " " + str
} }
return await ff.call(this, ...argus) routes.push(str)
if (options.validate && options.validate.$errto) {
delete options.validate.$errto
}
if (!handler) {
handler = ff
}
server.route({
method: method,
path: route,
handler: handler,
options: options,
})
} }
} }
options.validate = validateObj;
}
}
// && route.startsWith("/api")
if (ff.$swagger) {
options.description = ff.$swagger[0];
options.notes = ff.$swagger[1];
options.tags = ff.$swagger[2];
}
let str = route;
if (
(typeof options.auth === "string" && options.auth) ||
(typeof options.auth === "object" &&
options.auth.mode === "required")
) {
str =
" 需要权限 : " + " " + full(method) + " " + str;
} else if (
typeof options.auth === "object" &&
options.auth.mode === "optional"
) {
str =
" 不需权限(提供即需验证): " + " " + full(method) + " " + str;
} else if (
typeof options.auth === "object" &&
options.auth.mode === "try"
) {
str =
" 不需权限(提供无需验证): " + " " + full(method) + " " + str;
} else {
str =
" 不需权限 : " + " " + full(method) + " " + str;
} }
routes.push(str); })
return routes
if(options.validate && options.validate.$errto){ }
delete options.validate.$errto
}
if(!handler){
handler = ff
}
server.route({
method: method,
path: route,
handler: handler,
options: options,
});
}
}
}
});
return routes;
}
} }
function full(str: string, length = 10) { function full(str: string, length = 10) {
let len = str.length; let len = str.length
let need = length - len; let need = length - len
if (need <= 0) return str; if (need <= 0) return str
return str + [...Array(need)].map((v, i) => " ").join(""); return str + [...Array(need)].map((v, i) => " ").join("")
} }
const plugin = new routePlugin(); const plugin = new routePlugin()
export { plugin }; export { plugin }
export * from "./util/decorators"; export * from "./util/decorators"

20
packages/hapi-router/src/util/decorators.ts

@ -1,40 +1,40 @@
// @ts-nocheck // @ts-nocheck
/** /**
* *
* @param opts * @param opts
*/ */
type TMethod = "GET" | "POST" | "PUT" | "DELETE" type TMethod = "GET" | "POST" | "PUT" | "DELETE"
export function method(opts?:TMethod|Array<TMethod>) { export function method(opts?: TMethod | Array<TMethod>) {
return function (target, propertyKey: string, descriptor: PropertyDescriptor) { return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
target[propertyKey].$method = opts target[propertyKey].$method = opts
} }
} }
export function route(route?:string) { export function route(route?: string) {
return function (target, propertyKey: string, descriptor: PropertyDescriptor) { return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
target[propertyKey].$route = route target[propertyKey].$route = route
} }
} }
export function config(options:Object) { export function config(options: Object) {
return function (target, propertyKey: string, descriptor: PropertyDescriptor) { return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
target[propertyKey].$options = options target[propertyKey].$options = options
} }
} }
export function auth(isAuth:boolean | "try" | "required" | "optional" = true) { export function auth(isAuth: boolean | "try" | "required" | "optional" = true) {
return function (target, propertyKey: string, descriptor: PropertyDescriptor) { return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
target[propertyKey].$auth = isAuth target[propertyKey].$auth = isAuth
} }
} }
export function validate(validate:Object) { export function validate(validate: Object) {
return function (target, propertyKey: string, descriptor: PropertyDescriptor) { return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
target[propertyKey].$validate = validate target[propertyKey].$validate = validate
} }
} }
export function swagger(desc,notes,tags) { export function swagger(desc, notes, tags) {
return function (target, propertyKey: string, descriptor: PropertyDescriptor) { return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
target[propertyKey].$swagger = [desc,notes,tags] target[propertyKey].$swagger = [desc, notes, tags]
} }
} }

135
packages/hapi-router/src/util/index.ts

@ -3,80 +3,79 @@
const path = require("path") const path = require("path")
const fs = require("fs") const fs = require("fs")
export function removeIndex(ss:any) { export function removeIndex(ss: any) {
const remove = (str:any) => { const remove = (str: any) => {
if (str.endsWith("/index")) { if (str.endsWith("/index")) {
return str.slice(0, -6); return str.slice(0, -6)
} }
if (str.endsWith("index")) { if (str.endsWith("index")) {
return str.slice(0, -5); return str.slice(0, -5)
}
return str ? str : "/"
} }
return str ? str : "/"; let r = true
}; let rr = ss
let r = true; while (r) {
let rr = ss; if (rr.endsWith("/index")) {
while (r) { rr = remove(rr)
if (rr.endsWith("/index")) { } else {
rr = remove(rr); r = false
} else { }
r = false;
} }
} return rr ? rr : "/"
return rr ? rr : "/";
} }
export function isIndexEnd(str:any) { export function isIndexEnd(str: any) {
return str.length == 1 && str.endsWith("/"); return str.length == 1 && str.endsWith("/")
} }
export function walkDir( export function walkDir(
filePath:any, filePath: any,
exclude = ["node_modules", "^_", ".git", ".idea", ".gitignore", "client","\.txt$","\.test\.js$","\.test\.ts$"] exclude = ["node_modules", "^_", ".git", ".idea", ".gitignore", "client", ".txt$", ".test.js$", ".test.ts$"],
) { ) {
let files:any[] = []; let files: any[] = []
function Data(opts:any) { function Data(opts: any) {
this.relativeDir = opts.relativeDir; this.relativeDir = opts.relativeDir
this.relativeFile = opts.relativeFile; this.relativeFile = opts.relativeFile
this.filename = opts.filename; this.filename = opts.filename
this.file = opts.file; this.file = opts.file
this.absoluteFile = opts.absoluteFile; this.absoluteFile = opts.absoluteFile
this.relativeFileNoExt = opts.relativeFileNoExt; this.relativeFileNoExt = opts.relativeFileNoExt
this.absoluteDir = opts.absoluteDir; this.absoluteDir = opts.absoluteDir
} }
function readDir(filePath, dirname = ".") { function readDir(filePath, dirname = ".") {
let res = fs.readdirSync(filePath); let res = fs.readdirSync(filePath)
res.forEach((filename) => { res.forEach(filename => {
const filepath = path.resolve(filePath, filename); const filepath = path.resolve(filePath, filename)
const stat = fs.statSync(filepath); const stat = fs.statSync(filepath)
const name = filepath.split(path.sep).slice(-1)[0]; const name = filepath.split(path.sep).slice(-1)[0]
if (typeof exclude === "string" && new RegExp(exclude).test(name)) { if (typeof exclude === "string" && new RegExp(exclude).test(name)) {
return; return
} }
if (Array.isArray(exclude)) { if (Array.isArray(exclude)) {
for (let i = 0; i < exclude.length; i++) { for (let i = 0; i < exclude.length; i++) {
const excludeItem = exclude[i]; const excludeItem = exclude[i]
if (new RegExp(excludeItem).test(name)) { if (new RegExp(excludeItem).test(name)) {
return; return
} }
} }
} }
if (!stat.isFile()) { if (!stat.isFile()) {
readDir(filepath, dirname + path.sep + name); readDir(filepath, dirname + path.sep + name)
} else { } else {
const data = new Data({ const data = new Data({
relativeDir: dirname, relativeDir: dirname,
relativeFile: dirname + path.sep + path.parse(filepath).base, relativeFile: dirname + path.sep + path.parse(filepath).base,
relativeFileNoExt: dirname + path.sep + path.parse(filepath).name, relativeFileNoExt: dirname + path.sep + path.parse(filepath).name,
file: path.parse(filepath).base, file: path.parse(filepath).base,
filename: path.parse(filepath).name, filename: path.parse(filepath).name,
absoluteFile: filepath, absoluteFile: filepath,
absoluteDir: path.parse(filepath).dir, absoluteDir: path.parse(filepath).dir,
}); })
files.push(data); files.push(data)
} }
}); })
} }
readDir(filePath); readDir(filePath)
return files; return files
} }

30
public/js/common/main.js

@ -15,24 +15,26 @@ document.addEventListener("DOMContentLoaded", () => {
}) })
}) })
const $messages = Array.prototype.slice.call(document.querySelectorAll(".message-container .message button.delete"), 0) const $messages = Array.prototype.slice.call(
document.querySelectorAll(".message-container .message button.delete"),
0,
)
$messages.forEach((el, index) => { $messages.forEach((el, index) => {
let timeID; let timeID
function click() { function click() {
// Get the target from the "data-target" attribute // Get the target from the "data-target" attribute
const target = el.dataset.target const target = el.dataset.target
const $target = document.getElementById(target) const $target = document.getElementById(target)
el.removeEventListener("click", click) el.removeEventListener("click", click)
$target.remove() $target.remove()
clearTimeout(timeID) clearTimeout(timeID)
} }
timeID = setTimeout(() => { timeID = setTimeout(() => {
const target = el.dataset.target const target = el.dataset.target
const $target = document.getElementById(target) const $target = document.getElementById(target)
el.removeEventListener("click", click) el.removeEventListener("click", click)
$target.remove() $target.remove()
}, (index + 1) * 6000); }, (index + 1) * 6000)
el.addEventListener("click", click) el.addEventListener("click", click)
}) })
}) })

2
public/js/page/login.js

@ -14,4 +14,4 @@ if (referer) {
let url = new URLSearchParams(window.location.search).get("next") let url = new URLSearchParams(window.location.search).get("next")
referer.value = url ? url : "" referer.value = url ? url : ""
} }
console.log(referer); console.log(referer)

122
public/style/common/normalize.css

@ -8,9 +8,9 @@
* 2. Prevent adjustments of font size after orientation changes in iOS. * 2. Prevent adjustments of font size after orientation changes in iOS.
*/ */
html { html {
line-height: 1.15; /* 1 */ line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */
} }
/* Sections /* Sections
@ -21,7 +21,7 @@
*/ */
body { body {
margin: 0; margin: 0;
} }
/** /**
@ -29,7 +29,7 @@ body {
*/ */
main { main {
display: block; display: block;
} }
/** /**
@ -38,8 +38,8 @@ main {
*/ */
h1 { h1 {
font-size: 2em; font-size: 2em;
margin: 0.67em 0; margin: 0.67em 0;
} }
/* Grouping content /* Grouping content
@ -51,9 +51,9 @@ h1 {
*/ */
hr { hr {
box-sizing: content-box; /* 1 */ box-sizing: content-box; /* 1 */
height: 0; /* 1 */ height: 0; /* 1 */
overflow: visible; /* 2 */ overflow: visible; /* 2 */
} }
/** /**
@ -62,8 +62,8 @@ hr {
*/ */
pre { pre {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */ font-size: 1em; /* 2 */
} }
/* Text-level semantics /* Text-level semantics
@ -74,7 +74,7 @@ pre {
*/ */
a { a {
background-color: transparent; background-color: transparent;
} }
/** /**
@ -83,9 +83,9 @@ a {
*/ */
abbr[title] { abbr[title] {
border-bottom: none; /* 1 */ border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */ text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */ text-decoration: underline dotted; /* 2 */
} }
/** /**
@ -94,7 +94,7 @@ abbr[title] {
b, b,
strong { strong {
font-weight: bolder; font-weight: bolder;
} }
/** /**
@ -105,8 +105,8 @@ strong {
code, code,
kbd, kbd,
samp { samp {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */ font-size: 1em; /* 2 */
} }
/** /**
@ -114,7 +114,7 @@ samp {
*/ */
small { small {
font-size: 80%; font-size: 80%;
} }
/** /**
@ -124,18 +124,18 @@ small {
sub, sub,
sup { sup {
font-size: 75%; font-size: 75%;
line-height: 0; line-height: 0;
position: relative; position: relative;
vertical-align: baseline; vertical-align: baseline;
} }
sub { sub {
bottom: -0.25em; bottom: -0.25em;
} }
sup { sup {
top: -0.5em; top: -0.5em;
} }
/* Embedded content /* Embedded content
@ -146,7 +146,7 @@ sup {
*/ */
img { img {
border-style: none; border-style: none;
} }
/* Forms /* Forms
@ -162,10 +162,10 @@ input,
optgroup, optgroup,
select, select,
textarea { textarea {
font-family: inherit; /* 1 */ font-family: inherit; /* 1 */
font-size: 100%; /* 1 */ font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */ line-height: 1.15; /* 1 */
margin: 0; /* 2 */ margin: 0; /* 2 */
} }
/** /**
@ -174,8 +174,9 @@ textarea {
*/ */
button, button,
input { /* 1 */ input {
overflow: visible; /* 1 */
overflow: visible;
} }
/** /**
@ -184,8 +185,9 @@ input { /* 1 */
*/ */
button, button,
select { /* 1 */ select {
text-transform: none; /* 1 */
text-transform: none;
} }
/** /**
@ -196,7 +198,7 @@ button,
[type="button"], [type="button"],
[type="reset"], [type="reset"],
[type="submit"] { [type="submit"] {
-webkit-appearance: button; -webkit-appearance: button;
} }
/** /**
@ -207,8 +209,8 @@ button::-moz-focus-inner,
[type="button"]::-moz-focus-inner, [type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner { [type="submit"]::-moz-focus-inner {
border-style: none; border-style: none;
padding: 0; padding: 0;
} }
/** /**
@ -219,7 +221,7 @@ button:-moz-focusring,
[type="button"]:-moz-focusring, [type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring, [type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring { [type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText; outline: 1px dotted ButtonText;
} }
/** /**
@ -227,7 +229,7 @@ button:-moz-focusring,
*/ */
fieldset { fieldset {
padding: 0.35em 0.75em 0.625em; padding: 0.35em 0.75em 0.625em;
} }
/** /**
@ -238,12 +240,12 @@ fieldset {
*/ */
legend { legend {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
color: inherit; /* 2 */ color: inherit; /* 2 */
display: table; /* 1 */ display: table; /* 1 */
max-width: 100%; /* 1 */ max-width: 100%; /* 1 */
padding: 0; /* 3 */ padding: 0; /* 3 */
white-space: normal; /* 1 */ white-space: normal; /* 1 */
} }
/** /**
@ -251,7 +253,7 @@ legend {
*/ */
progress { progress {
vertical-align: baseline; vertical-align: baseline;
} }
/** /**
@ -259,7 +261,7 @@ progress {
*/ */
textarea { textarea {
overflow: auto; overflow: auto;
} }
/** /**
@ -269,8 +271,8 @@ textarea {
[type="checkbox"], [type="checkbox"],
[type="radio"] { [type="radio"] {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
padding: 0; /* 2 */ padding: 0; /* 2 */
} }
/** /**
@ -279,7 +281,7 @@ textarea {
[type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button { [type="number"]::-webkit-outer-spin-button {
height: auto; height: auto;
} }
/** /**
@ -288,8 +290,8 @@ textarea {
*/ */
[type="search"] { [type="search"] {
-webkit-appearance: textfield; /* 1 */ -webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */ outline-offset: -2px; /* 2 */
} }
/** /**
@ -297,7 +299,7 @@ textarea {
*/ */
[type="search"]::-webkit-search-decoration { [type="search"]::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
/** /**
@ -306,8 +308,8 @@ textarea {
*/ */
::-webkit-file-upload-button { ::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */ -webkit-appearance: button; /* 1 */
font: inherit; /* 2 */ font: inherit; /* 2 */
} }
/* Interactive /* Interactive
@ -318,7 +320,7 @@ textarea {
*/ */
details { details {
display: block; display: block;
} }
/* /*
@ -326,7 +328,7 @@ details {
*/ */
summary { summary {
display: list-item; display: list-item;
} }
/* Misc /* Misc
@ -337,7 +339,7 @@ summary {
*/ */
template { template {
display: none; display: none;
} }
/** /**
@ -345,5 +347,5 @@ template {
*/ */
[hidden] { [hidden] {
display: none; display: none;
} }

32
public/style/common/style.css

@ -1,21 +1,21 @@
html{ html {
overflow-y: auto overflow-y: auto;
} }
.message-container{ .message-container {
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
z-index: 999; z-index: 999;
overflow: auto; overflow: auto;
height: 100vh; height: 100vh;
} }
.message-container::-webkit-scrollbar{ .message-container::-webkit-scrollbar {
display: none; display: none;
} }
.message-container .message{ .message-container .message {
min-width: 250px; min-width: 250px;
max-width: 250px; max-width: 250px;
margin: 25px; margin: 25px;
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
} }

4
route.txt

@ -1,6 +1,6 @@
D:\1XYX\demo\hapi-demo\source\route\htmx对应路径: /home/topuser/桌面/demo/hapi-demo/source/route/htmx对应路径:
不需权限 : GET /htmx/clicked 不需权限 : GET /htmx/clicked
D:\1XYX\demo\hapi-demo\source\route\views对应路径: /home/topuser/桌面/demo/hapi-demo/source/route/views对应路径:
不需权限(提供无需验证): GET /404 不需权限(提供无需验证): GET /404
不需权限 : GET /css 不需权限 : GET /css
不需权限(提供无需验证): GET / 不需权限(提供无需验证): GET /

38
source/auth/index.ts

@ -1,27 +1,27 @@
import { Req } from "#/global"; import { Req } from "#/global"
export async function validateJwt(decoded, request: Req, h) { export async function validateJwt(decoded, request: Req, h) {
if (decoded.id) { if (decoded.id) {
const User = request.getModel("User") const User = request.getModel("User")
const result = await User.findOne({ where: { id: decoded.id } }); const result = await User.findOne({ where: { id: decoded.id } })
if (result == null) { if (result == null) {
return { isValid: false }; return { isValid: false }
}
return { isValid: true }
} else {
return { isValid: false }
} }
return { isValid: true };
} else {
return { isValid: false };
}
} }
export async function validateSession(request: Req, session) { export async function validateSession(request: Req, session) {
const User = request.getModel("User") const User = request.getModel("User")
if (session.id) { if (session.id) {
const result = await User.findOne({ where: { id: session.id } }); const result = await User.findOne({ where: { id: session.id } })
if (result == null) { if (result == null) {
return { valid: false }; return { valid: false }
}
return { valid: true, credentials: result }
} else {
return { valid: false }
} }
return { valid: true, credentials: result };
} else {
return { valid: false };
}
} }

68
source/log4js_config.ts

@ -1,37 +1,37 @@
import path from "path"; import path from "path"
export default function () { export default function () {
return { return {
appenders: { appenders: {
file: { file: {
type: "file", type: "file",
filename: path.resolve(__dirname, "../", "./log", "./Site.log"), filename: path.resolve(__dirname, "../", "./log", "./Site.log"),
}, },
SQL: { SQL: {
type: "file", type: "file",
filename: path.resolve(__dirname, "../", "./log", "./SQL.log"), filename: path.resolve(__dirname, "../", "./log", "./SQL.log"),
}, },
console: { console: {
type: "console", type: "console",
}, },
}, },
categories: { categories: {
default: { default: {
appenders: ["console"], appenders: ["console"],
level: "all", level: "all",
}, },
HAPI: { HAPI: {
appenders: ["console"], appenders: ["console"],
level: "all", level: "all",
}, },
Site: { Site: {
appenders: ["file", "console"], appenders: ["file", "console"],
level: "debug", level: "debug",
}, },
SQL: { SQL: {
appenders: ["SQL"], appenders: ["SQL"],
level: "debug", level: "debug",
}, },
}, },
}; }
} }

30
source/main.ts

@ -1,21 +1,21 @@
// require("dotenv").config(); // require("dotenv").config();
import { configure, getLogger } from "log4js"; import { configure, getLogger } from "log4js"
import log4jsConfig from "./log4js_config"; import log4jsConfig from "./log4js_config"
configure(log4jsConfig()); configure(log4jsConfig())
const loggerSite = getLogger("Site"); const loggerSite = getLogger("Site")
const loggerSQL = getLogger("SQL"); const loggerSQL = getLogger("SQL")
const logger = getLogger("HAPI"); const logger = getLogger("HAPI")
loggerSite.level = "debug"; loggerSite.level = "debug"
loggerSQL.level = "debug"; loggerSQL.level = "debug"
global.logger = logger; global.logger = logger
global.loggerSite = loggerSite; global.loggerSite = loggerSite
global.loggerSQL = loggerSQL; global.loggerSQL = loggerSQL
import { run } from "./run"; import { run } from "./run"
run().then((server) => { run().then(server => {
global.server = server; global.server = server
}); })

12
source/models/Color.ts

@ -4,20 +4,20 @@ module.exports = function (sequelize, DataTypes) {
{ {
// 图片地址 // 图片地址
color: { color: {
type: DataTypes.STRING type: DataTypes.STRING,
}, },
title: { title: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
describe: { describe: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
}, },
{ {
timestamps: false, timestamps: false,
} },
); )
return Color return Color
}; }

12
source/models/Constant.ts

@ -4,21 +4,21 @@ module.exports = function (sequelize, DataTypes) {
{ {
// 键 // 键
key: { key: {
type: DataTypes.STRING type: DataTypes.STRING,
}, },
// 值 // 值
value: { value: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
describe: { describe: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
}, },
{ {
timestamps: false, timestamps: false,
} },
); )
return Constant return Constant
}; }

93
source/models/User.ts

@ -1,64 +1,67 @@
import { Sequelize, DataTypes, Optional, Model } from "sequelize" import { Sequelize, DataTypes, Optional, Model } from "sequelize"
interface UserAttributes { interface UserAttributes {
id: number; id: number
username: string; username: string
password: string; password: string
nickname: string; nickname: string
email: string; email: string
createdAt?: Date; createdAt?: Date
updatedAt?: Date; updatedAt?: Date
deletedAt?: Date; deletedAt?: Date
} }
export interface UserInput extends Optional<UserAttributes, 'id'> { } export interface UserInput extends Optional<UserAttributes, "id"> {}
export interface UserOuput extends Required<UserAttributes> { } export interface UserOuput extends Required<UserAttributes> {}
export type TUserModel = ReturnType<typeof UserModel> export type TUserModel = ReturnType<typeof UserModel>
type DT = typeof DataTypes type DT = typeof DataTypes
export default function UserModel(sequelize: Sequelize, DataTypes: DT) { export default function UserModel(sequelize: Sequelize, DataTypes: DT) {
class User extends Model<UserAttributes, UserInput> implements UserAttributes { class User extends Model<UserAttributes, UserInput> implements UserAttributes {
public id: number; public id: number
public username: string; public username: string
public password: string; public password: string
public nickname: string; public nickname: string
public email: string; public email: string
// timestamps! // timestamps!
public readonly createdAt!: Date; public readonly createdAt!: Date
public readonly updatedAt!: Date; public readonly updatedAt!: Date
public readonly deletedAt!: Date; public readonly deletedAt!: Date
} }
User.init({ User.init(
id: { {
type: DataTypes.INTEGER.UNSIGNED, id: {
autoIncrement: true, type: DataTypes.INTEGER.UNSIGNED,
primaryKey: true, autoIncrement: true,
primaryKey: true,
},
username: {
type: DataTypes.STRING,
allowNull: false,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
nickname: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
},
}, },
username: { {
type: DataTypes.STRING, sequelize,
allowNull: false timestamps: true,
paranoid: true, // 对模型施加了一个软删除
}, },
password: { )
type: DataTypes.STRING,
allowNull: false
},
nickname: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
}
}, {
sequelize,
timestamps: true,
paranoid: true // 对模型施加了一个软删除
})
// 覆盖User的toJSON方法 // 覆盖User的toJSON方法
interface User{ interface User {
toJSON: ()=> UserOuput toJSON: () => UserOuput
} }
return User return User
}; }

26
source/models/ha/user.ts

@ -2,17 +2,19 @@ import { Sequelize, DataTypes } from "sequelize"
type DT = typeof DataTypes type DT = typeof DataTypes
module.exports = function (sequelize: Sequelize, DataTypes: DT) { module.exports = function (sequelize: Sequelize, DataTypes: DT) {
const User = sequelize.define('ha-user', { const User = sequelize.define(
username: { "ha-user",
type: DataTypes.STRING, {
allowNull: false username: {
type: DataTypes.STRING,
allowNull: false,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
}, },
password: { {},
type: DataTypes.STRING, )
allowNull: false
}
}, {
});
return User return User
}; }

42
source/models/ha/user_info.ts

@ -2,28 +2,30 @@ import { Sequelize, DataTypes } from "sequelize"
type DT = typeof DataTypes type DT = typeof DataTypes
module.exports = function (sequelize: Sequelize, DataTypes: DT) { module.exports = function (sequelize: Sequelize, DataTypes: DT) {
const UserInfo = sequelize.define('ha-user_info', { const UserInfo = sequelize.define(
nickname: { "ha-user_info",
type: DataTypes.STRING, {
allowNull: false nickname: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
},
avatar: {
type: DataTypes.STRING,
},
tel: {
type: DataTypes.STRING,
},
}, },
email: { {},
type: DataTypes.STRING, )
},
avatar: {
type: DataTypes.STRING,
},
tel: {
type: DataTypes.STRING,
}
}, {
});
// @ts-ignore // @ts-ignore
UserInfo.associate = function (models) { UserInfo.associate = function (models) {
models['ha-user'].hasOne(models['ha-user_info']); models["ha-user"].hasOne(models["ha-user_info"])
models['ha-user_info'].belongsTo(models['ha-user'], { foreignKey: 'user_id' }); models["ha-user_info"].belongsTo(models["ha-user"], { foreignKey: "user_id" })
}; }
return UserInfo return UserInfo
}; }

52
source/plugins/file-plugin.ts

@ -1,29 +1,29 @@
import { publicDir } from "@/util"; import { publicDir } from "@/util"
const Inert = require("@hapi/inert"); const Inert = require("@hapi/inert")
const filePlugin = { const filePlugin = {
name: "filePlugin", name: "filePlugin",
version: "0.0.1", version: "0.0.1",
register: async function (server, options) { register: async function (server, options) {
server.settings.routes = { server.settings.routes = {
files: { files: {
relativeTo: publicDir, relativeTo: publicDir,
}, },
}; }
await server.register(Inert); await server.register(Inert)
server.route({ server.route({
method: "GET", method: "GET",
path: "/public/{param*}", path: "/public/{param*}",
config: { auth: false }, config: { auth: false },
handler: { handler: {
directory: { directory: {
path: publicDir, path: publicDir,
index: true, index: true,
redirectToSlash: true, redirectToSlash: true,
}, },
}, },
}); })
}, },
}; }
export default filePlugin; export default filePlugin

286
source/plugins/index.ts

@ -1,155 +1,159 @@
import filePlugin from "./file-plugin"; import filePlugin from "./file-plugin"
import path from "path"; import path from "path"
import { baseDir, sourceDir } from "@/util"; import { baseDir, sourceDir } from "@/util"
import { plugin as routePlugin } from "@noderun/hapi-router"; import { plugin as routePlugin } from "@noderun/hapi-router"
import {ServerRegisterPluginObject, Plugin, Server, Request, ResponseObject} from "@hapi/hapi" import { ServerRegisterPluginObject, Plugin, Server, Request, ResponseObject } from "@hapi/hapi"
import Hoek from "@hapi/hoek" import Hoek from "@hapi/hoek"
import HapiYar from "@hapi/yar"; import HapiYar from "@hapi/yar"
import HapiCrumb from "@hapi/crumb" import HapiCrumb from "@hapi/crumb"
import { Stream } from "stream"; import { Stream } from "stream"
import HapiPino from "hapi-pino"; import HapiPino from "hapi-pino"
const pino = require('pino') const pino = require("pino")
const transport = pino.transport({ const transport = pino.transport({
targets: [ targets: [
// process.env.NODE_ENV !== 'production' ? { // process.env.NODE_ENV !== 'production' ? {
// target: 'pino-pretty', // target: 'pino-pretty',
// options: { // options: {
// colorize: true, // colorize: true,
// translateTime: 'HH:MM:ss.l mmmm dS yyyy "UTC"' // translateTime: 'HH:MM:ss.l mmmm dS yyyy "UTC"'
// }, // },
// level: 'info' } : { target: 'pino/file', level: 'info' }, // level: 'info' } : { target: 'pino/file', level: 'info' },
{ target: 'pino-pretty', level: 'trace', options: { {
destination: path.resolve(baseDir, "./log/pino.log"), target: "pino-pretty",
colorize: true, level: "trace",
translateTime: 'HH:MM:ss.l mmmm dS yyyy "UTC"', options: {
mkdir: true, destination: path.resolve(baseDir, "./log/pino.log"),
// append: false colorize: true,
}} translateTime: 'HH:MM:ss.l mmmm dS yyyy "UTC"',
// { target: 'pino/file', level: 'trace', options: { mkdir: true,
// destination: path.resolve(baseDir, "./log/pino.log"), // append: false
// mkdir: true, },
// // append: false },
// }} // { target: 'pino/file', level: 'trace', options: {
] // destination: path.resolve(baseDir, "./log/pino.log"),
// mkdir: true,
// // append: false
// }}
],
}) })
export default [ export default [
{ {
plugin: HapiPino, plugin: HapiPino,
options: { options: {
logPayload: true, logPayload: true,
logQueryParams: true, logQueryParams: true,
logPathParams: true, logPathParams: true,
logRouteTags: true, logRouteTags: true,
// logRequestStart: true, // logRequestStart: true,
instance: pino(transport), instance: pino(transport),
ignoreFunc: (options, request) => { ignoreFunc: (options, request) => {
return request.path.startsWith('/public') || request.path.startsWith('/404') return request.path.startsWith("/public") || request.path.startsWith("/404")
},
// stream: fs.createWriteStream(path.resolve(baseDir, "./log/pino.log"), { encoding: "utf-8" })
// prettyPrint: process.env.NODE_ENV !== 'production',
// Redact Authorization headers, see https://getpino.io/#/docs/redaction
// redact: ['req.headers.cookie']
},
},
{
plugin: filePlugin as unknown as Plugin<any>,
},
{
plugin: routePlugin as Plugin<any>,
options: {
auth: ["/api"],
sourceDir: [
// {
// dir: path.resolve(sourceDir, "route/api"),
// prefix: "api"
// },
{
dir: path.resolve(sourceDir, "route/htmx"),
prefix: "htmx",
},
{
dir: path.resolve(sourceDir, "route/views"),
prefix: "",
},
],
type: "session",
}, },
// stream: fs.createWriteStream(path.resolve(baseDir, "./log/pino.log"), { encoding: "utf-8" })
// prettyPrint: process.env.NODE_ENV !== 'production',
// Redact Authorization headers, see https://getpino.io/#/docs/redaction
// redact: ['req.headers.cookie']
}
},
{
plugin: filePlugin as unknown as Plugin<any>,
},
{
plugin: routePlugin as Plugin<any>,
options: {
auth: ['/api'],
sourceDir: [
// {
// dir: path.resolve(sourceDir, "route/api"),
// prefix: "api"
// },
{
dir: path.resolve(sourceDir, "route/htmx"),
prefix: "htmx"
},
{
dir: path.resolve(sourceDir, "route/views"),
prefix: ""
},
],
type: "session"
}, },
}, {
{ plugin: {
plugin: { name: "flash",
name: "flash", version: "0.0.1",
version: "0.0.1", // https://github.com/hks-epod/paydash/blob/master/lib/flash.js
// https://github.com/hks-epod/paydash/blob/master/lib/flash.js register: function (server: Server, options) {
register: function (server: Server, options) { server.ext("onPreResponse", async function (request: Request, h) {
server.ext('onPreResponse', async function(request: Request, h) { // @ts-ignore
// @ts-ignore if (request.response.variety === "file") return h.continue // 返回文件时不处理
if(request.response.variety === "file") return h.continue; // 返回文件时不处理 if (request.path.startsWith("/api") || request.path.startsWith("/htmx")) return h.continue
if(request.path.startsWith("/api") || request.path.startsWith("/htmx")) return h.continue; // 需要设置auth是try或者true才行
// 需要设置auth是try或者true才行 const isLogin = request.auth.isAuthenticated
const isLogin = request.auth.isAuthenticated; loggerSite.debug(`是否登录:${isLogin}, 请求路径:${request.path}, 请求方法:${request.method}`)
loggerSite.debug(`是否登录:${isLogin}, 请求路径:${request.path}, 请求方法:${request.method}`)
// @ts-ignore
// console.log(isLogin, request.path, request.response.variety);
// let user;
// if(isLogin){
// const { id } = request.auth.credentials;
// const User = request.getModel("User")
// user = <any>await User.findOne({ where: { id: id } });
// user = user.toJSON();
// delete user.password;
// }
// @ts-ignore
if (request.yar && request.yar.flash && request.response.variety === 'view') {
var flash = request.yar.flash();
request.yar.set('_flash', {});
// @ts-ignore // @ts-ignore
request.response.source.context = Hoek.applyToDefaults( // console.log(isLogin, request.path, request.response.variety);
{ // let user;
flash: !!Object.keys(flash).length?flash:false, // if(isLogin){
isLogin: isLogin, // const { id } = request.auth.credentials;
user: isLogin?request.auth.credentials:false, // const User = request.getModel("User")
}, // user = <any>await User.findOne({ where: { id: id } });
// @ts-ignore // user = user.toJSON();
request.response.source.context // delete user.password;
); // }
// @ts-ignore // @ts-ignore
if (request.yar && request.yar.flash && request.response.variety === "view") {
var flash = request.yar.flash()
request.yar.set("_flash", {})
// @ts-ignore
request.response.source.context = Hoek.applyToDefaults(
{
flash: !!Object.keys(flash).length ? flash : false,
isLogin: isLogin,
user: isLogin ? request.auth.credentials : false,
},
// @ts-ignore
request.response.source.context,
)
// @ts-ignore
}
return h.continue
})
},
} as Plugin<any>,
},
{
plugin: HapiYar,
options: {
name: "yar",
storeBlank: false,
cookieOptions: {
password: "dsRhw1Y5UZqB8SjfClbkrX9PF7yuDMV3JItcW0G4vgpaxONo6mzenHLQET2AiKyPUjjdgjo10amjfghy",
path: "/",
isSecure: false,
},
},
},
{
plugin: HapiCrumb,
options: {
autoGenerate: true,
logUnauthorized: true,
skip: function (request, reply) {
// 流的话直接通过,不需要验证,主要用于传递form-data数据时这里通不过
if (request.payload instanceof Stream) {
return true
} }
return h.continue; return false
}); },
} cookieOptions: {
} as Plugin<any> path: "/",
}, isSecure: false,
{ },
plugin: HapiYar,
options: {
name: "yar",
storeBlank: false,
cookieOptions: {
password: "dsRhw1Y5UZqB8SjfClbkrX9PF7yuDMV3JItcW0G4vgpaxONo6mzenHLQET2AiKyPUjjdgjo10amjfghy",
path: '/',
isSecure: false
}
}
},
{
plugin: HapiCrumb,
options: {
autoGenerate: true,
logUnauthorized: true,
skip: function(request, reply) {
// 流的话直接通过,不需要验证,主要用于传递form-data数据时这里通不过
if(request.payload instanceof Stream){
return true
}
return false;
}, },
cookieOptions: { },
path: '/', ] as unknown as ServerRegisterPluginObject<any>
isSecure: false
}
}
},
] as unknown as ServerRegisterPluginObject<any>;

20
source/route/api/index.ts

@ -1,13 +1,13 @@
import { Req, Res, ReturnValue } from "#/global"; import { Req, Res, ReturnValue } from "#/global"
import { auth, route } from "@noderun/hapi-router"; import { auth, route } from "@noderun/hapi-router"
export default class { export default class {
@route("/{path*}") @route("/{path*}")
@auth(false) @auth(false)
async any(req: Req, h: Res): ReturnValue { async any(req: Req, h: Res): ReturnValue {
return { return {
code: 404, code: 404,
data: null, data: null,
}; }
} }
} }

143
source/route/api/v1/upload/_upload.ts

@ -1,83 +1,82 @@
import path from "path"; import path from "path"
import { gSuccess, gFail, uploadDir, uploadPath } from "@/util"; import { gSuccess, gFail, uploadDir, uploadPath } from "@/util"
import { dateTimeFormat } from "@/util/util" import { dateTimeFormat } from "@/util/util"
const fs = require("fs"); const fs = require("fs")
const multiparty = require("multiparty"); const multiparty = require("multiparty")
const FileType = require("file-type"); const FileType = require("file-type")
function saveFile(file) { function saveFile(file) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const filename = file.originalFilename; const filename = file.originalFilename
const uploadedPath = file.path; const uploadedPath = file.path
const filetype = await FileType.fromFile(uploadedPath); const filetype = await FileType.fromFile(uploadedPath)
const _file = path.parse(filename); const _file = path.parse(filename)
if (filetype && (filetype.ext == "jpg" || filetype.ext == "png")) { if (filetype && (filetype.ext == "jpg" || filetype.ext == "png")) {
let _name = _file.name + "_" + dateTimeFormat(new Date(), "yyyy_MM_dd") + "_" + new Date().getTime() + _file.ext let _name =
const dstPath = path.resolve(uploadDir, _name); _file.name + "_" + dateTimeFormat(new Date(), "yyyy_MM_dd") + "_" + new Date().getTime() + _file.ext
fs.rename(uploadedPath, dstPath, function (err) { const dstPath = path.resolve(uploadDir, _name)
if (err) { fs.rename(uploadedPath, dstPath, function (err) {
console.log("rename error: " + err); if (err) {
reject(); console.log("rename error: " + err)
reject()
} else {
resolve(dstPath)
}
})
} else { } else {
resolve(dstPath); fs.unlinkSync(uploadedPath)
reject(new Error(filename + "文件不是图片"))
} }
}); })
} else {
fs.unlinkSync(uploadedPath);
reject(new Error(filename + "文件不是图片"));
}
});
} }
export default function (req, h) { export default function (req, h) {
const form = new multiparty.Form({ const form = new multiparty.Form({
uploadDir: uploadDir, //路径需要对应自己的项目更改 uploadDir: uploadDir, //路径需要对应自己的项目更改
/*设置文件保存路径 */ /*设置文件保存路径 */
encoding: "utf-8", encoding: "utf-8",
/*编码设置 */ /*编码设置 */
maxFilesSize: 20000 * 1024 * 1024, maxFilesSize: 20000 * 1024 * 1024,
/*设置文件最大值 20MB */ /*设置文件最大值 20MB */
keepExtensions: true, keepExtensions: true,
/*保留后缀*/ /*保留后缀*/
}); })
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
form.on("part", function (part) { form.on("part", function (part) {
console.log(part.filename); console.log(part.filename)
}); })
form.on("progress", function (bytesReceived, bytesExpected) { form.on("progress", function (bytesReceived, bytesExpected) {
if (bytesExpected === null) { if (bytesExpected === null) {
return; return
} }
var percentComplete = (bytesReceived / bytesExpected) * 100; var percentComplete = (bytesReceived / bytesExpected) * 100
console.log( console.log("the form is " + Math.floor(percentComplete) + "%" + " complete")
"the form is " + Math.floor(percentComplete) + "%" + " complete" })
); form.parse(req.payload, async function (err, fields, files) {
}); // console.log(err, fields, files);
form.parse(req.payload, async function (err, fields, files) {
// console.log(err, fields, files);
if (err) { if (err) {
resolve(err.message); resolve(err.message)
return; return
} }
const errList = []; const errList = []
const fileList = []; const fileList = []
for (let i = 0; i < files.file.length; i++) { for (let i = 0; i < files.file.length; i++) {
const file = files.file[i]; const file = files.file[i]
try { try {
const dstPath = await saveFile(file); const dstPath = await saveFile(file)
fileList.push(dstPath); fileList.push(dstPath)
} catch (error) { } catch (error) {
errList.push(error.message); errList.push(error.message)
} }
} }
if (errList.length) { if (errList.length) {
resolve(gFail(null, errList.join("\n"))); resolve(gFail(null, errList.join("\n")))
return; return
} }
// resolve(h.view("views/upload.ejs")); // resolve(h.view("views/upload.ejs"));
resolve([...new Set(fileList)]); resolve([...new Set(fileList)])
}); })
}); })
} }

48
source/route/api/v1/upload/index.ts

@ -1,29 +1,27 @@
import { config, method, route, swagger, validate } from "@noderun/hapi-router"; import { config, method, route, swagger, validate } from "@noderun/hapi-router"
import UploadFunc from "./_upload"; import UploadFunc from "./_upload"
import Joi from "joi"; import Joi from "joi"
export default class { export default class {
index(request, h) { index(request, h) {
return h.view("views/demo.ejs"); return h.view("views/demo.ejs")
} }
@config({ @config({
payload: { payload: {
maxBytes: 20000 * 1024 * 1024, maxBytes: 20000 * 1024 * 1024,
output: "stream", output: "stream",
parse: false, parse: false,
// multipart: true, // multipart: true,
allow: "multipart/form-data", allow: "multipart/form-data",
},
})
@method("POST")
@swagger("文件上传", "文件上传a ", ["sum", "api"])
async upload(req, h) {
const startTime = new Date().getTime()
const res = await UploadFunc(req, h)
const endTime = new Date().getTime()
console.log(`该请求处理时间为:${Number(endTime - startTime).toFixed(2)}ms`)
return res
} }
})
@method("POST")
@swagger("文件上传", "文件上传a ", ["sum", "api"])
async upload(req, h) {
const startTime = new Date().getTime();
const res = await UploadFunc(req, h);
const endTime = new Date().getTime();
console.log(
`该请求处理时间为:${Number(endTime - startTime).toFixed(2)}ms`
);
return res;
}
} }

176
source/route/api/v1/user/index.ts

@ -1,102 +1,96 @@
import { import { auth, method, route, swagger, validate, config } from "@noderun/hapi-router"
auth, import { gSuccess, gFail } from "@/util"
method, import * as bcrypt from "bcrypt"
route, import * as jwt from "jsonwebtoken"
swagger, import * as Joi from "joi"
validate, import { UserSchema } from "@/schema"
config, import { ReturnValue, Req, Res } from "#/global"
} from "@noderun/hapi-router";
import { gSuccess, gFail } from "@/util";
import * as bcrypt from "bcrypt";
import * as jwt from "jsonwebtoken";
import * as Joi from "joi";
import { UserSchema } from "@/schema";
import { ReturnValue, Req, Res } from "#/global";
export default class { export default class {
@validate({ @validate({
payload: UserSchema, payload: UserSchema,
}) })
@method("POST") @method("POST")
@swagger("用户注册", "返回注册用户的信息", ["api"]) @swagger("用户注册", "返回注册用户的信息", ["api"])
@auth(false) @auth(false)
async register(request: Req, h: Res): ReturnValue { async register(request: Req, h: Res): ReturnValue {
let { username, password, email } = request.payload as any; let { username, password, email, } = request.payload as any
if (!username) username = email; if (!username) username = email
const User = request.getModel("User") const User = request.getModel("User")
logger.trace(username, email); logger.trace(username, email)
try { try {
const result = await User.findOne({ where: { username: username } }); const result = await User.findOne({ where: { username: username } })
if (result != null) { if (result != null) {
return gFail(null, "已存在该用户"); return gFail(null, "已存在该用户")
} }
let salt = bcrypt.genSaltSync(10); let salt = bcrypt.genSaltSync(10)
let pwdLock = bcrypt.hashSync(password, salt); let pwdLock = bcrypt.hashSync(password, salt)
await User.create({ username, password: pwdLock, email }); // @ts-ignore
return gSuccess("success", "you have a good heart."); await User.create({ username, password: pwdLock, email })
} catch (e) { return gSuccess("success", "you have a good heart.")
return gFail(null, "新建用户失败"); } catch (e) {
return gFail(null, "新建用户失败")
}
} }
}
@method("POST") @method("POST")
async logout(request: Req, h: Res): ReturnValue { async logout(request: Req, h: Res): ReturnValue {
request.cookieAuth.clear(); request.cookieAuth.clear()
return gSuccess("success"); return gSuccess("success")
}
@validate({
payload: UserSchema,
})
@auth(false)
@method("POST")
@swagger("用户登录", "返回注册用户的信息", ["api"])
async login(request: Req, h: Res): ReturnValue {
let { username, password } = request.payload as any;
const User = request.getModel("User")
const result = <any>await User.findOne({ where: { username: username } });
if (result == null) {
return gFail(null, "不存在该用户");
} }
const validUser = bcrypt.compareSync(password, result.password);
if (!validUser) { @validate({
return gFail(null, "密码不正确"); payload: UserSchema,
})
@auth(false)
@method("POST")
@swagger("用户登录", "返回注册用户的信息", ["api"])
async login(request: Req, h: Res): ReturnValue {
let { username, password } = request.payload as any
const User = request.getModel("User")
const result = <any>await User.findOne({ where: { username: username } })
if (result == null) {
return gFail(null, "不存在该用户")
}
const validUser = bcrypt.compareSync(password, result.password)
if (!validUser) {
return gFail(null, "密码不正确")
}
//===== JWT ===== Start
// let token = jwt.sign({ id: result.id }, process.env.KEY);
// return gSuccess({ token: token });
//===== JWT ===== End
//===== session ===== Start
request.cookieAuth.set({ id: result.id })
//===== session ===== End
return gSuccess({})
} }
//===== JWT ===== Start
// let token = jwt.sign({ id: result.id }, process.env.KEY);
// return gSuccess({ token: token });
//===== JWT ===== End
//===== session ===== Start
request.cookieAuth.set({ id: result.id });
//===== session ===== End
return gSuccess({});
}
@method("DELETE") @method("DELETE")
@auth() @auth()
@swagger("删除用户", "删除用户账号", ["sum"]) @swagger("删除用户", "删除用户账号", ["sum"])
async del(request: Req, h: Res): ReturnValue { async del(request: Req, h: Res): ReturnValue {
const { id } = request.auth.credentials; const { id } = request.auth.credentials
const User = request.getModel("User") const User = request.getModel("User")
let result = await User.findOne({ where: { id: id } }); let result = await User.findOne({ where: { id: id } })
if (result == null) { if (result == null) {
return gFail(null, "不存在该用户"); return gFail(null, "不存在该用户")
}
await result.destroy()
return gSuccess(null, "删除成功")
} }
await result.destroy();
return gSuccess(null, "删除成功");
}
@method("GET") @method("GET")
@swagger("获取用户信息", "返回注册用户的信息", ["用户操作", "api"]) @swagger("获取用户信息", "返回注册用户的信息", ["用户操作", "api"])
async userinfo(request: Req, h: Res): ReturnValue { async userinfo(request: Req, h: Res): ReturnValue {
const { id } = request.auth.credentials; const { id } = request.auth.credentials
const User = request.getModel("User") const User = request.getModel("User")
let result = <any>await User.findOne({ where: { id: id } }); let result = <any>await User.findOne({ where: { id: id } })
if (result == null) { if (result == null) {
return gFail(null, "不存在该用户"); return gFail(null, "不存在该用户")
}
result = result.toJSON()
delete result.password
return gSuccess(result)
} }
result = result.toJSON();
delete result.password;
return gSuccess(result);
}
} }

2
source/route/htmx/index.ts

@ -8,6 +8,6 @@ export default class {
@route("/clicked") @route("/clicked")
async clicked(request: Req, h: Res): ReturnValue { async clicked(request: Req, h: Res): ReturnValue {
// return 'aaaaaaaaaaaaaabbbbbbbbbbbbbbbb' // return 'aaaaaaaaaaaaaabbbbbbbbbbbbbbbb'
return h.view('htmx/a.pug') return h.view("htmx/a.pug")
} }
} }

164
source/route/views/index.ts

@ -1,96 +1,96 @@
import { Req, Res, ReturnValue } from "#/global"; import { Req, Res, ReturnValue } from "#/global"
import { LoginUserSchema, RegisterUserSchema, UserSchema } from "@/schema"; import { LoginUserSchema, RegisterUserSchema, UserSchema } from "@/schema"
import { gFail, gSuccess } from "@/util"; import { gFail, gSuccess } from "@/util"
import { auth, config, method, route, validate } from "@noderun/hapi-router"; import { auth, config, method, route, validate } from "@noderun/hapi-router"
import * as bcrypt from "bcrypt"; import * as bcrypt from "bcrypt"
/** /**
* *
*/ */
export default class { export default class {
@route("/login") @route("/login")
@auth("try") @auth("try")
@method("GET") @method("GET")
async login_GET(request: Req, h: Res): ReturnValue { async login_GET(request: Req, h: Res): ReturnValue {
if (request.auth.isAuthenticated) { if (request.auth.isAuthenticated) {
request.yar.flash('warning', '您已经登陆'); request.yar.flash("warning", "您已经登陆")
return h.redirect("/") return h.redirect("/")
} else { } else {
logger.debug("未登录"); logger.debug("未登录")
}
return h.view("views/login.pug")
} }
return h.view("views/login.pug");
}
@validate({ @validate({
payload: LoginUserSchema, payload: LoginUserSchema,
$errto: '/login', $errto: "/login",
// failAction: 'log' // failAction: 'log'
failAction: 'function', failAction: "function",
failReason: '用户名或密码错误,请重试', failReason: "用户名或密码错误,请重试",
}) })
@method("POST") @method("POST")
@route("/login") @route("/login")
async login_POST(request: Req, h: Res): ReturnValue { async login_POST(request: Req, h: Res): ReturnValue {
const { username, password, referrer } = request.payload as any; const { username, password, referrer } = request.payload as any
const User = request.getModel("User"); const User = request.getModel("User")
const account = <any>await User.findOne({ where: { username: username } }); const account = <any>await User.findOne({ where: { username: username } })
if (!account || !(await bcrypt.compare(password, account.password))) { if (!account || !(await bcrypt.compare(password, account.password))) {
request.yar.flash('error', 'Invalid username or password'); request.yar.flash("error", "Invalid username or password")
return h.redirect("/login"); return h.redirect("/login")
}
request.cookieAuth.set({ id: account.id, nickname: account.nickname })
request.yar.flash("success", "用户已登录")
return h.redirect(referrer ? referrer : "/")
} }
request.cookieAuth.set({ id: account.id, nickname: account.nickname });
request.yar.flash('success', '用户已登录');
return h.redirect(referrer ? referrer : "/");
}
@method("GET") @method("GET")
@auth() @auth()
async logout(request: Req, h: Res): ReturnValue { async logout(request: Req, h: Res): ReturnValue {
request.yar.flash('success', '用户已退出'); request.yar.flash("success", "用户已退出")
request.cookieAuth.clear(); request.cookieAuth.clear()
return h.redirect('/'); return h.redirect("/")
}
@route("/register")
@auth("try")
@method("GET")
async registerView(request: Req, h: Res): ReturnValue {
if (request.auth.isAuthenticated) {
request.yar.flash('warning', '您已经登陆');
return h.redirect("/")
} else {
logger.debug("未登录");
} }
return h.view("views/login.pug");
}
@validate({ @route("/register")
payload: RegisterUserSchema, @auth("try")
}) @method("GET")
@method("POST") async registerView(request: Req, h: Res): ReturnValue {
async register(request: Req, h: Res): ReturnValue { if (request.auth.isAuthenticated) {
let { username, password, email, nickname } = request.payload as any; request.yar.flash("warning", "您已经登陆")
if(!email){ return h.redirect("/")
request.yar.flash('error', '必须填写邮箱'); } else {
return h.redirect("/login"); logger.debug("未登录")
}
return h.view("views/login.pug")
} }
if (!username) username = email;
if (!nickname) nickname = username; @validate({
const User = request.getModel("User") payload: RegisterUserSchema,
logger.trace(username, email); })
try { @method("POST")
const result = await User.findOne({ where: { username: username } }); async register(request: Req, h: Res): ReturnValue {
if (result != null) { let { username, password, email, nickname } = request.payload as any
request.yar.flash('error', '已存在该用户'); if (!email) {
return h.redirect("/login"); request.yar.flash("error", "必须填写邮箱")
} return h.redirect("/login")
let salt = bcrypt.genSaltSync(10); }
let pwdLock = bcrypt.hashSync(password, salt); if (!username) username = email
await User.create({ username, nickname, password: pwdLock, email }); if (!nickname) nickname = username
return h.redirect("/") const User = request.getModel("User")
} catch (e) { logger.trace(username, email)
request.yar.flash('error', '注册用户失败'); try {
return h.redirect("/login"); const result = await User.findOne({ where: { username: username } })
if (result != null) {
request.yar.flash("error", "已存在该用户")
return h.redirect("/login")
}
let salt = bcrypt.genSaltSync(10)
let pwdLock = bcrypt.hashSync(password, salt)
await User.create({ username, nickname, password: pwdLock, email })
return h.redirect("/")
} catch (e) {
request.yar.flash("error", "注册用户失败")
return h.redirect("/login")
}
} }
}
} }

165
source/route/views/index/index.ts

@ -1,100 +1,93 @@
import { import { auth, config, method, route, swagger, validate } from "@noderun/hapi-router"
auth, import { Req, Res, ReturnValue } from "#/global"
config, import path from "path"
method, import fs from "fs-extra"
route, import { baseDir } from "@/util"
swagger, import MarkdownIt from "markdown-it"
validate,
} from "@noderun/hapi-router";
import { Req, Res, ReturnValue } from "#/global";
import path from "path";
import fs from "fs-extra";
import { baseDir } from "@/util";
import MarkdownIt from "markdown-it";
export default class Index { export default class Index {
async css(request: Req, h: Res): ReturnValue { async css(request: Req, h: Res): ReturnValue {
return h.view("views/css.pug"); return h.view("views/css.pug")
} }
@auth("try") @auth("try")
async index(request: Req, h: Res): ReturnValue { async index(request: Req, h: Res): ReturnValue {
if (request.auth.isAuthenticated) { if (request.auth.isAuthenticated) {
// 登录了 // 登录了
} else { } else {
// 未登录 // 未登录
}
return h.view("views/index.pug", { isLogin: request.auth.isAuthenticated })
} }
return h.view("views/index.pug", { isLogin: request.auth.isAuthenticated });
}
@route("/about") @route("/about")
@auth("try") @auth("try")
async about(request: Req, h) { async about(request: Req, h) {
// console.log(request.auth); // console.log(request.auth);
// console.log(1); // console.log(1);
// try { // try {
// const User = request.getModel("User"); // const User = request.getModel("User");
// console.log(await User.findOne({ where: { username: "xieyaxin" } })); // console.log(await User.findOne({ where: { username: "xieyaxin" } }));
// } catch (error) { // } catch (error) {
// console.log(error); // console.log(error);
// } // }
// console.log(2); // console.log(2);
const md = new MarkdownIt(); const md = new MarkdownIt()
var result = md.render('# markdown-it rulezz!'); var result = md.render("# markdown-it rulezz!")
return h.view("views/about.pug", { return h.view("views/about.pug", {
md: result md: result,
}); })
} }
@route("/docs/{path*}") @route("/docs/{path*}")
@auth() @auth()
async docs(req: Req, h: Res): ReturnValue { async docs(req: Req, h: Res): ReturnValue {
// const {id} = req.auth.credentials // const {id} = req.auth.credentials
// try { // try {
// req.cookieAuth.ttl(720 * 24 * 60 * 60 * 1000) // req.cookieAuth.ttl(720 * 24 * 60 * 60 * 1000)
// req.cookieAuth.set({ id: id }); // req.cookieAuth.set({ id: id });
// } catch (error) { // } catch (error) {
// console.log(error); // console.log(error);
// } // }
if (req.params && req.params.path.endsWith(".md")) { if (req.params && req.params.path.endsWith(".md")) {
// console.log(path.resolve(baseDir, "docs/"+"*.md")); // console.log(path.resolve(baseDir, "docs/"+"*.md"));
// console.log(await glob("docs/"+"*.md")); // console.log(await glob("docs/"+"*.md"));
const mdPath = path.resolve(baseDir, "docs/"+req.params.path) const mdPath = path.resolve(baseDir, "docs/" + req.params.path)
if(fs.existsSync(mdPath)){ if (fs.existsSync(mdPath)) {
const str = fs.readFileSync(mdPath, "utf8") const str = fs.readFileSync(mdPath, "utf8")
console.log("---->", mdPath); console.log("---->", mdPath)
return h.view("views/css.pug", { return h.view("views/css.pug", {
content: str.toString() content: str.toString(),
}); })
} }
// 解析文档 // 解析文档
return h.view("views/css.pug"); return h.view("views/css.pug")
}
// 404页面
return h.redirect("/404")
} }
// 404页面 @route("/{path*}")
return h.redirect("/404"); async any(req: Req, h: Res): ReturnValue {
} console.log("404: ", req.raw.req.url)
@route("/{path*}") return h.redirect("/404?r=" + encodeURIComponent(req.raw.req.url))
async any(req: Req, h: Res): ReturnValue {
console.log('404: ',req.raw.req.url);
return h.redirect("/404?r="+encodeURIComponent(req.raw.req.url));
}
@route("/404")
@auth("try")
async 404(request: Req, h: Res): ReturnValue {
if (request.auth.isAuthenticated) {
// 登录了
} else {
// 未登录
} }
if(request.query?.r){ @route("/404")
// 可重定向返回 @auth("try")
async 404(request: Req, h: Res): ReturnValue {
if (request.auth.isAuthenticated) {
// 登录了
} else {
// 未登录
}
if (request.query?.r) {
// 可重定向返回
}
return h.view("404.pug", {
rollback: request.query?.r,
})
} }
return h.view("404.pug", {
rollback: request.query?.r
});
}
} }

8
source/route/views/nav/index.ts

@ -1,9 +1,9 @@
import { Req, Res, ReturnValue } from "#/global"; import { Req, Res, ReturnValue } from "#/global"
import { gSuccess } from "@/util"; import { gSuccess } from "@/util"
export default class Nav { export default class Nav {
async index(req: Req, h: Res): ReturnValue{ async index(req: Req, h: Res): ReturnValue {
const Constant = req.getModel("Constant") // const Constant = req.getModel("Constant")
return gSuccess("31231") return gSuccess("31231")
} }
} }

262
source/run.ts

@ -1,146 +1,146 @@
"use strict"; "use strict"
import plugins from "@/plugins"; import plugins from "@/plugins"
import path from "path"; import path from "path"
import { baseDir, templateDir } from "@/util"; import { baseDir, templateDir } from "@/util"
import { validateJwt, validateSession } from "./auth"; import { validateJwt, validateSession } from "./auth"
import Hapi, { Server } from "@hapi/hapi"; import Hapi, { Server } from "@hapi/hapi"
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize"
import { Req } from "#/global"; import { Req } from "#/global"
// const Hapi = require("@hapi/hapi"); // const Hapi = require("@hapi/hapi");
// const HapiSwagger = require("hapi-swagger"); // const HapiSwagger = require("hapi-swagger");
// const HapiSwagger = require("hapi-swaggered-ui"); // swagger v2版本 // const HapiSwagger = require("hapi-swaggered-ui"); // swagger v2版本
const pugPluginAlias = require('pug-alias'); const pugPluginAlias = require("pug-alias")
const run = async (): Promise<Server> => { const run = async (): Promise<Server> => {
const server = Hapi.server({ const server = Hapi.server({
port: 3388, port: 3388,
host: "localhost", host: "localhost",
}); })
await server.register([ await server.register([
{
plugin: require("hapi-sequelizejs"),
options: [
{ {
name: "data", // identifier plugin: require("hapi-sequelizejs"),
models: [__dirname + "/models/**/*.ts"], // paths/globs to model files options: [
// ignoredModels: [__dirname + "/server/models/**/*.js"], // OPTIONAL: paths/globs to ignore files {
sequelize: new Sequelize({ name: "data", // identifier
dialect: "sqlite", models: [__dirname + "/models/**/*.ts"], // paths/globs to model files
storage: path.resolve(__dirname, "./db/data.db"), // ignoredModels: [__dirname + "/server/models/**/*.js"], // OPTIONAL: paths/globs to ignore files
logging: false, sequelize: new Sequelize({
// logging: loggerSQL.debug.bind(loggerSQL) // Alternative way to use custom logger, displays all messages dialect: "sqlite",
}), // sequelize instance storage: path.resolve(__dirname, "./db/data.db"),
sync: true, // sync models - default false logging: false,
forceSync: false, // force sync (drops tables) - default false // logging: loggerSQL.debug.bind(loggerSQL) // Alternative way to use custom logger, displays all messages
}), // sequelize instance
sync: true, // sync models - default false
forceSync: false, // force sync (drops tables) - default false
},
],
}, },
], ])
},
]);
//===== JWT ===== Start //===== JWT ===== Start
// await server.register(require("hapi-auth-jwt2")); // await server.register(require("hapi-auth-jwt2"));
// server.auth.strategy("jwt", "jwt", { // server.auth.strategy("jwt", "jwt", {
// key: process.env.KEY, // Never Share your secret key // key: process.env.KEY, // Never Share your secret key
// validate: validateJwt, // validate function defined above // validate: validateJwt, // validate function defined above
// verifyOptions: { algorithms: ["HS256"] }, // verifyOptions: { algorithms: ["HS256"] },
// }); // });
//===== JWT ===== End //===== JWT ===== End
//===== session ===== Start //===== session ===== Start
// https://hapi.dev/module/cookie/api?v=11.0.2 // https://hapi.dev/module/cookie/api?v=11.0.2
await server.register(require("@hapi/cookie")); await server.register(require("@hapi/cookie"))
server.auth.strategy("session", "cookie", { server.auth.strategy("session", "cookie", {
cookie: { cookie: {
ttl: 1000 * 60 * 60 * 24, ttl: 1000 * 60 * 60 * 24,
path: '/', // 测试退出时set-cookie失效,加上这个好了 path: "/", // 测试退出时set-cookie失效,加上这个好了
name: "sid", //cookie的名字 name: "sid", //cookie的名字
password: process.env.KEY, password: process.env.KEY,
isSecure: false, // false: 允许 Cookie 通过不安全的连接传输,这会使其受到攻击 isSecure: false, // false: 允许 Cookie 通过不安全的连接传输,这会使其受到攻击
}, },
redirectTo(request: Req){ redirectTo(request: Req) {
if (request.path.startsWith('/api')) { if (request.path.startsWith("/api")) {
return false return false
} }
return "/login" return "/login"
}, },
appendNext: true, appendNext: true,
validateFunc: validateSession, validateFunc: validateSession,
}); })
server.auth.default("session"); server.auth.default("session")
//===== session ===== End //===== session ===== End
await server.register(plugins as any);
/** await server.register(plugins as any)
*
*/
// https://hapi.dev/module/vision/api/?v=6.1.0
await server.register(require("@hapi/vision"));
server.views({
engines: {
ejs: require("ejs"),
pug: require("pug"),
},
isCached: process.env.NODE_ENV === "development" ? false : true,
compileMode: "sync", // ejs
relativeTo: baseDir,
layout: false, // ejs
layoutPath: path.resolve(templateDir, "layout"), // ejs
path: "template",
// pug
compileOptions: {
// By default Pug uses relative paths (e.g. ../root.pug), when using absolute paths (e.g. include /root.pug), basedir is prepended.
// https://pugjs.org/language/includes.html
basedir: templateDir,
plugins: [
pugPluginAlias({
// as Function
'@': fn => fn.replace(/^@/, 'template')
})
]
},
});
// http://localhost:3000/documentation /**
await server.register([ *
{ */
plugin: require("hapi-swagger"), // https://hapi.dev/module/vision/api/?v=6.1.0
options: { await server.register(require("@hapi/vision"))
documentationPath: "/doc", server.views({
info: { engines: {
title: "Dream 文档", ejs: require("ejs"),
version: "1.0.0", pug: require("pug"),
}, },
grouping: "tags", isCached: process.env.NODE_ENV === "development" ? false : true,
tags: [ compileMode: "sync", // ejs
{ relativeTo: baseDir,
name: "sum", layout: false, // ejs
description: "working with maths", layoutPath: path.resolve(templateDir, "layout"), // ejs
externalDocs: { path: "template",
description: "Find out more", // pug
url: "http://example.org", compileOptions: {
}, // By default Pug uses relative paths (e.g. ../root.pug), when using absolute paths (e.g. include /root.pug), basedir is prepended.
}, // https://pugjs.org/language/includes.html
{ basedir: templateDir,
name: "store", plugins: [
description: "storing data", pugPluginAlias({
externalDocs: { // as Function
description: "Find out more", "@": fn => fn.replace(/^@/, "template"),
url: "http://example.org", }),
],
},
})
// http://localhost:3000/documentation
await server.register([
{
plugin: require("hapi-swagger"),
options: {
documentationPath: "/doc",
info: {
title: "Dream 文档",
version: "1.0.0",
},
grouping: "tags",
tags: [
{
name: "sum",
description: "working with maths",
externalDocs: {
description: "Find out more",
url: "http://example.org",
},
},
{
name: "store",
description: "storing data",
externalDocs: {
description: "Find out more",
url: "http://example.org",
},
},
],
}, },
}, },
], ])
}, await server.start()
}, logger.trace("Server running on %s", server.info.uri)
]); return server
await server.start(); }
logger.trace("Server running on %s", server.info.uri);
return server;
};
process.on("unhandledRejection", (err) => { process.on("unhandledRejection", err => {
console.log("unhandledRejection:", err); console.log("unhandledRejection:", err)
process.exit(1); process.exit(1)
}); })
export { run }; export { run }

34
source/schema/index.ts

@ -1,30 +1,32 @@
import * as Joi from "joi"; import * as Joi from "joi"
export const UserSchema = Joi.object({ export const UserSchema = Joi.object({
username: Joi.string().alphanum().min(6).max(35), username: Joi.string().alphanum().min(6).max(35),
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")).required(), password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")).required(),
email: Joi.string().email({ email: Joi.string().email({
minDomainSegments: 2, minDomainSegments: 2,
tlds: { allow: ["com", "net"] }, tlds: { allow: ["com", "net"] },
}) }),
}).or("username", "email"); }).or("username", "email")
export const RegisterUserSchema = Joi.object({ export const RegisterUserSchema = Joi.object({
username: Joi.string().alphanum().min(6).max(35), username: Joi.string().alphanum().min(6).max(35),
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")).required(), password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")).required(),
email: Joi.string().email({ email: Joi.string()
minDomainSegments: 2, .email({
tlds: { allow: ["com", "net"] }, minDomainSegments: 2,
}).required(), tlds: { allow: ["com", "net"] },
})
.required(),
nickname: Joi.string().alphanum().min(4).max(35), nickname: Joi.string().alphanum().min(4).max(35),
}) })
export const LoginUserSchema = Joi.object({ export const LoginUserSchema = Joi.object({
referrer: Joi.string().allow('').optional(), referrer: Joi.string().allow("").optional(),
username: Joi.string().min(6).max(35), //Joi.string().alphanum().min(6).max(35) username: Joi.string().min(6).max(35), //Joi.string().alphanum().min(6).max(35)
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")).required(), password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")).required(),
email: Joi.string().email({ email: Joi.string().email({
minDomainSegments: 2, minDomainSegments: 2,
tlds: { allow: ["com", "net"] }, tlds: { allow: ["com", "net"] },
}), }),
}).or("username", "email"); }).or("username", "email")

16
source/util/index.ts

@ -1,13 +1,13 @@
import path from "path"; import path from "path"
export * from "./res-helper"; export * from "./res-helper"
export const isDev = process.env.NODE_ENV === "development" export const isDev = process.env.NODE_ENV === "development"
export const isProd = process.env.NODE_ENV === "production" export const isProd = process.env.NODE_ENV === "production"
export const baseDir = path.resolve(__dirname, "../../"); export const baseDir = path.resolve(__dirname, "../../")
export const sourceDir = isProd? path.resolve(__dirname, "../../dist") : path.resolve(__dirname, "../../source"); export const sourceDir = isProd ? path.resolve(__dirname, "../../dist") : path.resolve(__dirname, "../../source")
export const publicDir = path.resolve(__dirname, "../../public"); export const publicDir = path.resolve(__dirname, "../../public")
export const uploadDir = path.resolve(publicDir, "upload"); export const uploadDir = path.resolve(publicDir, "upload")
export const uploadPath = "/public/upload"; // 图片上传地址 export const uploadPath = "/public/upload" // 图片上传地址
export const templateDir = path.resolve(baseDir, "template"); export const templateDir = path.resolve(baseDir, "template")

44
source/util/res-helper.ts

@ -1,29 +1,29 @@
export function gSuccess(data = null, message = "success") { export function gSuccess(data = null, message = "success") {
if (typeof data === "string") { if (typeof data === "string") {
return {
code: 200,
message: data,
data: null,
}
}
return { return {
code: 200, code: 200,
message: data, message: message,
data: null, data: data,
}; }
}
return {
code: 200,
message: message,
data: data,
};
} }
export function gFail(data = null, message = "fail") { export function gFail(data = null, message = "fail") {
if (typeof data === "string") { if (typeof data === "string") {
return {
code: 400,
message: data,
data: null,
}
}
return { return {
code: 400, code: 400,
message: data, message: message,
data: null, data: data,
}; }
}
return {
code: 400,
message: message,
data: data,
};
} }

60
source/util/util.ts

@ -1,43 +1,47 @@
export function dateTimeFormat(date, fmt = 'yyyy-MM-dd HH:mm:ss') { //日期时间格式化 export function dateTimeFormat(date, fmt = "yyyy-MM-dd HH:mm:ss") {
//日期时间格式化
if (!date) { if (!date) {
return '' return ""
} }
if (typeof date === 'string') { if (typeof date === "string") {
date = date.replace('T', ' ').replace('Z', ''); date = date.replace("T", " ").replace("Z", "")
date = new Date(date.replace(/-/g, '/')) date = new Date(date.replace(/-/g, "/"))
} }
if (typeof date === 'number') { if (typeof date === "number") {
date = new Date(date) date = new Date(date)
} }
var o = { var o = {
'M+': date.getMonth() + 1, "M+": date.getMonth() + 1,
'd+': date.getDate(), "d+": date.getDate(),
'h+': date.getHours() % 12 === 0 ? 12 : date.getHours() % 12, "h+": date.getHours() % 12 === 0 ? 12 : date.getHours() % 12,
'H+': date.getHours(), "H+": date.getHours(),
'm+': date.getMinutes(), "m+": date.getMinutes(),
's+': date.getSeconds(), "s+": date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3), "q+": Math.floor((date.getMonth() + 3) / 3),
'S': date.getMilliseconds() S: date.getMilliseconds(),
} }
var week = { var week = {
'0': '\u65e5', "0": "\u65e5",
'1': '\u4e00', "1": "\u4e00",
'2': '\u4e8c', "2": "\u4e8c",
'3': '\u4e09', "3": "\u4e09",
'4': '\u56db', "4": "\u56db",
'5': '\u4e94', "5": "\u4e94",
'6': '\u516d' "6": "\u516d",
} }
if (/(y+)/.test(fmt)) { if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length))
} }
if (/(E+)/.test(fmt)) { if (/(E+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, ((RegExp.$1.length > 1) ? (RegExp.$1.length > 2 ? '\u661f\u671f' : '\u5468') : '') + week[date.getDay() + '']) fmt = fmt.replace(
RegExp.$1,
(RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? "\u661f\u671f" : "\u5468") : "") + week[date.getDay() + ""],
)
} }
for (var k in o) { for (var k in o) {
if (new RegExp('(' + k + ')').test(fmt)) { if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length))) fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length))
} }
} }
return fmt return fmt
} }

8
template/404.pug

@ -1,9 +1,9 @@
extends layout/layout extends layout/layout
block head block head
link(rel="stylesheet", href="/public/css/views/404.css") link(rel="stylesheet", href="/public/css/views/404.css")
block content block content
div(style="text-align: center") div(style="text-align: center")
span.text404 404 span.text404 404
div 重定向回:#{rollback} div 重定向回:#{rollback}

120
template/helper/flush.pug

@ -1,62 +1,62 @@
//- 服务器反馈UI //- 服务器反馈UI
if flash if flash
.message-container .message-container
- index = 0 - index = 0
if flash.error if flash.error
each item in flash.error each item in flash.error
- index++ - index++
.message.is-danger(id="message"+index) .message.is-danger(id="message"+index)
.message-header .message-header
p 错误 p 错误
button.delete(aria-label='delete' data-target="message"+index) button.delete(aria-label='delete' data-target="message"+index)
.message-body .message-body
| #{item} | #{item}
if flash.success if flash.success
each item in flash.success each item in flash.success
- index++ - index++
.message.is-success(id="message"+index) .message.is-success(id="message"+index)
.message-header .message-header
p 成功 p 成功
button.delete(aria-label='delete' data-target="message"+index) button.delete(aria-label='delete' data-target="message"+index)
.message-body .message-body
| #{item} | #{item}
if flash.info if flash.info
each item in flash.info each item in flash.info
- index++ - index++
.message.is-info(id="message"+index) .message.is-info(id="message"+index)
.message-header .message-header
p 信息 p 信息
button.delete(aria-label='delete' data-target="message"+index) button.delete(aria-label='delete' data-target="message"+index)
.message-body .message-body
| #{item} | #{item}
if flash.warning if flash.warning
each item in flash.warning each item in flash.warning
- index++ - index++
.message.is-warning(id="message"+index) .message.is-warning(id="message"+index)
.message-header .message-header
p 警告 p 警告
button.delete(aria-label='delete' data-target="message"+index) button.delete(aria-label='delete' data-target="message"+index)
.message-body .message-body
| #{item} | #{item}
//- .toast-container.top-0.end-0.p-3 //- .toast-container.top-0.end-0.p-3
//- each item in flash.error //- each item in flash.error
//- .toast.show(role='alert', aria-live='assertive', aria-atomic='true') //- .toast.show(role='alert', aria-live='assertive', aria-atomic='true')
//- .toast-header //- .toast-header
//- img.rounded.me-2(src='/public/image/icons/error.svg', alt='错误' style="width:20px;height: 20px;") //- img.rounded.me-2(src='/public/image/icons/error.svg', alt='错误' style="width:20px;height: 20px;")
//- strong.me-auto 提示 //- strong.me-auto 提示
//- //- small.text-muted just now //- //- small.text-muted just now
//- button.btn-close(type='button', data-bs-dismiss='toast', aria-label='Close') //- button.btn-close(type='button', data-bs-dismiss='toast', aria-label='Close')
//- .toast-body. //- .toast-body.
//- #{item} //- #{item}
//- .toast-container.position-fixed.bottom-0.end-0.p-3 //- .toast-container.position-fixed.bottom-0.end-0.p-3
//- #liveToast.toast(role='alert', aria-live='assertive', aria-atomic='true') //- #liveToast.toast(role='alert', aria-live='assertive', aria-atomic='true')
//- .toast-header //- .toast-header
//- img.rounded.me-2(src='...', alt='...') //- img.rounded.me-2(src='...', alt='...')
//- strong.me-auto Bootstrap //- strong.me-auto Bootstrap
//- small 11 mins ago //- small 11 mins ago
//- button.btn-close(type='button', data-bs-dismiss='toast', aria-label='Close') //- button.btn-close(type='button', data-bs-dismiss='toast', aria-label='Close')
//- .toast-body. //- .toast-body.
//- Hello, world! This is a toast message. //- Hello, world! This is a toast message.
//- ul //- ul
//- each item in flash.error //- each item in flash.error
//- li #{item} //- li #{item}

79
template/ui/header.pug

@ -1,39 +1,44 @@
nav.is-fixed-top.navbar(role='navigation', aria-label='main navigation', style="box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;") nav.is-fixed-top.navbar(role='navigation', aria-label='main navigation', style="box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;")
.container .container
.navbar-brand .navbar-brand
a.navbar-item(href='/') a.navbar-item(href='/')
img(src='https://bulma.io/images/bulma-logo.png', width='112', height='28') img(src='https://bulma.io/images/bulma-logo.png', width='112', height='28')
a.navbar-burger(role='button', aria-label='menu', aria-expanded='false', data-target='navbarBasicExample') a.navbar-burger(role='button', aria-label='menu', aria-expanded='false', data-target='navbarBasicExample')
span(aria-hidden='true') span(aria-hidden='true')
span(aria-hidden='true') span(aria-hidden='true')
span(aria-hidden='true') span(aria-hidden='true')
#navbarBasicExample.navbar-menu #navbarBasicExample.navbar-menu
.navbar-start .navbar-start
a.navbar-item a.navbar-item
| 文档 | 文档
.navbar-item.has-dropdown.is-hoverable .navbar-item.has-dropdown.is-hoverable
a.navbar-link a.navbar-link
| 更多 | 更多
.navbar-dropdown .navbar-dropdown
a.navbar-item(href="/about") a.navbar-item(href="/about")
| 关于本站 | 关于本站
a.navbar-item a.navbar-item
| 关于作者 | 关于作者
hr.navbar-divider hr.navbar-divider
a.navbar-item a.navbar-item
| 报告问题 | 报告问题
.navbar-end .navbar-end
if !isLogin if !isLogin
.navbar-item .navbar-item
.buttons .buttons
a.button.is-primary(href="/register") a.button.is-primary(href="/register")
strong 注册 strong 注册
a.button.is-light(href="/login") a.button.is-light(href="/login")
| 登录 | 登录
else else
.navbar-item .navbar-item.has-dropdown.is-hoverable
.buttons a.navbar-link
button.button.is-white | #{user.nickname}
| #{user.nickname} .navbar-dropdown
a.button.is-danger.is-light(href="/logout") a.navbar-item
| 退出 | 用户资料
hr.navbar-divider
a.navbar-item
| 退出
//- a.button.is-danger.is-light(href="/logout")
//- | 退出

6
template/views/about.pug

@ -1,12 +1,12 @@
extends /layout/layout extends /layout/layout
block var block var
-title="关于" -title="关于"
block head block head
block content block content
section.section section.section
.container.content!= md .container.content!= md
block script block script

12
template/views/index.pug

@ -9,16 +9,6 @@ block head
block content block content
section.section section.section
.container .container
h1.title div sda
| Hello World
p.subtitle
| My first website with
strong Bulma
| !
if isLogin if isLogin
button(hx-get="/htmx/clicked" hx-push-url="/about" hx-trigger="click" hx-target="this" hx-swap="outerHTML") Click Me! button(hx-get="/htmx/clicked" hx-push-url="/about" hx-trigger="click" hx-target="this" hx-swap="outerHTML") Click Me!
<form action="/api/v1/upload/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file" multiple />
+security
<button type="submit" id="upload">上传</button>
</form>

20
template/views/login.pug

@ -1,22 +1,22 @@
extends /layout/layout extends /layout/layout
block var block var
-title="登陆" // 网页标题 -title="登陆" // 网页标题
-hideHeader=true -hideHeader=true
block head block head
+css("style/views/login.css") +css("style/views/login.css")
block content block content
.login .login
h1.title.is-1 登录 h1.title.is-1 登录
form(action='/login' method='post') form(action='/login' method='post')
input(id="referrer" type="text" name="referrer" class="form-control" style="display:none;") input(id="referrer" type="text" name="referrer" class="form-control" style="display:none;")
input(type='text', name='username', placeholder='用户名', required) input(type='text', name='username', placeholder='用户名', required)
input(type='password', name='password', placeholder='密码', required) input(type='password', name='password', placeholder='密码', required)
+security +security
button.btn.btn-primary.btn-block.btn-large(type='submit') 现在登录! button.btn.btn-primary.btn-block.btn-large(type='submit') 现在登录!
a(href="/register" style="margin-top: 8px;color: white;font-size: 14px;display: inline-block;float: right") 前往注册 a(href="/register" style="margin-top: 8px;color: white;font-size: 14px;display: inline-block;float: right") 前往注册
block script block script
+script("js/page/login.js") +script("js/page/login.js")

14
template/views/user.pug

@ -1,15 +1,15 @@
extends @/layout/layout extends @/layout/layout
block var block var
-title="用户信息" // 网页标题 -title="用户信息" // 网页标题
//- -hideHeader=true //- -hideHeader=true
block head block head
block content block content
section.section section.section
.container .container
div nickname: #{user.nickname} div nickname: #{user.nickname}
div email: #{user.email} div email: #{user.email}
div username: #{user.username} div username: #{user.username}
div id: #{user.id} div id: #{user.id}

25
types/global.d.ts

@ -1,27 +1,24 @@
import { Logger } from "log4js"; import { Logger } from "log4js"
import { Server } from "@hapi/hapi"; import { Server } from "@hapi/hapi"
import { Request, ResponseToolkit, Lifecycle } from "@hapi/hapi"; import { Request, ResponseToolkit, Lifecycle } from "@hapi/hapi"
import { Model, ModelCtor } from "Sequelize"; import { TUserModel } from "@/models/User"
import { TUserModel } from "@/models/User";
declare global { declare global {
var server: Server; var server: Server
var logger: Logger; var logger: Logger
var loggerSite: Logger; var loggerSite: Logger
var loggerSQL: Logger; var loggerSQL: Logger
} }
interface Models { interface Models {
"User": TUserModel User: TUserModel
} }
declare module '@hapi/hapi' { declare module "@hapi/hapi" {
interface Request { interface Request {
getModel<T extends keyof Models, M extends Models[T]>(name: T): M getModel<T extends keyof Models, M extends Models[T]>(name: T): M
} }
interface ResponseToolkit { interface ResponseToolkit {}
}
} }
export declare type Req = Request export declare type Req = Request

Loading…
Cancel
Save