1
0
mirror of https://github.com/fumiama/paper-manager.git synced 2026-06-10 02:40:23 +08:00

finish backend login

This commit is contained in:
源文雨
2023-03-17 18:29:02 +08:00
parent 5889f0e30a
commit cd571e9e25
12 changed files with 844 additions and 279 deletions

37
backend/api/base.go Normal file
View File

@@ -0,0 +1,37 @@
package api
import (
"encoding/json"
"io"
)
const (
codeError = -1
codeSuccess = 0
codeTimeout = 401
)
const (
typeSuccess = "success"
typeError = "error"
)
const (
messageOk = "ok"
)
type base struct {
Code int `json:"code"`
Message string `json:"message"`
Result any `json:"result"`
Type string `json:"type"`
}
func writeresult(w io.Writer, c int, r any, m, t string) error {
return json.NewEncoder(w).Encode(&base{
Code: c,
Result: r,
Message: m,
Type: t,
})
}

142
backend/api/login.go Normal file
View File

@@ -0,0 +1,142 @@
package api
import (
"crypto/md5"
crand "crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"math/rand"
"sync/atomic"
"time"
"github.com/FloatTech/ttl"
"github.com/RomiChan/syncx"
base14 "github.com/fumiama/go-base16384"
"github.com/fumiama/paper-manager/backend/global"
)
var (
errNoSuchUser = errors.New("invalid username or password")
errTooManySalts = errors.New("too many salts")
errInvalidLoginStatus = errors.New("invalid login status")
errEmptySalt = errors.New("empty salt")
errWrongPassword = errors.New("invalid username or password")
errTooManyFailedLogins = errors.New("too many failed logins")
)
const (
loginStatusYes = iota - 1
loginStatusNo
loginStatusFail1
loginStatusFail2
loginStatusFail3
loginStatusFailLast
)
const maxSaltCount = 4
type saltinfo struct {
Salt string `json:"salt"`
count *uintptr
}
var (
loginsalts = ttl.NewCache[string, saltinfo](time.Minute)
loginstatus = syncx.Map[string, int]{}
)
func getLoginSalt(username string) (*saltinfo, error) {
if !global.UserDB.IsNameExists(username) {
return nil, errNoSuchUser
}
s, _ := loginstatus.Load(username)
if s != loginStatusNo {
return nil, errInvalidLoginStatus
}
salt := loginsalts.Get(username)
if salt.count != nil {
if atomic.AddUintptr(salt.count, 1) >= maxSaltCount {
time.AfterFunc(time.Minute*2, func() { atomic.StoreUintptr(salt.count, 0) })
return nil, errTooManySalts
}
if salt.Salt != "" {
return &salt, nil
}
}
buf := make([]byte, 7*(rand.Intn(8)+1))
_, err := crand.Read(buf)
if err != nil {
return nil, err
}
salt.Salt = base14.EncodeToString(buf)
salt.count = new(uintptr)
loginsalts.Set(username, salt)
return &salt, nil
}
type role struct {
RoleName string `json:"roleName"`
Value string `json:"value"`
}
type loginResult struct {
Roles []role `json:"roles"`
UserID int `json:"userId"`
Username string `json:"username"`
Token string `json:"token"`
RealName string `json:"realName"`
Desc string `json:"desc"`
}
var (
usertokens = ttl.NewCache[string, *global.User](time.Hour)
)
func login(username, challenge string) (*loginResult, error) {
if !global.UserDB.IsNameExists(username) {
return nil, errNoSuchUser
}
s, loaded := loginstatus.LoadOrStore(username, loginStatusFail1)
if loaded {
if s == loginStatusYes {
return nil, errInvalidLoginStatus
}
if s >= loginStatusFailLast {
return nil, errTooManyFailedLogins
}
loginstatus.Store(username, s+1)
}
salt := loginsalts.Get(username)
if salt.count == nil || salt.Salt == "" {
return nil, errEmptySalt
}
user, err := global.UserDB.GetUserByName(username)
if err != nil {
return nil, err
}
h := md5.New()
h.Write(base14.StringToBytes(user.Pswd))
h.Write(base14.StringToBytes(salt.Salt))
passchlg := hex.EncodeToString(h.Sum(make([]byte, 0, md5.Size)))
if passchlg != challenge {
return nil, errWrongPassword
}
var buf [6 * 8]byte
_, err = crand.Read(buf[:])
if err != nil {
return nil, err
}
token := base64.RawStdEncoding.EncodeToString(buf[:])
usertokens.Set(token, &user)
loginstatus.Store(username, loginStatusYes)
return &loginResult{
Roles: []role{{RoleName: user.Role.Nick(), Value: user.Role.String()}},
UserID: *user.ID,
Username: user.Name,
Token: token,
RealName: user.Nick,
Desc: user.Desc,
}, nil
}

