mirror of
https://github.com/fumiama/paper-manager.git
synced 2026-06-08 01:24:55 +08:00
finish backend login
This commit is contained in:
37
backend/api/base.go
Normal file
37
backend/api/base.go
Normal 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
142
backend/api/login.go
Normal 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
|
||||
}
|
||||
@@ -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
42
backend/api/user.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user