diff --git a/public/js/page/user.js b/public/js/page/user.js
new file mode 100644
index 0000000..5cf40b8
--- /dev/null
+++ b/public/js/page/user.js
@@ -0,0 +1,9 @@
+const el = document.getElementById("upload")
+const placeholder = document.getElementById("upload-placeholder")
+el.addEventListener("change", e => {
+ const file = e.target.files[0]
+ const url = URL.createObjectURL(file)
+ const html = `
`
+ placeholder.innerHTML = html
+ placeholder.insertAdjacentHTML("afterend", "--->
")
+})
diff --git a/readme.md b/readme.md
index 130c323..c907805 100644
--- a/readme.md
+++ b/readme.md
@@ -31,6 +31,7 @@ https://blog.csdn.net/tiger1334/article/details/93468736
docker build -t hapi-website -f ./Dockerfile ./
docker run -itd --name website -p 8899:3388 hapi-website /bin/bash
+docker run -itd --name website -p 8899:3388 hapi-website 去除/bin/bash看会运行么
docker exec -it 容器ID /bin/bash
diff --git a/route.txt b/route.txt
index 1ea29c9..23b1463 100644
--- a/route.txt
+++ b/route.txt
@@ -1,6 +1,6 @@
-/home/topuser/Code/@project/hapi-demo/source/route/htmx对应路径:
+D:\1XYX\pro\hapi-demo\source\route\htmx对应路径:
不需权限 : GET /htmx/path/{path*}
-/home/topuser/Code/@project/hapi-demo/source/route/views对应路径:
+D:\1XYX\pro\hapi-demo\source\route\views对应路径:
不需权限(提供无需验证): GET /404
不需权限(提供无需验证): GET /
不需权限(提供无需验证): GET /about
@@ -12,6 +12,7 @@
不需权限(提供无需验证): GET /register
不需权限 : POST /register
需要权限 : POST /upload
+ 需要权限 : POST /user
需要权限 : GET /user
需要权限 : GET /user/logout
需要权限 : POST /user/del
\ No newline at end of file
diff --git a/source/db/data.db b/source/db/data.db
index 7dd26e0..ae78046 100644
Binary files a/source/db/data.db and b/source/db/data.db differ
diff --git a/source/models/Attachment.ts b/source/models/Attachment.ts
new file mode 100644
index 0000000..bf999e7
--- /dev/null
+++ b/source/models/Attachment.ts
@@ -0,0 +1,74 @@
+// CREATE TABLE attachments (
+// id INT PRIMARY KEY AUTO_INCREMENT,
+// filename VARCHAR(255) NOT NULL,
+// filepath VARCHAR(255) NOT NULL,
+// file_type VARCHAR(50) NOT NULL,
+// file_size INT NOT NULL,
+// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+// );
+
+import { Sequelize, DataTypes, Optional, Model } from "sequelize"
+
+interface AttachmentAttributes {
+ id: number
+ filename: string
+ filepath: string
+ file_type: string
+ file_size: number
+
+ createdAt?: Date
+ updatedAt?: Date
+}
+
+export interface AttachmentInput extends Optional {}
+export interface AttachmentOutput extends Required {}
+export type TAttachmentModel = ReturnType
+
+type DT = typeof DataTypes
+export default function AttachmentModel(sequelize: Sequelize, DataTypes: DT) {
+ class Attachment extends Model implements AttachmentAttributes {
+ public id!: number
+ public filename!: string
+ public filepath!: string
+ public file_type!: string
+ public file_size!: number
+
+ // timestamps!
+ public readonly createdAt!: Date
+ public readonly updatedAt!: Date
+ }
+ Attachment.init(
+ {
+ id: {
+ type: DataTypes.INTEGER,
+ autoIncrement: true,
+ primaryKey: true,
+ },
+ filename: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ },
+ filepath: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ },
+ file_type: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ },
+ file_size: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ },
+ },
+ {
+ modelName: "attachment",
+ sequelize,
+ underscored: true,
+ timestamps: true,
+ },
+ )
+
+ return Attachment
+}
diff --git a/source/plugins/router-plugin/index.ts b/source/plugins/router-plugin/index.ts
index 0623103..5bf986a 100755
--- a/source/plugins/router-plugin/index.ts
+++ b/source/plugins/router-plugin/index.ts
@@ -168,12 +168,29 @@ class routePlugin {
if (!handler) {
handler = ff
}
- server.route({
- method: method,
- path: route,
- handler: handler,
- options: options,
- })
+ if(Array.isArray(method)){
+ for (let i = 0; i < method.length; i++) {
+ const m = method[i];
+ const op = Object.assign({}, options)
+ if(m.toLowerCase() === "get"){
+ // get请求没有这个
+ delete op.payload
+ }
+ server.route({
+ method: m,
+ path: route,
+ handler: handler,
+ options: op,
+ })
+ }
+ }else{
+ server.route({
+ method: method,
+ path: route,
+ handler: handler,
+ options: options,
+ })
+ }
}
}
}
diff --git a/source/route/views/upload/index.ts b/source/route/views/upload/index.ts
index cef2ba1..ad05448 100644
--- a/source/route/views/upload/index.ts
+++ b/source/route/views/upload/index.ts
@@ -1,11 +1,12 @@
import { auth, config, method, route, swagger, validate } from "@noderun/hapi-router"
import UploadFunc from "./_upload"
-import path, { resolve } from "path";
-import { gFail, uploadDir } from "@/util";
-import { fileTypeFromFile, fileTypeFromStream } from "file-type";
-import { dateTimeFormat } from "@/util/util";
-import fs from "fs-extra";
-import { Req, Res } from "#/global";
+import path, { resolve } from "path"
+import { gFail, uploadDir } from "@/util"
+import { fileTypeFromFile, fileTypeFromStream } from "file-type"
+import { dateTimeFormat } from "@/util/util"
+import fs from "fs-extra"
+import { Req, Res } from "#/global"
+import * as bcrypt from "bcrypt"
const multiparty = require("multiparty")
export default class {
@@ -22,18 +23,40 @@ export default class {
@method("POST")
@auth()
async index(request: Req, h: Res) {
- const { id } = request.auth.credentials
- const { filelist, fields } = await Save(request.payload)
+ const { id, username, nickname, email, tel } = request.auth.credentials
+ const { filelist, fields } = await Save(request.payload, request)
const result = {}
- if (fields["username"] && fields["username"][0]) {
+ if (fields["username"] && fields["username"][0] && username !== fields["username"][0]) {
result["username"] = fields["username"][0]
}
+ if (fields["tel"] && fields["tel"][0] && tel !== fields["tel"][0]) {
+ result["tel"] = fields["tel"][0]
+ }
+ if (fields["nickname"] && fields["nickname"][0] && nickname !== fields["nickname"][0]) {
+ result["nickname"] = fields["nickname"][0]
+ }
+ if (fields["email"] && fields["email"][0] && email !== fields["email"][0]) {
+ result["email"] = fields["email"][0]
+ }
+ if (fields["password"] && fields["password"][0]) {
+ const pwd = fields["password"][0]
+ let salt = bcrypt.genSaltSync(10)
+ let pwdLock = bcrypt.hashSync(pwd, salt)
+ result["password"] = pwdLock
+ }
if (filelist && filelist[0]) {
result["avatar"] = filelist[0]
}
if (JSON.stringify(result) !== "{}") {
const UserModel = request.getModel("user")
- UserModel.update(result, { where: { id } })
+ const user = await UserModel.findOne({ where: { username: fields["username"] } })
+ if (!!user && user.id !== id) {
+ request.yar.flash("error", "用户名已被他人占用")
+ return h.redirect("/user")
+ }
+ console.log(fields);
+
+ await UserModel.update(result, { where: { id } })
}
return h.redirect("/user")
}
@@ -54,7 +77,7 @@ function saveFile(file) {
console.log("rename error: " + err)
reject()
} else {
- resolve(path.resolve("/public/upload/" + _name))
+ resolve("/public/upload/" + _name)
}
})
} else {
@@ -64,7 +87,8 @@ function saveFile(file) {
})
}
-function Save(payload) {
+function Save(payload, req: Req) {
+ const AttachmentModel = req.getModel("attachment")
const form = new multiparty.Form({
uploadDir: uploadDir, //路径需要对应自己的项目更改
/*设置文件保存路径 */
@@ -118,7 +142,7 @@ function Save(payload) {
}
resolve({
fields,
- filelist: [...new Set(fileList)]
+ filelist: [...new Set(fileList)],
})
})
})
diff --git a/source/route/views/user.ts b/source/route/views/user.ts
index a314f0f..1315a15 100644
--- a/source/route/views/user.ts
+++ b/source/route/views/user.ts
@@ -1,18 +1,162 @@
import { Req, Res, ReturnValue } from "#/global"
import { UserSchema } from "@/schema"
-import { gFail, gSuccess } from "@/util"
-import { auth, config, method, route, validate } from "@noderun/hapi-router"
-import { sequelize } from "@sequelize"
+import { auth, config, method, route, route_path, swagger, validate } from "@noderun/hapi-router"
+import path, { resolve } from "path"
+import { gFail, uploadDir } from "@/util"
+import { fileTypeFromFile, fileTypeFromStream } from "file-type"
+import { dateTimeFormat } from "@/util/util"
+import fs from "fs-extra"
import * as bcrypt from "bcrypt"
+const multiparty = require("multiparty")
+
+function saveFile(file) {
+ return new Promise(async (resolve, reject) => {
+ const filename = file.originalFilename
+ const uploadedPath = file.path
+ const filetype = await fileTypeFromFile(uploadedPath)
+ const _file = path.parse(filename)
+ if (filetype && (filetype.ext == "jpg" || filetype.ext == "png")) {
+ let _name =
+ _file.name + "_" + dateTimeFormat(new Date(), "yyyy_MM_dd") + "_" + new Date().getTime() + _file.ext
+ const dstPath = path.resolve(uploadDir, _name)
+ fs.rename(uploadedPath, dstPath, function (err) {
+ if (err) {
+ console.log("rename error: " + err)
+ reject()
+ } else {
+ resolve("/public/upload/" + _name)
+ }
+ })
+ } else {
+ fs.unlinkSync(uploadedPath)
+ reject(new Error(filename + "文件不是图片"))
+ }
+ })
+}
+
+function Save(payload, req: Req) {
+ const AttachmentModel = req.getModel("attachment")
+ const form = new multiparty.Form({
+ uploadDir: uploadDir, //路径需要对应自己的项目更改
+ /*设置文件保存路径 */
+ encoding: "utf-8",
+ /*编码设置 */
+ maxFilesSize: 20000 * 1024 * 1024,
+ /*设置文件最大值 20MB */
+ keepExtensions: true,
+ /*保留后缀*/
+ })
+ return new Promise(async (resolve, reject) => {
+ form.on("part", function (part) {
+ console.log(1111)
+ console.log(part.filename)
+ })
+ form.on("progress", function (bytesReceived, bytesExpected) {
+ if (bytesExpected === null) {
+ return
+ }
+
+ // var percentComplete = (bytesReceived / bytesExpected) * 100
+ // console.log("the form is " + Math.floor(percentComplete) + "%" + " complete")
+ })
+ form.parse(payload, async function (err, fields, files) {
+ // console.log(err, fields, files);
+ if (err) {
+ reject(err.message)
+ return
+ }
+ const errList = []
+ const fileList = []
+ if (files && files.file && files.file.length) {
+ for (let i = 0; i < files.file.length; i++) {
+ const file = files.file[i]
+ if (file.originalFilename === "" && file.size === 0) {
+ const uploadedPath = file.path
+ fs.unlinkSync(uploadedPath)
+ continue
+ }
+ try {
+ const dstPath = await saveFile(file)
+ fileList.push(dstPath)
+ } catch (error) {
+ errList.push(error.message)
+ }
+ }
+ }
+ if (errList.length) {
+ reject(gFail(null, errList.join("\n")))
+ return
+ }
+ resolve({
+ fields,
+ filelist: [...new Set(fileList)],
+ })
+ })
+ })
+}
+
/**
* 登录界面
*/
export default class {
+ @config({
+ payload: {
+ maxBytes: 20000 * 1024 * 1024,
+ output: "stream",
+ parse: false,
+ multipart: true,
+ timeout: false,
+ allow: ["multipart/form-data", "application/x-www-form-urlencoded"],
+ },
+ })
+ @method("POST")
+ @auth()
+ @route_path("/user")
+ async index_post(request: Req, h: Res): ReturnValue {
+ const { id, username, nickname, email, tel } = request.auth.credentials
+ const { filelist, fields } = await Save(request.payload, request)
+ const result = {}
+ if (fields["username"] && fields["username"][0] && username !== fields["username"][0]) {
+ result["username"] = fields["username"][0]
+ }
+ if (fields["tel"] && fields["tel"][0] && tel !== fields["tel"][0]) {
+ result["tel"] = fields["tel"][0]
+ }
+ if (fields["nickname"] && fields["nickname"][0] && nickname !== fields["nickname"][0]) {
+ result["nickname"] = fields["nickname"][0]
+ }
+ if (fields["email"] && fields["email"][0] && email !== fields["email"][0]) {
+ result["email"] = fields["email"][0]
+ }
+ if (fields["password"] && fields["password"][0]) {
+ const pwd = fields["password"][0]
+ let salt = bcrypt.genSaltSync(10)
+ let pwdLock = bcrypt.hashSync(pwd, salt)
+ result["password"] = pwdLock
+ }
+ if (filelist && filelist[0]) {
+ result["avatar"] = filelist[0]
+ }
+ if (JSON.stringify(result) !== "{}") {
+ const UserModel = request.getModel("user")
+ const user = await UserModel.findOne({ where: { username: fields["username"] } })
+ if (!!user && user.id !== id) {
+ request.yar.flash("error", "用户名已被他人占用")
+ }
+ if (!!user && user.id === id) {
+ await UserModel.update(result, { where: { id } })
+ // @ts-ignore
+ request.auth.credentials = await UserModel.findOne({ where: { id } })
+ }
+ }
+ return h.redirect("/user")
+ }
+
@method("GET")
@auth()
- async index(request: Req, h: Res): ReturnValue {
+ @route_path("/user")
+ async index_get(request: Req, h: Res): ReturnValue {
const isRenderHtmx = Reflect.has(request.query, "htmx")
- const { id } = request.auth.credentials
if (isRenderHtmx) {
return h.view("htmx/path/user.pug")
}
diff --git a/source/run.ts b/source/run.ts
index 8f6bcf7..829b17d 100644
--- a/source/run.ts
+++ b/source/run.ts
@@ -149,7 +149,7 @@ const run = async (): Promise => {
},
])
await server.start()
- logger.trace("Server running on %s", server.info.uri)
+ logger.trace("Server running on %s", server.info.uri.replace("0.0.0.0", "localhost"))
return server
}
diff --git a/source/schema/index.ts b/source/schema/index.ts
index cd66a42..7e68eab 100644
--- a/source/schema/index.ts
+++ b/source/schema/index.ts
@@ -23,7 +23,7 @@ export const RegisterUserSchema = Joi.object({
export const LoginUserSchema = Joi.object({
referrer: Joi.string().allow("").optional(),
- username: Joi.string().min(6).max(35), //Joi.string().alphanum().min(6).max(35)
+ username: Joi.string().min(5).max(35), //Joi.string().alphanum().min(6).max(35)
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")).required(),
// email: Joi.string().email({
// minDomainSegments: 2,
diff --git a/template/htmx/path/user.pug b/template/htmx/path/user.pug
index 4aab823..abb67b6 100644
--- a/template/htmx/path/user.pug
+++ b/template/htmx/path/user.pug
@@ -1,70 +1,89 @@
include @/helper/helper.pug
+include @/helper/flush.pug
block var
-title=user.nickname || "Welcome" // 网页标题
title #{user.nickname || "Welcome"}
-form(action="/upload" method="post" enctype="multipart/form-data" style="margin: 0 auto; width: 500px;")
+form(action="/user" method="post" enctype="multipart/form-data" style="margin: 0 auto; width: 500px;")
.field
- .label 用户名
- .control
+ .label 用户名
+ .control
input.input(type="text" name="username" placeholder="请输入用户名" value=user.username)
.field
+ .label 密码
+ .control
+ input.input(type="password" name="password" placeholder="请输入密码")
+ .field
+ .label 昵称
+ .control
+ input.input(type="text" name="nickname" placeholder="请输入昵称" value=user.nickname)
+ .field
+ .label 邮箱
+ .control
+ input.input(type="email" name="email" placeholder="请输入邮箱" value=user.email)
+ .field
+ .label 手机号
+ .control
+ input.input(type="tel" name="tel" placeholder="请输入手机号" value=user.tel)
+ .field
.label 头像
.control
.file.is-primary
label.file-label
- input.file-input(type="file", name="file")
+ input.file-input(type="file" name="file" id="upload")
span.file-cta
span.file-label
| 上传头像
- if user.avatar
- .field
- .control
- .image.is-128x128
- img.is-rounded(src=user.avatar alt=user.username)
+ div(style="display: flex;align-items:center;")
+ .field#upload-placeholder
+ if user.avatar
+ .field
+ .control
+ .image.is-128x128
+ img.is-rounded(src=user.avatar alt=user.username)
+security
.field.is-grouped
.control
button.button.is-link(type="submit") 提交
.control
button.button.is-link.is-light 取消
++script("js/page/user.js")
-
-form(action="", method="post" style="margin: 0 auto; width: 500px;margin-top:20px")
- .field
- .label 用户名
- .control
- input.input(type="text" placeholder="请输入用户名" value=user.username)
- .field
- .label 昵称
- .control
- input.input(type="text" placeholder="请输入用户名" value=user.nickname)
- .field
- .label 邮箱
- .control
- input.input(type="email" placeholder="请输入邮箱" value=user.email)
- .field
- .label 手机号
- .control
- input.input(type="tel" placeholder="请输入手机号" value=user.tel)
- .field
- .label 头像
- .control
- .file.is-primary
- label.file-label
- input.file-input(type="file", name="file")
- span.file-cta
- span.file-label
- | 上传头像
- if user.avatar
- .field
- .control
- .image.is-128x128
- img.is-rounded(src=user.avatar alt=user.username)
- .field.is-grouped
- .control
- button.button.is-link(type="submit") 提交
- .control
- button.button.is-link.is-light 取消
+//- form(action="", method="post" style="margin: 0 auto; width: 500px;margin-top:20px")
+//- .field
+//- .label 用户名
+//- .control
+//- input.input(type="text" placeholder="请输入用户名" value=user.username)
+//- .field
+//- .label 昵称
+//- .control
+//- input.input(type="text" placeholder="请输入用户名" value=user.nickname)
+//- .field
+//- .label 邮箱
+//- .control
+//- input.input(type="email" placeholder="请输入邮箱" value=user.email)
+//- .field
+//- .label 手机号
+//- .control
+//- input.input(type="tel" placeholder="请输入手机号" value=user.tel)
+//- .field
+//- .label 头像
+//- .control
+//- .file.is-primary
+//- label.file-label
+//- input.file-input(type="file", name="file")
+//- span.file-cta
+//- span.file-label
+//- | 上传头像
+//- if user.avatar
+//- .field
+//- .control
+//- .image.is-128x128
+//- img.is-rounded(src=user.avatar alt=user.username)
+//- .field.is-grouped
+//- .control
+//- button.button.is-link(type="submit") 提交
+//- .control
+//- button.button.is-link.is-light 取消
\ No newline at end of file
diff --git a/template/ui/header.pug b/template/ui/header.pug
index f9bb09e..da9f114 100644
--- a/template/ui/header.pug
+++ b/template/ui/header.pug
@@ -35,8 +35,8 @@ nav.is-fixed-top.navbar(role='navigation', aria-label='main navigation', style="
.navbar-item.has-dropdown.is-hoverable
a.navbar-link
.image.is-28x28(style="margin-right: 8px;")
- img.is-rounded(src=user.avatar alt=user.username)
- div #{user.username}
+ img.is-rounded(src=user.avatar alt=user.nickname)
+ div #{user.nickname}
.navbar-dropdown.is-right
a.navbar-item(hx-get="/user?htmx" hx-push-url="/user" hx-trigger="click" hx-target="#single-page" hx-swap="innerHTML")
| 用户资料
diff --git a/types/global.d.ts b/types/global.d.ts
index 3dda974..0833b5a 100644
--- a/types/global.d.ts
+++ b/types/global.d.ts
@@ -4,6 +4,7 @@ import { Request, ResponseToolkit, Lifecycle } from "@hapi/hapi"
import { TUserModel } from "@/models/user"
import { TColorModel } from "@/models/color"
import yar from "@hapi/yar"
+import { TAttachmentModel } from "@/models/Attachment"
declare global {
var server: Server
@@ -15,6 +16,7 @@ declare global {
interface Models {
user: TUserModel
color: TColorModel
+ attachment: TAttachmentModel
}
declare module "sequelize" {