View File

@@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"net/http"
"github.com/fumiama/paper-manager/backend/utils"
@@ -8,8 +9,62 @@ import (
// Handler serves all backend /api call
func Handler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/getLoginSalt" {
if !utils.IsMethod("GET", w, r) {
return
}
username := r.URL.Query().Get("username")
if username == "" {
http.Error(w, "400 Bad Request: empty username", http.StatusBadRequest)
return
}
salt, err := getLoginSalt(username)
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
}
writeresult(w, codeSuccess, salt, messageOk, typeSuccess)
return
}
if r.URL.Path == "/api/login" {
if !utils.IsMethod("POST", w, r) {
return
}
defer r.Body.Close()
var body loginbody
err := json.NewDecoder(r.Body).Decode(&body)
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
}
r, err := login(body.Username, body.Password)
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
}
writeresult(w, codeSuccess, r, messageOk, typeSuccess)
return
}
if r.URL.Path == "/api/getUserInfo" {
if !utils.IsMethod("GET", w, r) {
return
}
token := r.Header.Get("Authorization")
r, err := getUserInfo(token)
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
}
writeresult(w, codeSuccess, r, messageOk, typeSuccess)
return
}
if !utils.IsMethod("GET", w, r) {
return
}
http.Error(w, "404 Not Found", http.StatusNotFound)
}
type loginbody struct {
Username string `json:"username"`
Password string `json:"password"`
}

42
backend/api/user.go Normal file
View File

@@ -0,0 +1,42 @@
package api
import (
"errors"
"github.com/fumiama/paper-manager/backend/global"
)
var (
errInvalidToken = errors.New("invalid token")
)
type getUserInfoResult struct {
UserID int `json:"userId"`
Username string `json:"username"`
RealName string `json:"realName"`
Avatar string `json:"avatar"`
Desc string `json:"desc"`
HomePath string `json:"homePath"`
Roles []role `json:"roles"`
}
func getUserInfo(token string) (*getUserInfoResult, error) {
user := usertokens.Get(token)
if user == nil {
return nil, errInvalidToken
}
return &getUserInfoResult{
UserID: *user.ID,
Username: user.Name,
RealName: user.Nick,
Avatar: user.Avtr,
Desc: user.Desc,
HomePath: func() string {
if user.Role == global.RoleSuper {
return "/dashboard/analysis"
}
return "/dashboard/workbench"
}(),
Roles: []role{{RoleName: user.Role.Nick(), Value: user.Role.String()}},
}, nil
}

26
backend/global/db.go Normal file
View File

@@ -0,0 +1,26 @@
package global
import (
"sync"
sql "github.com/FloatTech/sqlite"
)
const (
userdbpath = DataFolder + "user.db"
filedbpath = DataFolder + "file.db"
)
type UserDatabase struct {
mu sync.RWMutex
db sql.Sqlite
}
type FileDatabase struct {
mu sync.RWMutex
db sql.Sqlite
}
var (
UserDB = UserDatabase{db: sql.Sqlite{DBPath: userdbpath}}
FileDB = FileDatabase{db: sql.Sqlite{DBPath: filedbpath}}
)

16
backend/global/file.go Normal file
View File

