feat: add login log record
This commit is contained in:
@@ -66,6 +66,7 @@ func Setup() {
|
|||||||
&models.Settings{},
|
&models.Settings{},
|
||||||
&models.CronMessages{},
|
&models.CronMessages{},
|
||||||
&models.HostedMessage{},
|
&models.HostedMessage{},
|
||||||
|
&models.LoginLog{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ func CheckAuth(username, password string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserByUsername(username string) (*Auth, error) {
|
||||||
|
var auth Auth
|
||||||
|
err := db.Where("username = ?", username).First(&auth).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
// EditUser 编辑用户信息
|
// EditUser 编辑用户信息
|
||||||
func EditUser(username string, data map[string]interface{}) error {
|
func EditUser(username string, data map[string]interface{}) error {
|
||||||
if err := db.Model(&Auth{}).Where("username = ? ", username).Updates(data).Error; err != nil {
|
if err := db.Model(&Auth{}).Where("username = ? ", username).Updates(data).Error; err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "message-nest/pkg/util"
|
||||||
|
|
||||||
|
type LoginLog struct {
|
||||||
|
ID uint `gorm:"autoIncrement;type:integer;primaryKey" json:"id"`
|
||||||
|
UserID int `json:"user_id" gorm:"type:int;index"`
|
||||||
|
Username string `json:"username" gorm:"type:varchar(100);default:'';index"`
|
||||||
|
IP string `json:"ip" gorm:"type:varchar(64);default:'';"`
|
||||||
|
UA string `json:"ua" gorm:"type:varchar(512);default:'';"`
|
||||||
|
CreatedAt util.Time `json:"created_on" gorm:"column:created_on;autoCreateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddLoginLog(userID int, username string, ip string, ua string) error {
|
||||||
|
log := LoginLog{UserID: userID, Username: username, IP: ip, UA: ua}
|
||||||
|
return db.Create(&log).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRecentLoginLogs(limit int) ([]LoginLog, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 8
|
||||||
|
}
|
||||||
|
var logs []LoginLog
|
||||||
|
err := db.Model(&LoginLog{}).Order("id DESC").Limit(limit).Find(&logs).Error
|
||||||
|
return logs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+14
-8
@@ -1,15 +1,16 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"message-nest/pkg/app"
|
"message-nest/pkg/app"
|
||||||
"message-nest/pkg/e"
|
"message-nest/pkg/e"
|
||||||
"message-nest/pkg/util"
|
"message-nest/pkg/util"
|
||||||
"message-nest/service/auth_service"
|
"message-nest/service/auth_service"
|
||||||
|
"message-nest/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type auth struct {
|
type auth struct {
|
||||||
@@ -51,7 +52,12 @@ func GetAuth(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
appG.CResponse(http.StatusOK, "登录成功!", map[string]string{
|
// 查询用户ID并记录登录日志
|
||||||
|
if u, _ := models.GetUserByUsername(req.Username); u != nil {
|
||||||
|
_ = models.AddLoginLog(u.ID, req.Username, c.ClientIP(), c.GetHeader("User-Agent"))
|
||||||
|
}
|
||||||
|
|
||||||
|
appG.CResponse(http.StatusOK, "登录成功!", map[string]string{
|
||||||
"token": token,
|
"token": token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-nest/pkg/app"
|
||||||
|
"message-nest/pkg/e"
|
||||||
|
"message-nest/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRecentLoginLogs 最近登录日志(默认8条)
|
||||||
|
func GetRecentLoginLogs(c *gin.Context) {
|
||||||
|
appG := app.Gin{C: c}
|
||||||
|
logs, err := models.GetRecentLoginLogs(8)
|
||||||
|
if err != nil {
|
||||||
|
appG.CResponse(e.ERROR, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appG.CResponse(e.SUCCESS, "success", gin.H{
|
||||||
|
"lists": logs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -82,6 +82,9 @@ func InitRouter(f embed.FS) *gin.Engine {
|
|||||||
apiV1.POST("/settings/reset", v1.RestDefaultSettings)
|
apiV1.POST("/settings/reset", v1.RestDefaultSettings)
|
||||||
apiV1.GET("/settings/getsetting", v1.GetUserSetting)
|
apiV1.GET("/settings/getsetting", v1.GetUserSetting)
|
||||||
|
|
||||||
|
// login logs
|
||||||
|
apiV1.GET("/loginlogs/recent", v1.GetRecentLoginLogs)
|
||||||
|
|
||||||
// statistic
|
// statistic
|
||||||
apiV1.GET("/statistic", v1.GetStatisticData)
|
apiV1.GET("/statistic", v1.GetStatisticData)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
// @ts-ignore
|
||||||
|
import { request } from '@/api/api'
|
||||||
|
|
||||||
|
interface LoginLog {
|
||||||
|
id: number
|
||||||
|
user_id: number
|
||||||
|
username: string
|
||||||
|
ip: string
|
||||||
|
ua: string
|
||||||
|
created_on: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const logs = ref<LoginLog[]>([])
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const rsp = await request.get('/loginlogs/recent')
|
||||||
|
const data = rsp.data
|
||||||
|
if (data && data.code === 200 && data.data) {
|
||||||
|
logs.value = data.data.lists || []
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchLogs)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-semibold">最近登录日志</h2>
|
||||||
|
<button class="text-sm text-muted-foreground hover:text-foreground" @click="fetchLogs" :disabled="loading">
|
||||||
|
{{ loading ? '刷新中...' : '刷新' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-hidden rounded-md border border-border">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-muted">
|
||||||
|
<th class="px-3 py-2 text-left">用户名</th>
|
||||||
|
<th class="px-3 py-2 text-left">IP</th>
|
||||||
|
<th class="px-3 py-2 text-left">UA</th>
|
||||||
|
<th class="px-3 py-2 text-left">登录时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="!logs.length">
|
||||||
|
<td colspan="4" class="px-3 py-6">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<span class="text-sm text-muted-foreground">暂无数据</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="item in logs" :key="item.id" class="border-t border-border">
|
||||||
|
<td class="px-3 py-2">{{ item.username }}</td>
|
||||||
|
<td class="px-3 py-2">{{ item.ip }}</td>
|
||||||
|
<td class="px-3 py-2 truncate max-w-[420px]" :title="item.ua">{{ item.ua }}</td>
|
||||||
|
<td class="px-3 py-2">{{ item.created_on }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@ import PasswordSettings from './PasswordSettings.vue'
|
|||||||
import LogsSettings from './LogsSettings.vue'
|
import LogsSettings from './LogsSettings.vue'
|
||||||
import SiteSettings from './SiteSettings.vue'
|
import SiteSettings from './SiteSettings.vue'
|
||||||
import AboutSettings from './AboutSettings.vue'
|
import AboutSettings from './AboutSettings.vue'
|
||||||
|
import LoginLogs from './LoginLogs.vue'
|
||||||
|
|
||||||
// 当前选中的设置项
|
// 当前选中的设置项
|
||||||
const activeTab = ref('password')
|
const activeTab = ref('password')
|
||||||
@@ -30,6 +31,9 @@ const activeTab = ref('password')
|
|||||||
<!-- 站点设置 -->
|
<!-- 站点设置 -->
|
||||||
<SiteSettings v-if="activeTab === 'site'" />
|
<SiteSettings v-if="activeTab === 'site'" />
|
||||||
|
|
||||||
|
<!-- 登录日志 -->
|
||||||
|
<LoginLogs v-if="activeTab === 'loginlogs'" />
|
||||||
|
|
||||||
<!-- 站点关于 -->
|
<!-- 站点关于 -->
|
||||||
<AboutSettings v-if="activeTab === 'about'" />
|
<AboutSettings v-if="activeTab === 'about'" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ defineEmits<Emits>()
|
|||||||
const settingsMenu = [
|
const settingsMenu = [
|
||||||
{ id: 'password', name: '重置密码', icon: KeyIcon },
|
{ id: 'password', name: '重置密码', icon: KeyIcon },
|
||||||
{ id: 'logs', name: '日志清理', icon: TrashIcon },
|
{ id: 'logs', name: '日志清理', icon: TrashIcon },
|
||||||
|
{ id: 'loginlogs', name: '登录日志', icon: InfoIcon },
|
||||||
{ id: 'site', name: '站点设置', icon: SettingsIcon },
|
{ id: 'site', name: '站点设置', icon: SettingsIcon },
|
||||||
{ id: 'about', name: '站点关于', icon: InfoIcon }
|
{ id: 'about', name: '站点关于', icon: InfoIcon }
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user