This commit is contained in:
2025-11-12 20:04:38 +08:00
parent 58fce7ec2a
commit a5add3dd73
15 changed files with 328 additions and 91 deletions
+2 -4
View File
@@ -18,10 +18,8 @@ database:
user: user:
cookieTimeout: 604800 cookieTimeout: 604800
passHashType: "md5" #密码哈希类型 text md5 passHashType: "md5" #密码哈希类型 text md5 md5salt
avatarSavePath: "/data/avatar/" #头像保存的真实位置
avatarGinrouterPath: "/avatar/" #gin 路由的路径
avatarPath: "/static/avatars/def.png"
file: file:
maxSize: 52428800 maxSize: 52428800
+3 -1
View File
@@ -3,6 +3,8 @@
"apiErr":-1, "apiErr":-1,
"postErr":-2, "postErr":-2,
"jsonErr":-3, "jsonErr":-3,
"userNameDup":-4 "userNameDup":-4,
"userNameNoFund":-41,
"userPassIncorrect":-42
} }
-3
View File
@@ -17,9 +17,6 @@ type ConfigsWeb_ struct {
type ConfigsUser_ struct { type ConfigsUser_ struct {
CookieTimeout int `mapstructure:"cookieTimeout"` CookieTimeout int `mapstructure:"cookieTimeout"`
PassHashType string `mapstructure:"passHashType"` PassHashType string `mapstructure:"passHashType"`
AvatarSavePath string `mapstructure:"avatarSavePath"`
AvatarGinrouterPath string `mapstructure:"avatarGinrouterPath"`
AvatarPath string `mapstructure:"avatarPath"`
} }
type ConfigsFile_ struct { type ConfigsFile_ struct {
+10 -4
View File
@@ -39,13 +39,19 @@ func Md5Str(str string) string {
return hashString2 return hashString2
} }
func HashUserPass(str string) string { func HashUserPass(user *TabUser_) {
switch ConfigsUser.PassHashType { switch ConfigsUser.PassHashType {
case "text": case "text":
return str break
case "md5": case "md5":
return Md5Str(str) user.Pass = Md5Str(user.Pass)
case "md5salt":
if user.Salt == "" {
user.Salt = RandStr32()
}
user.Pass = Md5Str(Md5Str(user.Pass) + user.Salt)
} }
return GetCurrentTimeString() + RandStr32() //如果转换失败返回当前时间,避免撞库
} }
+43 -10
View File
@@ -1,17 +1,50 @@
package models package models
import "time" import (
"net"
"strings"
type APIRequestLog struct { "github.com/gin-gonic/gin"
ID int64 `gorm:"primaryKey;column:id" json:"id"` )
IPAddress string `gorm:"column:ip_address;size:45;not null" json:"ip_address"`
Path string `gorm:"column:path;size:500;not null" json:"path"` // GetRealIP 获取真实IP(处理代理)
Method string `gorm:"column:method;size:10;not null" json:"method"` func GetRealIP(c *gin.Context) string {
StatusCode int `gorm:"column:status_code;index" json:"status_code"` // 优先级顺序
Message string `gorm:"column:error_message;type:text" json:"error_message"` headers := []string{
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP" json:"created_at"` "CF-Connecting-IP", // Cloudflare
"True-Client-IP",
"X-Forwarded-For",
"X-Real-IP",
} }
func LogAdd() { for _, header := range headers {
if ip := c.GetHeader(header); ip != "" {
// 处理多个IP的情况(如 X-Forwarded-For: client, proxy1, proxy2
if strings.Contains(ip, ",") {
ips := strings.Split(ip, ",")
ip = strings.TrimSpace(ips[0])
}
if net.ParseIP(ip) != nil {
return ip
}
}
}
// 最后使用Gin的ClientIP方法
return c.ClientIP()
}
func LogAdd(c *gin.Context, msg string) {
var logtemp APIRequestLog_
logtemp.IPAddress = GetRealIP(c)
logtemp.Path = c.Request.URL.Path
logtemp.Method = c.Request.Method
logtemp.Message = msg
//fmt.Println(logtemp)
DB.Create(&logtemp)
} }
+14 -6
View File
@@ -30,6 +30,7 @@ type TabUser_ struct {
Email string `gorm:"size:255;index"` // 字符串长度限制100 索引 Email string `gorm:"size:255;index"` // 字符串长度限制100 索引
Pass string `gorm:"size:128"` // 建议存储哈希后的密码 Pass string `gorm:"size:128"` // 建议存储哈希后的密码
Type string `gorm:"size:64;default:user"` // Type string `gorm:"size:64;default:user"` //
Salt string `gorm:"size:64;"`
Date time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"` // 默认当前时间 Date time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"` // 默认当前时间
} }
@@ -71,15 +72,20 @@ type TabCookie_ struct {
UserID uint `gorm:"size:16;not null"` UserID uint `gorm:"size:16;not null"`
Name string `gorm:"size:255;not null;index"` Name string `gorm:"size:255;not null;index"`
Value string `gorm:"size:255;not null;index"` Value string `gorm:"size:255;not null;index"`
Domain string `gorm:"size:255;not null"`
Path string `gorm:"size:255;not null;default:/"`
ExpiresAt time.Time `gorm:"type:datetime;index"` ExpiresAt time.Time `gorm:"type:datetime;index"`
CreatedAt time.Time `gorm:"type:datetime;not null;default:CURRENT_TIMESTAMP"` CreatedAt time.Time `gorm:"type:datetime;not null;default:CURRENT_TIMESTAMP"`
UpdatedAt time.Time `gorm:"type:datetime;index;not null;default:CURRENT_TIMESTAMP"` UpdatedAt time.Time `gorm:"type:datetime;index;not null;default:CURRENT_TIMESTAMP"`
SecureFlag bool `gorm:"not null;default:false"` Remember bool `gorm:"default:false"`
HttpOnly bool `gorm:"not null;default:false"` }
SameSite string `gorm:"size:10;not null;default:'Lax'"`
PartitionKey string `gorm:"size:50"` type APIRequestLog_ struct {
ID int64 `gorm:"primaryKey;column:id" json:"id"`
IPAddress string `gorm:"column:ip_address;size:45;not null" json:"ip_address"`
Path string `gorm:"column:path;size:500;not null" json:"path"`
Method string `gorm:"column:method;size:10;not null" json:"method"`
StatusCode int `gorm:"column:status_code;index" json:"status_code"`
Message string `gorm:"column:error_message;type:text" json:"error_message"`
CreatedAt time.Time `gorm:"column:created_at;type:datetime;default:CURRENT_TIMESTAMP" json:"created_at"`
} }
func DatabaseInit() error { func DatabaseInit() error {
@@ -115,5 +121,7 @@ func DatabaseInit() error {
DB.AutoMigrate(&TabFileInfo_{}) DB.AutoMigrate(&TabFileInfo_{})
DB.AutoMigrate(&APIRequestLog_{})
return nil return nil
} }
+85 -9
View File
@@ -3,6 +3,7 @@ package routers
import ( import (
"fmt" "fmt"
"ops/models" "ops/models"
"strconv"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -20,8 +21,12 @@ func ApiInit() {
if models.DB.Where(&user).First(&user).Error == nil { if models.DB.Where(&user).First(&user).Error == nil {
} else { } else {
fmt.Println("用户不存在") //fmt.Println("用户不存在")
user.Pass = models.HashUserPass("adminpassword")
//对密码加盐
user.Salt = models.RandStr32()
user.Pass = "adminpassword"
models.HashUserPass(&user)
models.DB.Create(&user) // 传入指针 models.DB.Create(&user) // 传入指针
} }
@@ -54,6 +59,12 @@ type From_user_add struct {
Userpass string `json:"userpass"` Userpass string `json:"userpass"`
} }
type From_user_login struct {
Username string `json:"username"`
Userpass string `json:"userpass"`
Remember bool `json:"remember"`
}
func ApiUser(r *gin.RouterGroup) { func ApiUser(r *gin.RouterGroup) {
r.GET("/test", func(ctx *gin.Context) { r.GET("/test", func(ctx *gin.Context) {
@@ -62,6 +73,67 @@ func ApiUser(r *gin.RouterGroup) {
r.POST("/test", func(ctx *gin.Context) { r.POST("/test", func(ctx *gin.Context) {
ReturnJson(ctx, "apiOK", nil) ReturnJson(ctx, "apiOK", nil)
}) })
//用户登陆
r.POST("/login", func(ctx *gin.Context) {
var loginuser From_user_login
data, _ := SeparateData(ctx)
if data != nil {
if err := mapstructure.Decode(data, &loginuser); err == nil {
if loginuser.Username != "" && loginuser.Userpass != "" {
//传入的数据都ok,获取用户信息
getuser := models.TabUser_{
Name: loginuser.Username,
}
if models.DB.Where(&getuser).First(&getuser).Error == nil {
//倒入数据
user := models.TabUser_{
Pass: loginuser.Userpass, //密码明文
Salt: getuser.Salt, //保存的盐制
}
//哈希密
models.HashUserPass(&user)
if user.Pass == getuser.Pass {
//用户密码正确,生成cookie
cookie := models.TabCookie_{
UserID: getuser.ID,
Name: "login",
Value: models.RandStr32(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(models.ConfigsUser.CookieTimeout) * time.Second), //计算过期时间,
Remember: loginuser.Remember,
}
models.DB.Create(&cookie) // 传入指针
redata := map[string]interface{}{
"cookie": cookie,
}
ReturnJson(ctx, "apiOK", redata)
} else {
ReturnJson(ctx, "userPassIncorrect", nil)
}
} else {
//用户不存在
ReturnJson(ctx, "userNameNoFund", nil)
}
} else {
ReturnJson(ctx, "jsonErr", nil)
}
} else {
ReturnJson(ctx, "jsonErr", nil)
}
} else {
ReturnJson(ctx, "postErr", nil)
}
})
//用户注册
r.POST("/register", func(ctx *gin.Context) { r.POST("/register", func(ctx *gin.Context) {
//转换传进来的数据 //转换传进来的数据
var jsonData From_user_add var jsonData From_user_add
@@ -79,8 +151,7 @@ func ApiUser(r *gin.RouterGroup) {
// Date 字段无需赋值,数据库会自动填充默认值 // Date 字段无需赋值,数据库会自动填充默认值
} }
if newUser.Name != "" && newUser.Pass != "" && newUser.Email != "" { if newUser.Name != "" && newUser.Pass != "" && newUser.Email != "" {
//对用户的密码进行哈希替换
newUser.Pass = models.HashUserPass(newUser.Pass)
//用户名是唯一的,先读取是否有这个用户名 //用户名是唯一的,先读取是否有这个用户名
var user models.TabUser_ var user models.TabUser_
user.Name = newUser.Name user.Name = newUser.Name
@@ -90,13 +161,18 @@ func ApiUser(r *gin.RouterGroup) {
ReturnJson(ctx, "userNameDup", nil) ReturnJson(ctx, "userNameDup", nil)
} else { } else {
//fmt.Println("用户不存在") //fmt.Println("用户不存在")
//对密码加盐
newUser.Salt = models.RandStr32()
//对用户的密码进行哈希替换
models.HashUserPass(&newUser)
models.DB.Create(&newUser) // 传入指针 models.DB.Create(&newUser) // 传入指针
// //创建info //创建用户后写一个log
// var user_info models.TabUserInfo_
// user_info.AvatarPath = models.ConfigsUser.AvatarPath models.LogAdd(ctx, "New user id:"+strconv.Itoa(int(newUser.ID)))
// user_info.UserID = newUser.ID
// models.DB.Create(&user_info) // 传入指针
ReturnJson(ctx, "apiOK", nil) ReturnJson(ctx, "apiOK", nil)
} }
@@ -1,46 +1,45 @@
<script setup> <script setup>
import { useUserStore } from "@/stores/user";
import { RouterLink} from 'vue-router' import { RouterLink } from "vue-router";
import { useI18n } from 'vue-i18n' import { useI18n } from "vue-i18n";
import { myfuncs } from '@/myfunc.js' import { myfuncs } from "@/myfunc.js";
import { onMounted, ref } from 'vue' import { onMounted, ref } from "vue";
// 使用 vue-i18n 的 Composition API // 使用 vue-i18n 的 Composition API
const { t, locale } = useI18n() const { t, locale } = useI18n();
const userStore = useUserStore();
const theTeme = ref('light') const theTeme = ref("light");
const lang_sele = ref(null) const lang_sele = ref(null);
const isLogin = false
function set_them(temp) { function set_them(temp) {
theTeme.value = temp theTeme.value = temp;
myfuncs.setTheme(temp, true) myfuncs.setTheme(temp, true);
} }
function changeLanguage(lang) { function changeLanguage(lang) {
// 切换语言 // 切换语言
const selectElement = lang.target const selectElement = lang.target;
const selectedLang = selectElement.value const selectedLang = selectElement.value;
locale.value = selectedLang locale.value = selectedLang;
myfuncs.save('userLanguage', selectedLang) myfuncs.save("userLanguage", selectedLang);
//console.log("selectedLang:",selectedLang); //console.log("selectedLang:",selectedLang);
} }
onMounted(() => { onMounted(() => {
const savedTheme = myfuncs.getThemefromStorge() const savedTheme = myfuncs.getThemefromStorge();
theTeme.value = savedTheme theTeme.value = savedTheme;
myfuncs.setTheme(savedTheme, false) myfuncs.setTheme(savedTheme, false);
const userLang = myfuncs.load('userLanguage') const userLang = myfuncs.load("userLanguage");
if (userLang) { if (userLang) {
locale.value = userLang locale.value = userLang;
if (lang_sele.value) { if (lang_sele.value) {
;(lang_sele.value).value = userLang lang_sele.value.value = userLang;
} }
} }
}) });
</script> </script>
<template> <template>
@@ -60,7 +59,9 @@ onMounted(() => {
</button> </button>
<!-- END NAVBAR TOGGLER --> <!-- END NAVBAR TOGGLER -->
<!-- BEGIN NAVBAR LOGO --> <!-- BEGIN NAVBAR LOGO -->
<div class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3"> <div
class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3"
>
<router-link to="/" aria-label="Tabler"> <router-link to="/" aria-label="Tabler">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -137,7 +138,7 @@ onMounted(() => {
</a> </a>
</div> </div>
<!-- 这里判断是否已经登陆 是则显示用户信息 否则显示登陆按钮 --> <!-- 这里判断是否已经登陆 是则显示用户信息 否则显示登陆按钮 -->
<div v-if="!isLogin" class="nav-item"> <div v-if="!userStore.isLoggedIn" class="nav-item">
<router-link to="login" class="btn btn-outline-primary"> <router-link to="login" class="btn btn-outline-primary">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -157,18 +158,21 @@ onMounted(() => {
<path d="M19 22v-6"></path> <path d="M19 22v-6"></path>
<path d="M22 19l-3 -3l-3 3"></path> <path d="M22 19l-3 -3l-3 3"></path>
</svg> </svg>
{{ t('message.login_or_register') }} {{ t("message.login_or_register") }}
</router-link> </router-link>
</div> </div>
<div v-if="isLogin" class="nav-item dropdown"> <div v-if="userStore.isLoggedIn" class="nav-item dropdown">
<a <a
href="#" href="#"
class="nav-link d-flex lh-1 p-0 px-2" class="nav-link d-flex lh-1 p-0 px-2"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-label="Open user menu" aria-label="Open user menu"
> >
<span class="avatar avatar-sm" style="background-image: url(./static/avatars/000m.jpg)"> <span
class="avatar avatar-sm"
style="background-image: url(./static/avatars/000m.jpg)"
>
</span> </span>
<div class="d-none d-xl-block ps-2"> <div class="d-none d-xl-block ps-2">
<div>Paweł Kuna</div> <div>Paweł Kuna</div>
@@ -213,15 +217,23 @@ onMounted(() => {
> >
<path d="M5 12l-2 0l9 -9l9 9l-2 0" /> <path d="M5 12l-2 0l9 -9l9 9l-2 0" />
<path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" /> <path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" />
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" /></svg <path
d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"
/></svg
></span> ></span>
<span class="nav-link-title"> {{ t('appname.home') }} </span> <span class="nav-link-title">
{{ t("appname.home") }}
</span>
</router-link> </router-link>
</li> </li>
</ul> </ul>
<div class="ms-auto"> <div class="ms-auto">
<select class="form-select" @change="changeLanguage" ref="lang_sele"> <select
class="form-select"
@change="changeLanguage"
ref="lang_sele"
>
<option value="en">English</option> <option value="en">English</option>
<option value="zh-CN">中文</option> <option value="zh-CN">中文</option>
</select> </select>
+4 -1
View File
@@ -37,7 +37,10 @@
"network_err":"Network error", "network_err":"Network error",
"username_dup":"Duplicate username", "username_dup":"Duplicate username",
"registration_successful":"Registration successful!", "registration_successful":"Registration successful!",
"server_error":"Server Error" "server_error":"Server Error",
"user_not_found":"User not found",
"username_or_password_incorrect":"Username or password incorrect.",
"login_successful":"Login successful"
}, },
"button": { "button": {
"submit": "Submit", "submit": "Submit",
+4 -1
View File
@@ -37,7 +37,10 @@
"network_err":"网络错误", "network_err":"网络错误",
"username_dup":"用户名重复", "username_dup":"用户名重复",
"registration_successful":"注册成功!", "registration_successful":"注册成功!",
"server_error":"服务端错误" "server_error":"服务端错误",
"user_not_found":"找不到用户",
"username_or_password_incorrect":"用户或密码错误",
"login_successful":"登录成功"
}, },
"button": { "button": {
"submit": "提交", "submit": "提交",
+2 -2
View File
@@ -23,7 +23,7 @@ export const my_network_func = {
//把cookie插入json //把cookie插入json
var data = {}; var data = {};
data["data"] = json; data["data"] = json;
var cookie = myfuncs.load_json("cookie"); var cookie = myfuncs.loadJson("cookie");
if (cookie) { if (cookie) {
data["cookie"] = cookie.Value; data["cookie"] = cookie.Value;
} }
@@ -46,7 +46,7 @@ export const my_network_func = {
if (response.data.cookie.Value == "") { if (response.data.cookie.Value == "") {
myfuncs.dele("cookie"); myfuncs.dele("cookie");
} else { } else {
myfuncs.save_json("cookie", response.data.cookie); myfuncs.saveJson("cookie", response.data.cookie);
} }
} }
} }
+27 -2
View File
@@ -8,6 +8,31 @@ export const myfuncs = {
console.log("myfuncs test ok"); console.log("myfuncs test ok");
}, },
//临时保存的数据,浏览器专属
saveT(key,data){
sessionStorage.setItem(key, data)
},
loadT(key){
return sessionStorage.getItem(key)
},
deleT(key){
sessionStorage.removeItem(key)
},
saveJsonT(key,data){
this.saveT(key,JSON.stringify(data))
},
loadJsonT(key){
const js_data=this.loadT(key)
if(js_data){
return JSON.parse(js_data)
}else{
return null
}
},
save(key,data){ save(key,data){
localStorage.setItem(key, data) localStorage.setItem(key, data)
}, },
@@ -18,11 +43,11 @@ export const myfuncs = {
localStorage.removeItem(key) localStorage.removeItem(key)
}, },
save_json(key,data){ saveJson(key,data){
this.save(key,JSON.stringify(data)) this.save(key,JSON.stringify(data))
}, },
load_json(key){ loadJson(key){
const js_data=this.load(key) const js_data=this.load(key)
if(js_data){ if(js_data){
return JSON.parse(js_data) return JSON.parse(js_data)
+4
View File
@@ -15,6 +15,9 @@ export const useUserStore = defineStore("user", () => {
const login = () => { const login = () => {
isLoggedIn.value = true; isLoggedIn.value = true;
}; };
const loginUpdata = () => {
isLoggedIn.value = true;
};
return { return {
userInfo, userInfo,
@@ -22,5 +25,6 @@ export const useUserStore = defineStore("user", () => {
isLoggedIn, isLoggedIn,
logout, logout,
login, login,
loginUpdata,
}; };
}); });
+71 -1
View File
@@ -1,9 +1,16 @@
<script setup> <script setup>
import { onMounted, watch, ref } from "vue"; import { onMounted, watch, ref } from "vue";
import { useRouter } from "vue-router";
import { useUserStore } from "@/stores/user";
import { my_network_func } from "@/my_network_func";
import { myfuncs } from "@/myfunc.js";
import MyOffcanvas from "@/components/MyOffcanvas.vue"; import MyOffcanvas from "@/components/MyOffcanvas.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
// 使用 vue-i18n 的 Composition API // 使用 vue-i18n 的 Composition API
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const router = useRouter();
const userStore = useUserStore();
const mos = ref(); const mos = ref();
@@ -11,6 +18,7 @@ const username = ref();
const password = ref(); const password = ref();
const isRemember = ref(); const isRemember = ref();
const isShowPassword = ref(false); const isShowPassword = ref(false);
function togglePasswordVisibility() { function togglePasswordVisibility() {
isShowPassword.value = !isShowPassword.value; isShowPassword.value = !isShowPassword.value;
@@ -42,7 +50,69 @@ function login() {
return; return;
} }
console.log("登录信息:", { user, pass, remember }); //console.log("登录信息:", { user, pass, remember });
my_network_func.postJson(
"/users/login",
{
username: user,
userpass: pass,
remember: remember,
},
(r) => {
console.log(r)
switch (r.statusCode) {
case 200:
switch (r.data.err_code) {
case -41:
username.value?.classList.add("is-invalid");
mos.value?.showAlert(
"warning",
t("message.user_not_found"),
5000
);
break;
case -42:
username.value?.classList.add("is-invalid");
password.value?.classList.add("is-invalid");
mos.value?.showAlert(
"warning",
t("message.username_or_password_incorrect"),
5000
);
break;
case 0:
//登录成功,载入cookie
//临时保存cookie
myfuncs.saveJsonT("userCookie",r.data.return.cookie)
if(remember){
//长期保存cookie
myfuncs.saveJson("userCookie",r.data.return.cookie)
}
//userStore.isLoggedIn=true
//更新用户信息
mos.value?.showAlert(
"success",
t("message.login_successful"),
1000,
() => {
router.back()
}
);
break;
default:
mos.value?.showAlert("danger", t("message.server_error"), 5000);
break;
}
break;
default:
mos.value?.showAlert("danger", t("message.network_err"), 5000);
break;
}
}
);
} }
function functionupdataTitle() { function functionupdataTitle() {
@@ -75,7 +75,7 @@ function createAccount() {
userpass: userpassword.value?.value, userpass: userpassword.value?.value,
}, },
(r) => { (r) => {
console.log(r); //console.log(r);
switch (r.statusCode) { switch (r.statusCode) {
case 200: case 200:
switch (r.data.err_code) { switch (r.data.err_code) {