@@ -0,0 +1,16 @@
package global
import "time"
func init() {
err := FileDB.db.Open(time.Hour)
if err != nil {
panic(err)
}
}
type File struct {
ID *int
}
func (f *FileDatabase) AddFile() {}

440
backend/global/user.go Normal file
View File

@@ -0,0 +1,440 @@
package global
import (
"errors"
"strconv"
"strings"
"time"
"github.com/fumiama/paper-manager/backend/utils"
"github.com/sirupsen/logrus"
)
const (
RoleNil UserRole = iota
RoleSuper
RoleFileManager
RoleUser
)
type UserRole uint8
func (r UserRole) String() string {
switch r {
case RoleSuper:
return "super"
case RoleFileManager:
return "filemgr"
case RoleUser:
return "user"
}
return "nil"
}
func (r UserRole) Nick() string {
switch r {
case RoleSuper:
return "课程组长"
case RoleFileManager:
return "归档代理"
case RoleUser:
return "课程组员"
}
return "nil"
}
const (
UserTableUser = "user"
UserTableMessage = "msg"
)
var (
ErrInvalidRole = errors.New("invalid role")
ErrEmptyPassword = errors.New("empty password")
ErrEmptyName = errors.New("empty name")
ErrInvalidUsersCount = errors.New("invalid users count")
ErrEmptyUserID = errors.New("empty user ID")
ErrEmptyContect = errors.New("empty contact")
ErrUsernameExists = errors.New("username exists")
)
func init() {
isinit := utils.IsNotExist(UserDB.db.DBPath)
err := UserDB.db.Open(time.Hour)
if err != nil {
panic(err)
}
_, err = UserDB.db.DB.Exec("PRAGMA foreign_keys = ON;")
if err != nil {
panic(err)
}
err = UserDB.db.Create(UserTableUser, &User{})
if err != nil {
panic(err)
}
err = UserDB.db.Create(UserTableMessage, &Message{},
"FOREIGN KEY(ToID) REFERENCES "+UserTableUser+"(ID)",
)
if err != nil {
panic(err)
}
if isinit { // 添加初始账户
UserDB.AddUser(&User{
Role: RoleSuper,
Pswd: "123456",
Name: "fumiama",
Nick: "源文雨",
Avtr: "https://q1.qlogo.cn/g?b=qq&nk=1332524221&s=640",
Cont: "028-61830156",
Desc: "日は山の端にかかりぬ",
}, "系统")
logrus.Warn("[global.user] 初次启动, 创建初始账户 fumiama 密码 123456")
}
}
// User stores a user in table named UserTableUser
type User struct {
ID *int
Role UserRole
Date int64 // Date is the creating date's unix timestamp
Pswd string
Last int64 // Last is the last password reseting unix timestamp
Name string
Nick string
Avtr string // Avtr is the user's avatar, typically a image url
Cont string // Cont is the user's contact, ex. phone number
Desc string
}
// AddUser but cannot customize the ID field for it is self-increasing
func (u *UserDatabase) AddUser(user *User, opname string) error {
user.ID = nil
if user.Role == RoleNil || user.Role > RoleUser {
return ErrInvalidRole
}
if user.Pswd == "" {
return ErrEmptyPassword
}
if user.Name == "" {
return ErrEmptyName
}
if u.IsNameExists(user.Name) {
return ErrUsernameExists
}
user.Date = time.Now().Unix()
user.Last = user.Date
_ = u.notifyUserAdded(opname, user.Name)
u.mu.Lock()
defer u.mu.Unlock()
return u.db.InsertUnique(UserTableUser, user)
}
// UpdateUserInfo ...
func (u *UserDatabase) UpdateUserInfo(id int, nick, avtr, desc string) error {
user, err := u.GetUserByID(id)
if err != nil {
return err
}
if nick != "" {
user.Nick = nick
}
if avtr != "" {
user.Avtr = avtr
}
if desc != "" {
user.Desc = desc
}
u.mu.Lock()
defer u.mu.Unlock()
return u.db.Insert(UserTableUser, user)
}
// UpdateUserRole ...
func (u *UserDatabase) UpdateUserRole(id int, nr UserRole) error {
if nr == RoleNil || nr > RoleUser {
return ErrInvalidRole
}
user, err := u.GetUserByID(id)
if err != nil {
return err
}
user.Role = nr
u.mu.Lock()
defer u.mu.Unlock()
return u.db.Insert(UserTableUser, user)
}
// UpdateUserPassword ...
func (u *UserDatabase) UpdateUserPassword(id int, npwd string) error {
if npwd == "" {
return ErrEmptyPassword
}
user, err := u.GetUserByID(id)
if err != nil {
return err
}
user.Last = time.Now().Unix()
user.Pswd = npwd
_ = u.notifyPasswordChange(user.Name, npwd)
u.mu.Lock()
defer u.mu.Unlock()
return u.db.Insert(UserTableUser, user)
}
// UpdateUserContact ...
func (u *UserDatabase) UpdateUserContact(id int, ncont string) error {
if ncont == "" {
return ErrEmptyContect
}
user, err := u.GetUserByID(id)
if err != nil {
return err
}
user.Cont = ncont
_ = u.notifyContactChange(user.Name, ncont)
u.mu.Lock()
defer u.mu.Unlock()
return u.db.Insert(UserTableUser, user)
}
// GetUserByName avoids sql injection by removing ; ' " =
func (u *UserDatabase) GetUserByName(username string) (user User, err error) {
username = strings.NewReplacer(";", "", "'", "", `"`, "", "=", "").Replace(username)
u.mu.RLock()
err = u.db.Find(UserTableUser, &user, "WHERE Name='"+username+"'")
u.mu.RUnlock()
return
}
// IsNameExists avoids sql injection by removing ; ' " =
func (u *UserDatabase) IsNameExists(username string) bool {
username = strings.NewReplacer(";", "", "'", "", `"`, "", "=", "").Replace(username)
u.mu.RLock()
defer u.mu.RUnlock()
return u.db.CanFind(UserTableUser, "WHERE Name='"+username+"'")
}
// GetUserByID ...
func (u *UserDatabase) GetUserByID(id int) (user User, err error) {
u.mu.RLock()
err = u.db.Find(UserTableUser, &user, "WHERE ID="+strconv.Itoa(id))
u.mu.RUnlock()
return
}
// DelUserByID ...
func (u *UserDatabase) DelUserByID(id int) (err error) {
u.mu.Lock()
err = u.db.Del(UserTableUser, "WHERE ID="+strconv.Itoa(id))
u.mu.Unlock()
return
}
// GetUsers will set Pswd field to empty
func (u *UserDatabase) GetUsers() (users []User, err error) {
var user User
u.mu.RLock()
defer u.mu.RUnlock()
n, err := u.db.Count(UserTableUser)
if err != nil {
return
}
users = make([]User, n)
i := 0
err = u.db.FindFor(UserTableUser, &user, "", func() error {
user.Pswd = ""
users[i] = user
i++
if i >= n {
return ErrInvalidUsersCount
}
return nil
})
return
}
func (u *UserDatabase) GetSuperIDs() (ids []int, err error) {
var user User
ids = make([]int, 0, 16)
u.mu.RLock()
defer u.mu.RUnlock()
err = u.db.FindFor(UserTableUser, &user, "WHERE Role="+strconv.Itoa(int(RoleSuper)), func() error {
ids = append(ids, *user.ID)
return nil
})
return
}
// Message is shown in the workbench
type Message struct {
ID *int
ToID int // ToID user's ID
Date int64
Text string // Text is the message content
Name string // Name is the user's name to add in register message
Cont string // Cont is the user's phone number to add in register message or an operator's name in add user message
Pswd string // Pswd is the user's password to add in register message
}
// SendMessage will send a message
func (u *UserDatabase) SendMessage(m *Message) error {
m.ID = nil
m.Date = time.Now().Unix()
u.mu.Lock()
defer u.mu.Unlock()
return u.db.InsertUnique(UserTableMessage, &m)
}
// NotifyRegister will send register notification to all supers
func (u *UserDatabase) NotifyRegister(name, cont, pswd string) error {
if name == "" {
return ErrEmptyName
}
if pswd == "" {
return ErrEmptyPassword
}
tos, err := u.GetSuperIDs()
if err != nil {
return err
}
m := Message{
Date: time.Now().Unix(),
Text: "收到来自 " + name + " 的注册请求, 联系方式: " + cont,
Name: name,
Cont: cont,
Pswd: pswd,
}
u.mu.Lock()
defer u.mu.Unlock()
for _, to := range tos {
m.ToID = to
err = u.db.InsertUnique(UserTableMessage, &m)
if err != nil {
return err
}
}
return nil
}
// notifyUserAdded will send notification to all supers
func (u *UserDatabase) notifyUserAdded(opname, name string) error {
if opname == "" || name == "" {
return ErrEmptyName
}
tos, err := u.GetSuperIDs()
if err != nil {
return err
}
m := Message{
Date: time.Now().Unix(),
Text: opname + "添加了用户 " + name,
Name: name,
Cont: opname,
}
u.mu.Lock()
defer u.mu.Unlock()
for _, to := range tos {
m.ToID = to
err = u.db.InsertUnique(UserTableMessage, &m)
if err != nil {
return err
}
}
return nil
}
// notifyContactChange will send notification to all supers
func (u *UserDatabase) notifyContactChange(name, cont string) error {
if name == "" {
return ErrEmptyName
}
tos, err := u.GetSuperIDs()
if err != nil {
return err
}
m := Message{
Date: time.Now().Unix(),
Text: "用户 " + name + " 更改联系方式为: " + cont,
Name: name,
Cont: cont,
}
u.mu.Lock()
defer u.mu.Unlock()
for _, to := range tos {
m.ToID = to
err = u.db.InsertUnique(UserTableMessage, &m)
if err != nil {
return err
}
}
return nil
}
// notifyPasswordChange will send notification to all supers
func (u *UserDatabase) notifyPasswordChange(name, npwd string) error {
if name == "" {
return ErrEmptyName
}
tos, err := u.GetSuperIDs()
if err != nil {
return err
}
m := Message{
Date: time.Now().Unix(),
Text: "用户 " + name + " 更改了密码",
Name: name,
Pswd: npwd,
}
u.mu.Lock()
defer u.mu.Unlock()
for _, to := range tos {
m.ToID = to
err = u.db.InsertUnique(UserTableMessage, &m)
if err != nil {
return err
}
}
return nil
}
// GetMessagesOfUser set Pswd field to empty
func (u *UserDatabase) GetMessagesOfUser(to int) (ms []Message, err error) {
u.mu.RLock()
defer u.mu.RUnlock()
n, err := u.db.Count(UserTableMessage)
if err != nil {
return
}
ms = make([]Message, 0, n)
m := Message{}
err = u.db.FindFor(UserTableMessage, &m, "WHERE ToID="+strconv.Itoa(to), func() error {
m.Pswd = ""
ms = append(ms, m)
return nil
})
return
}
// GetMessageByID ...
func (u *UserDatabase) GetMessageByID(id int) (m Message, err error) {
u.mu.RLock()
err = u.db.Find(UserTableMessage, &m, "WHERE ID="+strconv.Itoa(id))
u.mu.RUnlock()
return
}
// DelMessageByID ...
func (u *UserDatabase) DelMessageByID(id int) (err error) {
u.mu.Lock()
err = u.db.Del(UserTableMessage, "WHERE ID="+strconv.Itoa(id))
u.mu.Unlock()
return
}

25
backend/utils/file.go Normal file
View File

@@ -0,0 +1,25 @@
package utils
import "os"
// IsExist 文件/路径存在
func IsExist(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}
// IsNotExist 文件/路径不存在
func IsNotExist(path string) bool {
_, err := os.Stat(path)
return err != nil && os.IsNotExist(err)
}
// FileSize 获取文件大小
func FileSize(path string) (n int64) {
stat, err := os.Stat(path)
if err != nil {
return
}
n = stat.Size()
return
}