web端支持管理员用户和普通用户区分,支持删除用户

This commit is contained in:
johlanse 2022-08-30 15:13:09 +08:00
parent d858723b2d
commit ba767ad611
18 changed files with 222 additions and 49 deletions

View File

@ -41,12 +41,12 @@ type Config struct {
QQ struct {
} `mapstructure:"qq"`
Web struct {
Enable bool `json:"enable" yaml:"enable" mapstructure:"enable"`
Account string `json:"account" yaml:"account" mapstructure:"account"`
Password string `json:"password" yaml:"password" mapstructure:"password"`
Host string `json:"host" yaml:"host" mapstructure:"host"`
Port int `json:"port" yaml:"port" mapstructure:"port"`
Announcement string `json:"announcement" yaml:"announcement" mapstructure:"announcement"`
Enable bool `json:"enable" yaml:"enable" mapstructure:"enable"`
Account string `json:"account" yaml:"account" mapstructure:"account"`
Password string `json:"password" yaml:"password" mapstructure:"password"`
Host string `json:"host" yaml:"host" mapstructure:"host"`
Port int `json:"port" yaml:"port" mapstructure:"port"`
CommonUser map[string]string `json:"common_user" mapstructure:"common_user"`
} `json:"web" mapstructure:"web"`
Cron string `json:"cron" yaml:"cron" mapstructure:"cron"`
CronRandomWait int `json:"cron_random_wait" yaml:"cron_random_wait" mapstructure:"cron_random_wait"`
@ -89,12 +89,9 @@ type Config struct {
AppKey string `json:"app_key" yaml:"app_key" mapstructure:"app_key"`
} `json:"ji_guang_push" yaml:"ji_guang_push" mapstructure:"ji_guang_push"`
SuperUser string `json:"super_user" yaml:"super_user" mapstructure:"super_user"`
SuperPassword string `json:"super_password" yaml:"super_password" mapstructure:"super_password"`
// github的代理地址用于检查更新或者其他的
GithubProxy string `json:"github_proxy" yaml:"github_proxy" mapstructure:"github_proxy"`
// 热重载
HotReload bool `json:"hot_reload" yaml:"hot_reload" mapstructure:"hot_reload"`
version string `mapstructure:"version"`

View File

@ -55,6 +55,10 @@ web:
account: admin
# 网页端登录密码
password: admin
# web端登录普通用户的账号密码支持多个用户,普通用户只能看到自己的信息
common_user:
# 代表账号为user,密码为123的普通用户可添加多个继续在下面写就好了
user: 123
# 微信公众号测试号配置
wechat:

View File

@ -6,6 +6,16 @@
然后查看报错内容截图并在[github](https://github.com/johlanse/study_xxqg/issues) 提交issue
```
+ ### 关于cookie的时间问题
```yaml
原理是是通过带上当前cookie访问一个api即可在1.0.35版本之后我通过cron定时执行保活默认的cron是 0 */1 * * *
目前暂不知道能够续期的次数
如果你想让访问间隔时间更短或者更长,可以通过添加环境变量 CHECK_ENV 为cron值
```
+ ### 浏览器中登录不上怎么办?显示一个白条没反应
```yaml

View File

@ -81,17 +81,25 @@ web:
host: 0.0.0.0
# 监听的端口号 0-65535可选
port: 8081
# web端登录账号
# web端登录管理员的账号
accountadmin
# web端登录的密码
# web端登录管理员的密码
password: admin
# web端登录普通用户的账号密码支持多个用户,普通用户只能看到自己的信息
common_user:
# 代表账号为user,密码为123的普通用户可添加多个继续在下面写就好了
user: 123
# user1: 123
# user2: 123
```
+ 开启后通过浏览器访问 *http://ip:port*或者*http://ip:port/new*即可打开网址 ,若为docker运行则ip为宿主机公网ip,端口为docker映射到宿主机的端口
+ 若无法访问首先检查程序运行日志查看有无报错其次查看docker的运行情况端口是否映射正常然后可以通过curl命令检测在宿主机中能否访问然后检查防火墙之类的
+ 若点击登录之后出现一个小框然后无反应,则说明账户密码错误,请重新配置程序账户密码并重启程序
> 登录的账号密码是在配置文件中配置,不是学习强国的登录账号
> 登录的账号密码是在配置文件中配置,不是学习强国的登录账号,管理员登录支持删除用户,同时能看到所有人的用户信息,普通用户就是```common_user```下面配置的用户,支持多个用户,键是账号,值是密码
### 钉钉推送
配置config.yml的如下部分,具体使用教程详情参考[钉钉](https://developers.dingtalk.com/document/robots/custom-robot-access?spm=ding_open_doc.document.0.0.7f875e5903iVpC#topic-2026027)

View File

@ -4,10 +4,12 @@ package model
import (
"net/http"
"os"
"sync"
"time"
"github.com/playwright-community/playwright-go"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"github.com/johlanse/study_xxqg/utils"
@ -66,7 +68,8 @@ func Query() ([]*User, error) {
return nil, err
}
if u.Status != 0 {
if utils.CheckUserCookie(u.ToCookies()) {
if ok, _ := utils.CheckUserCookie(u.ToCookies()); ok {
users = append(users, u)
} else {
log.Warningln(u.Nick + "的cookie已失效")
@ -140,7 +143,7 @@ func QueryByPushID(pushID string) ([]*User, error) {
return users, err
}
if u.Status != 0 {
if utils.CheckUserCookie(u.ToCookies()) {
if ok, _ := utils.CheckUserCookie(u.ToCookies()); ok {
users = append(users, u)
} else {
log.Warningln(u.Nick + "的cookie已失效")
@ -304,7 +307,14 @@ func check() {
log.Errorf("%v 出现错误,%v", "auth check", err)
}
}()
for {
c := cron.New()
cr := "0 */1 * * *"
if crEnv, ok := os.LookupEnv("CHECK_ENV"); ok {
cr = crEnv
log.Infoln("已成功自定义保活cron : " + cr)
}
_, err := c.AddFunc(cr, func() {
log.Infoln("开始执行保活任务")
users, _ := Query()
for _, user := range users {
response, _ := utils.GetClient().R().SetCookies(user.ToCookies()...).Get("https://pc-api.xuexi.cn/open/api/auth/check")
@ -314,12 +324,16 @@ func check() {
token = cookie.Value
}
}
if token != "" {
if token != "" && user.Token != token {
user.Token = token
_ = UpdateUser(user)
log.Infoln("用户" + user.Nick + "的ck已成功保活cookie")
}
}
time.Sleep(time.Hour * time.Duration(2))
})
if err != nil {
log.Errorln("添加保活任务失败" + err.Error())
return
}
c.Start()
}

View File

@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/guonaihong/gout"
@ -20,10 +21,15 @@ func (d *Ding) Send() func(id string, kind string, message string) {
s := TypeSecret{Secret: d.Secret, Webhook: d.Token}
return func(id string, kind string, message string) {
if kind == "flush" {
if strings.Contains(message, "login.xuexi.cn") {
message = fmt.Sprintf("[点我登录](%v)", message)
}
err := s.SendMessage(map[string]interface{}{
"msgtype": "markdown",
"markdown": map[string]string{
"title": "学习强国登录",
"title": "study_xxqg信息推送",
"text": message,
},
})

View File

@ -211,6 +211,22 @@ func handleRestart(id string) {
// @param message
//
func sendMsg(id, message string) {
// 登录消息单独采用模板发送
if strings.Contains(message, "login.xuexi.cn") {
_, err := wx.SendTemplateMessage(&mp.TemplateMessage{
ToUser: id,
TemplateId: conf.GetConfig().Wechat.LoginTempID,
URL: message,
TopColor: "",
RawJSONData: nil,
})
if err != nil {
log.Errorln(err.Error())
return
}
return
}
m := map[string]interface{}{
"data": map[string]string{
"value": message,

View File

@ -48,17 +48,17 @@ func GetAbout() string {
* @param user
* @return bool
*/
func CheckUserCookie(cookies []*http.Cookie) bool {
func CheckUserCookie(cookies []*http.Cookie) (bool, error) {
client := req.C().DevMode()
response, err := client.R().SetCookies(cookies...).Get("https://pc-api.xuexi.cn/open/api/score/get")
if err != nil {
log.Errorln("获取用户总分错误" + err.Error())
return false
return true, err
}
if !gjson.GetBytes(response.Bytes(), "ok").Bool() {
return false
return false, err
}
return true
return true, err
}
var (

View File

@ -37,7 +37,15 @@ func checkToken() gin.HandlerFunc {
ctx.JSON(200, Resp{
Code: 200,
Message: "",
Data: nil,
Data: 1,
Success: true,
Error: "",
})
} else if checkCommonUser(token) {
ctx.JSON(200, Resp{
Code: 200,
Message: "",
Data: 2,
Success: true,
Error: "",
})
@ -63,6 +71,14 @@ func userLogin() gin.HandlerFunc {
_ = ctx.BindJSON(u)
config := conf.GetConfig()
if u.Account == config.Web.Account && u.Password == config.Web.Password {
ctx.JSON(200, Resp{
Code: 200,
Message: "登录成功,尊贵的管理员用户",
Data: utils.StrMd5(u.Account + u.Password),
Success: true,
Error: "",
})
} else if checkCommonUser(utils.StrMd5(u.Account + u.Password)) {
ctx.JSON(200, Resp{
Code: 200,
Message: "登录成功",
@ -163,6 +179,23 @@ func getUsers() gin.HandlerFunc {
})
return
}
level := ctx.GetInt("level")
if level != 1 {
users, err = model.QueryByPushID(ctx.GetString("token"))
if err != nil {
return
}
if users == nil {
ctx.JSON(200, Resp{
Code: 200,
Message: "查询成功",
Data: []interface{}{},
Success: true,
Error: "",
})
return
}
}
var datas []map[string]interface{}
for _, user := range users {
@ -326,6 +359,17 @@ func generate() gin.HandlerFunc {
func deleteUser() gin.HandlerFunc {
return func(ctx *gin.Context) {
uid := ctx.Query("uid")
level := ctx.GetInt("level")
if level != 1 {
ctx.JSON(200, Resp{
Code: 401,
Message: "你没有权限删除用户!",
Data: "",
Success: false,
Error: "你没有权限删除用户!",
})
return
}
err := model.DeleteUser(uid)
if err != nil {
ctx.JSON(200, Resp{

View File

@ -87,7 +87,7 @@ func check() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := ctx.GetHeader("Authorization")
token = strings.Split(token, " ")[1]
if token == "" || (utils.StrMd5(config.Web.Account+config.Web.Password) != token) {
if token == "" {
ctx.JSON(401, Resp{
Code: 401,
Message: "the auth fail",
@ -96,8 +96,33 @@ func check() gin.HandlerFunc {
Error: "",
})
ctx.Abort()
} else {
} else if utils.StrMd5(config.Web.Account+config.Web.Password) == token {
ctx.Set("level", 1)
ctx.Set("token", token)
ctx.Next()
} else if checkCommonUser(token) {
ctx.Set("level", 2)
ctx.Set("token", token)
ctx.Next()
} else {
ctx.JSON(401, Resp{
Code: 401,
Message: "the auth fail",
Data: nil,
Success: false,
Error: "",
})
ctx.Abort()
}
}
}
func checkCommonUser(token string) bool {
config := conf.GetConfig()
for key, value := range config.Web.CommonUser {
if token == utils.StrMd5(key+value) {
return true
}
}
return false
}

View File

@ -1,15 +1,15 @@
{
"files": {
"main.css": "/static/xxqg/build/static/css/main.6f1e3389.css",
"main.js": "/static/xxqg/build/static/js/main.8720e9a4.js",
"main.js": "/static/xxqg/build/static/js/main.b72f3ffd.js",
"static/js/787.273d6ce9.chunk.js": "/static/xxqg/build/static/js/787.273d6ce9.chunk.js",
"index.html": "/static/xxqg/build/index.html",
"main.6f1e3389.css.map": "/static/xxqg/build/static/css/main.6f1e3389.css.map",
"main.8720e9a4.js.map": "/static/xxqg/build/static/js/main.8720e9a4.js.map",
"main.b72f3ffd.js.map": "/static/xxqg/build/static/js/main.b72f3ffd.js.map",
"787.273d6ce9.chunk.js.map": "/static/xxqg/build/static/js/787.273d6ce9.chunk.js.map"
},
"entrypoints": [
"static/css/main.6f1e3389.css",
"static/js/main.8720e9a4.js"
"static/js/main.b72f3ffd.js"
]
}

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/static/xxqg/build/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/static/xxqg/build/logo192.png"/><link rel="manifest" href="/static/xxqg/build/manifest.json"/><title>Study XXQG</title><script defer="defer" src="/static/xxqg/build/static/js/main.8720e9a4.js"></script><link href="/static/xxqg/build/static/css/main.6f1e3389.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/static/xxqg/build/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/static/xxqg/build/logo192.png"/><link rel="manifest" href="/static/xxqg/build/manifest.json"/><title>Study XXQG</title><script defer="defer" src="/static/xxqg/build/static/js/main.b72f3ffd.js"></script><link href="/static/xxqg/build/static/css/main.6f1e3389.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@ import './App.css';
import {Button, Dialog, Divider, Form, Input, List, Modal, NavBar, Popup, TextArea, Toast,} from "antd-mobile";
import {UnorderedListOutline} from "antd-mobile-icons";
import {ListItem} from "antd-mobile/es/components/list/list-item";
import {checkQrCode, getLog, getScore, getToken, getUsers, getLink, stopStudy, study, login, checkToken,getAbout} from "./utils/api";
import {checkQrCode, getLog, getScore, getToken, getUsers, getLink, stopStudy, study, login, checkToken,getAbout,deleteUser} from "./utils/api";
import QrCode from 'qrcode.react';
import * as util from "util";
@ -14,24 +14,46 @@ class App extends React.Component<any, any> {
this.state = {
popup_visible: false,
index: "login",
is_login: false
is_login: false,
// 用户等级1是管理员2是普通用户
level: 2
};
}
set_level = (level:number)=>{
this.setState({
level: level
})
}
set_login = ()=>{
this.setState({
is_login: true
})
this.check_token()
}
check_token = ()=>{
checkToken().then((t) =>{
console.log(t)
if (!t){
console.log("未登录")
}else {
if (t.data === 1){
console.log("管理员登录")
this.set_level(1)
}else {
console.log("不是管理员登录")
this.set_level(2)
}
this.setState({
is_login: true
})
}
})
}
componentDidMount() {
checkToken().then((t) =>{
console.log(t)
if (t){
this.set_login()
}
})
this.check_token()
}
@ -43,7 +65,7 @@ class App extends React.Component<any, any> {
left={<UnorderedListOutline fontSize={36} onClick={this.back}/>}>
{"study_xxqg"}
</NavBar>
<Router data={this.state.index}/>
<Router data={this.state.index} level={this.state.level} set_level = {this.set_level}/>
<Popup
bodyStyle={{width: '50vw'}}
visible={this.state.popup_visible}
@ -97,11 +119,10 @@ class Login extends Component<any, any>{
onFinish = (value:string)=>{
login(JSON.stringify(value)).then(resp => {
console.log(resp.message)
Dialog.alert({content: resp.message,closeOnMaskClick:false})
if (resp.success){
window.localStorage.setItem("xxqg_token",resp.data)
this.props.parent.set_login()
}else {
Dialog.alert({content: resp.message,closeOnMaskClick:false})
}
})
@ -154,7 +175,7 @@ class Router extends Component<any, any>{
<QrCode style={{margin:10}} fgColor={"#000000"} size={200} value={this.state.img} />
</>;
let userList = <Users data={"12"}/>;
let userList = <Users data={"12"} level={this.props.level}/>;
let config = <h1></h1>
let help = <Help />
let log = <Log />
@ -364,10 +385,29 @@ class Users extends Component<any, any>{
}
}
delete_user = (uid:string,nick:string)=>{
Dialog.confirm({content:"你确定要删除用户"+nick+"吗?"}).then((confirm) => {
if (confirm){
deleteUser(uid).then((data) => {
if (data.success){
getUsers().then(users =>{
console.log(users)
this.setState({
users: users.data
})
})
}else {
Dialog.show({content:data.error})
}
})
}
})
}
render() {
let elements = []
for (let i = 0; i < this.state.users.length; i++) {
console.log(this.props.level)
elements.push(
<>
<ListItem key={this.state.users[i].uid} style={{border:"blue soild 1px"}}>
@ -379,6 +419,8 @@ class Users extends Component<any, any>{
</Button>
<br />
<Button onClick={this.getScore.bind(this,this.state.users[i].token,this.state.users[i].nick)} color={"success"} block={true}></Button>
<br />
<Button style={{display: this.props.level !== 1 ? "none" : "inline"}} onClick={this.delete_user.bind(this,this.state.users[i].uid,this.state.users[i].nick)} color={"danger"} block={true}></Button>
</ListItem>
<Divider />
</>

View File

@ -24,12 +24,13 @@ export async function checkToken() {
return false
}
let responseData = await http.post(base + "/auth/check/"+token);
return responseData.data.success;
return responseData.data;
}
export async function login(data) {
let responseData = await http.post(base+"/auth/login",data);
return responseData.data;
@ -54,13 +55,19 @@ export async function getAbout(){
}
export async function getToken(code,sign){
let resp = await http.post(base+"/user/",{
let token = window.localStorage.getItem("xxqg_token")
let resp = await http.post(base+"/user?register_id="+token,{
"code":code,
"state":sign
});
return resp.data;
}
export async function deleteUser(uid){
let resp = await http.delete(base+"/user?uid="+uid);
return resp.data;
}
export async function getUsers(){
let resp = await http.get(base+"/user");
return resp.data