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" {