npmrun 2 years ago
parent
commit
b7e87aff66
  1. 9
      public/js/page/user.js
  2. 1
      readme.md
  3. 5
      route.txt
  4. BIN
      source/db/data.db
  5. 74
      source/models/Attachment.ts
  6. 29
      source/plugins/router-plugin/index.ts
  7. 50
      source/route/views/upload/index.ts
  8. 154
      source/route/views/user.ts
  9. 2
      source/run.ts
  10. 2
      source/schema/index.ts
  11. 111
      template/htmx/path/user.pug
  12. 4
      template/ui/header.pug
  13. 2
      types/global.d.ts

9
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 = `<div class="control" style="margin-right: 6px;"><div class="image is-128x128"><img class="is-rounded" src="${url}"></div></div>`
placeholder.innerHTML = html
placeholder.insertAdjacentHTML("afterend", "<div>---></div>")
})

1
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

5
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

BIN
source/db/data.db

Binary file not shown.

74
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<AttachmentAttributes, "id"> {}
export interface AttachmentOutput extends Required<AttachmentAttributes> {}
export type TAttachmentModel = ReturnType<typeof AttachmentModel>
type DT = typeof DataTypes
export default function AttachmentModel(sequelize: Sequelize, DataTypes: DT) {
class Attachment extends Model<AttachmentAttributes, AttachmentInput> 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
}

29
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,
})
}
}
}
}

50
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)],
})
})
})

154
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<any>(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")
}

2
source/run.ts

@ -149,7 +149,7 @@ const run = async (): Promise<Server> => {
},
])
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
}

2
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,

111
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 取消

4
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")
| 用户资料

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

Loading…
Cancel
Save