web 使用专用登录接口

This commit is contained in:
xmdhs 2023-10-09 20:58:33 +08:00
parent 63808f0a14
commit 74fd0902ac
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
20 changed files with 293 additions and 125 deletions

23
cmd/authlibskin/main.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"context"
"flag"
"os"
"github.com/xmdhs/authlib-skin/config"
)
var configPath string
func init() {
flag.StringVar(&configPath, "c", "", "")
flag.Parse()
}
func main() {
ctx := context.Background()
os.ReadFile(configPath)
config.YamlDeCode()
}

16
config/yaml.go Normal file
View File

@ -0,0 +1,16 @@
package config
import (
"fmt"
"gopkg.in/yaml.v3"
)
func YamlDeCode(b []byte) (Config, error) {
var c Config
err := yaml.Unmarshal(b, &c)
if err != nil {
return c, fmt.Errorf("YamlDeCode: %w", err)
}
return c, nil
}

View File

@ -1,24 +1,21 @@
import type { tokenData, ApiUser, ApiServerInfo, YggProfile, ApiConfig, List, UserInfo, EditUser } from '@/apis/model' import type { tokenData, ApiUser, ApiServerInfo, YggProfile, ApiConfig, List, UserInfo, EditUser } from '@/apis/model'
import { apiGet } from '@/apis/utils' import { apiGet } from '@/apis/utils'
export async function login(username: string, password: string) { export async function login(email: string, password: string, captchaToken: string) {
const v = await fetch(import.meta.env.VITE_APIADDR + "/api/yggdrasil/authserver/authenticate", { const v = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/user/login", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
"username": username, "email": email,
"password": password, "password": password,
"CaptchaToken": captchaToken
}) })
}) })
const data = await v.json() return await apiGet<tokenData>(v)
if (!v.ok) {
throw new Error(data?.errorMessage)
}
return data as tokenData
} }
export async function register(email: string, username: string, password: string, captchaToken: string) { export async function register(email: string, username: string, password: string, captchaToken: string) {
const v = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/user/reg", { const v = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/user/reg", {
method: "PUT", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
"Email": email, "Email": email,
"Password": password, "Password": password,
@ -119,7 +116,7 @@ export async function ListUser(page: number, token: string, email: string, name:
} }
export async function editUser(u: EditUser, token: string, uid: string) { export async function editUser(u: EditUser, token: string, uid: string) {
const r = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/admin/user/" + uid,{ const r = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/admin/user/" + uid, {
method: "POST", method: "POST",
headers: { headers: {
"Authorization": "Bearer " + token "Authorization": "Bearer " + token

View File

@ -1,9 +1,7 @@
export interface tokenData { export interface tokenData {
accessToken: string token: string
selectedProfile: { name: string
name: string uuid: string
id: string
}
} }
export interface Api<T> { export interface Api<T> {

View File

@ -18,6 +18,9 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
import Loading from '@/components/Loading' import Loading from '@/components/Loading'
import CheckInput, { refType } from '@/components/CheckInput' import CheckInput, { refType } from '@/components/CheckInput'
import useTitle from '@/hooks/useTitle'; import useTitle from '@/hooks/useTitle';
import CaptchaWidget from '@/components/CaptchaWidget';
import type { refType as CaptchaWidgetRef } from '@/components/CaptchaWidget'
import { ApiErr } from '@/apis/error';
@ -29,6 +32,8 @@ export default function SignIn() {
const checkList = React.useRef<Map<string, refType>>(new Map<string, refType>()) const checkList = React.useRef<Map<string, refType>>(new Map<string, refType>())
const navigate = useNavigate(); const navigate = useNavigate();
useTitle("登录") useTitle("登录")
const captchaRef = React.useRef<CaptchaWidgetRef>(null)
const [captchaToken, setCaptchaToken] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
@ -44,17 +49,33 @@ export default function SignIn() {
if (loading) return if (loading) return
setLoading(true) setLoading(true)
login(postData.email!, postData.password ?? ""). login(postData.email!, postData.password ?? "", captchaToken).
then(v => { then(v => {
if (!v) return if (!v) return
setToken(v.accessToken) setToken(v.token)
setUserInfo({ setUserInfo({
uuid: v.selectedProfile.id, uuid: v.uuid,
name: v.selectedProfile.name, name: v.name,
}) })
navigate("/profile") navigate("/profile")
}). }).
catch(v => [setErr(String(v)), console.warn(v)]). catch(v => {
captchaRef.current?.reload()
if (v instanceof ApiErr) {
switch (v.code) {
case 6:
setErr("错误的密码")
break
case 9:
setErr("用户已被禁用")
break
}
return
}
setErr(String(v))
console.warn(v)
}).
finally(() => setLoading(false)) finally(() => setLoading(false))
}; };
@ -104,6 +125,7 @@ export default function SignIn() {
id="password" id="password"
autoComplete="current-password" autoComplete="current-password"
/> />
<CaptchaWidget ref={captchaRef} onSuccess={setCaptchaToken} />
<Button <Button
type="submit" type="submit"
fullWidth fullWidth

1
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/google/wire v0.5.0 github.com/google/wire v0.5.0
github.com/samber/lo v1.38.1 github.com/samber/lo v1.38.1
golang.org/x/crypto v0.7.0 golang.org/x/crypto v0.7.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (

1
go.sum
View File

@ -116,6 +116,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -2,15 +2,12 @@ package handle
import ( import (
"context" "context"
"errors"
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/xmdhs/authlib-skin/model" "github.com/xmdhs/authlib-skin/model"
"github.com/xmdhs/authlib-skin/service"
"github.com/xmdhs/authlib-skin/service/utils"
U "github.com/xmdhs/authlib-skin/utils" U "github.com/xmdhs/authlib-skin/utils"
) )
@ -28,11 +25,7 @@ func (h *Handel) NeedAuth(handle http.Handler) http.Handler {
} }
t, err := h.webService.Auth(ctx, token) t, err := h.webService.Auth(ctx, token)
if err != nil { if err != nil {
if errors.Is(err, utils.ErrTokenInvalid) { h.handleErrorService(ctx, w, err)
h.handleError(ctx, w, err.Error(), model.ErrAuth, 401, slog.LevelDebug)
return
}
h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn)
return return
} }
r = r.WithContext(context.WithValue(ctx, tokenKey, t)) r = r.WithContext(context.WithValue(ctx, tokenKey, t))
@ -46,11 +39,7 @@ func (h *Handel) NeedAdmin(handle http.Handler) http.Handler {
t := ctx.Value(tokenKey).(*model.TokenClaims) t := ctx.Value(tokenKey).(*model.TokenClaims)
err := h.webService.IsAdmin(ctx, t) err := h.webService.IsAdmin(ctx, t)
if err != nil { if err != nil {
if errors.Is(err, service.ErrNotAdmin) { h.handleErrorService(ctx, w, err)
h.handleError(ctx, w, err.Error(), model.ErrNotAdmin, 401, slog.LevelDebug)
return
}
h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn)
return return
} }
handle.ServeHTTP(w, r) handle.ServeHTTP(w, r)
@ -78,7 +67,7 @@ func (h *Handel) ListUser() http.HandlerFunc {
ul, uc, err := h.webService.ListUser(ctx, pagei, email, name) ul, uc, err := h.webService.ListUser(ctx, pagei, email, name)
if err != nil { if err != nil {
h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn) h.handleErrorService(ctx, w, err)
return return
} }
encodeJson(w, model.API[model.List[model.UserList]]{Data: model.List[model.UserList]{List: ul, Total: uc}}) encodeJson(w, model.API[model.List[model.UserList]]{Data: model.List[model.UserList]{List: ul, Total: uc}})
@ -106,7 +95,7 @@ func (h *Handel) EditUser() http.HandlerFunc {
} }
err = h.webService.EditUser(ctx, a, uidi) err = h.webService.EditUser(ctx, a, uidi)
if err != nil { if err != nil {
h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn) h.handleErrorService(ctx, w, err)
return return
} }
encodeJson[any](w, model.API[any]{ encodeJson[any](w, model.API[any]{

View File

@ -3,12 +3,53 @@ package handle
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"log/slog" "log/slog"
"net/http" "net/http"
"github.com/xmdhs/authlib-skin/model" "github.com/xmdhs/authlib-skin/model"
"github.com/xmdhs/authlib-skin/service"
"github.com/xmdhs/authlib-skin/service/utils"
sutils "github.com/xmdhs/authlib-skin/service/utils"
) )
func (h *Handel) handleErrorService(ctx context.Context, w http.ResponseWriter, err error) {
if errors.Is(err, service.ErrExistUser) {
h.handleError(ctx, w, err.Error(), model.ErrExistUser, 400, slog.LevelDebug)
return
}
if errors.Is(err, service.ErrExitsName) {
h.handleError(ctx, w, err.Error(), model.ErrExitsName, 400, slog.LevelDebug)
return
}
if errors.Is(err, service.ErrRegLimit) {
h.handleError(ctx, w, err.Error(), model.ErrRegLimit, 400, slog.LevelDebug)
return
}
if errors.Is(err, service.ErrCaptcha) {
h.handleError(ctx, w, err.Error(), model.ErrCaptcha, 400, slog.LevelDebug)
return
}
if errors.Is(err, service.ErrPassWord) {
h.handleError(ctx, w, err.Error(), model.ErrPassWord, 401, slog.LevelDebug)
return
}
if errors.Is(err, sutils.ErrUserDisable) {
h.handleError(ctx, w, err.Error(), model.ErrUserDisable, 401, slog.LevelDebug)
return
}
if errors.Is(err, service.ErrNotAdmin) {
h.handleError(ctx, w, err.Error(), model.ErrNotAdmin, 401, slog.LevelDebug)
return
}
if errors.Is(err, utils.ErrTokenInvalid) {
h.handleError(ctx, w, err.Error(), model.ErrAuth, 401, slog.LevelDebug)
return
}
h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn)
}
func (h *Handel) handleError(ctx context.Context, w http.ResponseWriter, msg string, code model.APIStatus, httpcode int, level slog.Level) { func (h *Handel) handleError(ctx context.Context, w http.ResponseWriter, msg string, code model.APIStatus, httpcode int, level slog.Level) {
h.logger.Log(ctx, level, msg) h.logger.Log(ctx, level, msg)
w.WriteHeader(httpcode) w.WriteHeader(httpcode)

View File

@ -1,12 +1,10 @@
package handle package handle
import ( import (
"errors"
"log/slog" "log/slog"
"net/http" "net/http"
"github.com/xmdhs/authlib-skin/model" "github.com/xmdhs/authlib-skin/model"
"github.com/xmdhs/authlib-skin/service"
"github.com/xmdhs/authlib-skin/utils" "github.com/xmdhs/authlib-skin/utils"
) )
@ -20,7 +18,7 @@ func (h *Handel) Reg() http.HandlerFunc {
return return
} }
u, err := utils.DeCodeBody[model.User](r.Body, h.validate) u, err := utils.DeCodeBody[model.UserReg](r.Body, h.validate)
if err != nil { if err != nil {
h.handleError(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug) h.handleError(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
return return
@ -32,19 +30,7 @@ func (h *Handel) Reg() http.HandlerFunc {
} }
err = h.webService.Reg(ctx, u, rip, ip) err = h.webService.Reg(ctx, u, rip, ip)
if err != nil { if err != nil {
if errors.Is(err, service.ErrExistUser) { h.handleErrorService(ctx, w, err)
h.handleError(ctx, w, err.Error(), model.ErrExistUser, 400, slog.LevelDebug)
return
}
if errors.Is(err, service.ErrExitsName) {
h.handleError(ctx, w, err.Error(), model.ErrExitsName, 400, slog.LevelDebug)
return
}
if errors.Is(err, service.ErrRegLimit) {
h.handleError(ctx, w, err.Error(), model.ErrRegLimit, 400, slog.LevelDebug)
return
}
h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn)
return return
} }
encodeJson(w, model.API[any]{ encodeJson(w, model.API[any]{
@ -53,6 +39,33 @@ func (h *Handel) Reg() http.HandlerFunc {
} }
} }
func (h *Handel) Login() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ip, err := utils.GetIP(r)
if err != nil {
h.handleError(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
return
}
l, err := utils.DeCodeBody[model.Login](r.Body, h.validate)
if err != nil {
h.handleError(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
return
}
lr, err := h.webService.Login(ctx, l, ip)
if err != nil {
h.handleErrorService(ctx, w, err)
return
}
encodeJson(w, model.API[model.LoginRep]{
Code: 0,
Data: lr,
})
}
}
func (h *Handel) UserInfo() http.HandlerFunc { func (h *Handel) UserInfo() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -81,11 +94,7 @@ func (h *Handel) ChangePasswd() http.HandlerFunc {
} }
err = h.webService.ChangePasswd(ctx, c, t) err = h.webService.ChangePasswd(ctx, c, t)
if err != nil { if err != nil {
if errors.Is(err, service.ErrPassWord) { h.handleErrorService(ctx, w, err)
h.handleError(ctx, w, err.Error(), model.ErrPassWord, 401, slog.LevelDebug)
return
}
h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn)
return return
} }
encodeJson(w, model.API[any]{ encodeJson(w, model.API[any]{
@ -106,11 +115,7 @@ func (h *Handel) ChangeName() http.HandlerFunc {
} }
err = h.webService.ChangeName(ctx, c.Name, t) err = h.webService.ChangeName(ctx, c.Name, t)
if err != nil { if err != nil {
if errors.Is(err, service.ErrExitsName) { h.handleErrorService(ctx, w, err)
h.handleError(ctx, w, err.Error(), model.ErrExitsName, 400, slog.LevelDebug)
return
}
h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn)
return return
} }
encodeJson(w, model.API[any]{ encodeJson(w, model.API[any]{

View File

@ -13,4 +13,6 @@ const (
ErrPassWord ErrPassWord
ErrExitsName ErrExitsName
ErrNotAdmin ErrNotAdmin
ErrUserDisable
ErrCaptcha
) )

View File

@ -13,7 +13,7 @@ type List[T any] struct {
List []T `json:"list"` List []T `json:"list"`
} }
type User struct { type UserReg struct {
Email string `validate:"required,email"` Email string `validate:"required,email"`
Password string `validate:"required,min=6,max=50"` Password string `validate:"required,min=6,max=50"`
Name string `validate:"required,min=3,max=16"` Name string `validate:"required,min=3,max=16"`
@ -71,3 +71,15 @@ type EditUser struct {
IsDisable bool `json:"is_disable"` IsDisable bool `json:"is_disable"`
DelTextures bool `json:"del_textures"` DelTextures bool `json:"del_textures"`
} }
type Login struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password"`
CaptchaToken string
}
type LoginRep struct {
Token string `json:"token"`
Name string `json:"name"`
UUID string `json:"uuid"`
}

View File

@ -66,7 +66,8 @@ func newYggdrasil(handelY *yggdrasil.Yggdrasil) http.Handler {
func newSkinApi(handel *handle.Handel) http.Handler { func newSkinApi(handel *handle.Handel) http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Put("/user/reg", handel.Reg()) r.Post("/user/reg", handel.Reg())
r.Post("/user/login", handel.Login())
r.Get("/config", handel.GetConfig()) r.Get("/config", handel.GetConfig())
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"github.com/xmdhs/authlib-skin/db/ent" "github.com/xmdhs/authlib-skin/db/ent"
"github.com/xmdhs/authlib-skin/db/ent/predicate" "github.com/xmdhs/authlib-skin/db/ent/predicate"
@ -79,6 +80,7 @@ func (w *WebService) ListUser(ctx context.Context, page int, email, name string)
func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) error { func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) error {
uuid := "" uuid := ""
changePasswd := false
err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error { err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error {
up := tx.User.UpdateOneID(uid).SetEmail(u.Email) up := tx.User.UpdateOneID(uid).SetEmail(u.Email)
if u.Password != "" { if u.Password != "" {
@ -88,6 +90,7 @@ func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) er
if err != nil { if err != nil {
return err return err
} }
changePasswd = true
} }
err := tx.UserProfile.Update().Where(userprofile.HasUserWith(user.ID(uid))).SetName(u.Name).Exec(ctx) err := tx.UserProfile.Update().Where(userprofile.HasUserWith(user.ID(uid))).SetName(u.Name).Exec(ctx)
if err != nil { if err != nil {
@ -134,5 +137,11 @@ func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) er
return fmt.Errorf("EditUser: %w", err) return fmt.Errorf("EditUser: %w", err)
} }
} }
if changePasswd {
err = w.cache.Del([]byte("auth" + strconv.Itoa(uid)))
if err != nil {
return fmt.Errorf("EditUser: %w", err)
}
}
return nil return nil
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@ -26,6 +27,8 @@ type turnstileRet struct {
ErrorCodes []string `json:"error-codes"` ErrorCodes []string `json:"error-codes"`
} }
var ErrCaptcha = errors.New("验证码错误")
type ErrTurnstile struct { type ErrTurnstile struct {
ErrorCodes []string ErrorCodes []string
} }
@ -34,7 +37,10 @@ func (e ErrTurnstile) Error() string {
return strings.Join(e.ErrorCodes, " ") return strings.Join(e.ErrorCodes, " ")
} }
func (w *WebService) verifyTurnstile(ctx context.Context, token, ip string) error { func (w *WebService) verifyCaptcha(ctx context.Context, token, ip string) error {
if w.config.Captcha.Type != "turnstile" {
return nil
}
bw := &bytes.Buffer{} bw := &bytes.Buffer{}
err := json.NewEncoder(bw).Encode(turnstileResponse{ err := json.NewEncoder(bw).Encode(turnstileResponse{
Secret: w.config.Captcha.Secret, Secret: w.config.Captcha.Secret,
@ -63,9 +69,9 @@ func (w *WebService) verifyTurnstile(ctx context.Context, token, ip string) erro
} }
if !t.Success { if !t.Success {
return fmt.Errorf("verifyTurnstile: %w", ErrTurnstile{ return fmt.Errorf("verifyTurnstile: %w", errors.Join(ErrTurnstile{
ErrorCodes: t.ErrorCodes, ErrorCodes: t.ErrorCodes,
}) }, ErrCaptcha))
} }
return nil return nil
} }

View File

@ -25,7 +25,7 @@ var (
ErrChangeName = errors.New("离线模式 uuid 不允许修改用户名") ErrChangeName = errors.New("离线模式 uuid 不允许修改用户名")
) )
func (w *WebService) Reg(ctx context.Context, u model.User, ipPrefix, ip string) error { func (w *WebService) Reg(ctx context.Context, u model.UserReg, ipPrefix, ip string) error {
var userUuid string var userUuid string
if w.config.OfflineUUID { if w.config.OfflineUUID {
userUuid = utils.UUIDGen(u.Name) userUuid = utils.UUIDGen(u.Name)
@ -33,11 +33,9 @@ func (w *WebService) Reg(ctx context.Context, u model.User, ipPrefix, ip string)
userUuid = strings.ReplaceAll(uuid.New().String(), "-", "") userUuid = strings.ReplaceAll(uuid.New().String(), "-", "")
} }
if w.config.Captcha.Type == "turnstile" { err := w.verifyCaptcha(ctx, u.CaptchaToken, ip)
err := w.verifyTurnstile(ctx, u.CaptchaToken, ip) if err != nil {
if err != nil { return fmt.Errorf("Reg: %w", err)
return fmt.Errorf("Reg: %w", err)
}
} }
if w.config.MaxIpUser != 0 { if w.config.MaxIpUser != 0 {
@ -52,7 +50,7 @@ func (w *WebService) Reg(ctx context.Context, u model.User, ipPrefix, ip string)
p, s := utils.Argon2ID(u.Password) p, s := utils.Argon2ID(u.Password)
err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error { err = utils.WithTx(ctx, w.client, func(tx *ent.Tx) error {
count, err := tx.User.Query().Where(user.EmailEQ(u.Email)).ForUpdate().Count(ctx) count, err := tx.User.Query().Where(user.EmailEQ(u.Email)).ForUpdate().Count(ctx)
if err != nil { if err != nil {
return err return err
@ -93,6 +91,30 @@ func (w *WebService) Reg(ctx context.Context, u model.User, ipPrefix, ip string)
return nil return nil
} }
func (w *WebService) Login(ctx context.Context, l model.Login, ip string) (model.LoginRep, error) {
err := w.verifyCaptcha(ctx, l.CaptchaToken, ip)
if err != nil {
return model.LoginRep{}, fmt.Errorf("Login: %w", err)
}
u, err := w.client.User.Query().Where(user.Email(l.Email)).WithProfile().Only(ctx)
if err != nil {
return model.LoginRep{}, fmt.Errorf("Login: %w", err)
}
err = w.validatePass(ctx, u, l.Password)
if err != nil {
return model.LoginRep{}, fmt.Errorf("Login: %w", err)
}
jwt, err := utilsService.CreateToken(ctx, u, w.client, w.cache, w.prikey, "web")
if err != nil {
return model.LoginRep{}, fmt.Errorf("Login: %w", err)
}
return model.LoginRep{
Token: jwt,
Name: u.Edges.Profile.Name,
UUID: u.Edges.Profile.UUID,
}, nil
}
func (w *WebService) Info(ctx context.Context, t *model.TokenClaims) (model.UserInfo, error) { func (w *WebService) Info(ctx context.Context, t *model.TokenClaims) (model.UserInfo, error) {
u, err := w.client.User.Query().Where(user.ID(t.UID)).First(ctx) u, err := w.client.User.Query().Where(user.ID(t.UID)).First(ctx)
if err != nil { if err != nil {
@ -111,8 +133,9 @@ func (w *WebService) ChangePasswd(ctx context.Context, p model.ChangePasswd, t *
if err != nil { if err != nil {
return fmt.Errorf("ChangePasswd: %w", err) return fmt.Errorf("ChangePasswd: %w", err)
} }
if !utils.Argon2Compare(p.Old, u.Password, u.Salt) { err = w.validatePass(ctx, u, p.Old)
return fmt.Errorf("ChangePasswd: %w", ErrPassWord) if err != nil {
return fmt.Errorf("ChangePasswd: %w", err)
} }
pass, salt := utils.Argon2ID(p.New) pass, salt := utils.Argon2ID(p.New)
if u.Edges.Token != nil { if u.Edges.Token != nil {

View File

@ -15,10 +15,12 @@ import (
"github.com/xmdhs/authlib-skin/db/ent/usertoken" "github.com/xmdhs/authlib-skin/db/ent/usertoken"
"github.com/xmdhs/authlib-skin/model" "github.com/xmdhs/authlib-skin/model"
"github.com/xmdhs/authlib-skin/model/yggdrasil" "github.com/xmdhs/authlib-skin/model/yggdrasil"
"github.com/xmdhs/authlib-skin/utils"
) )
var ( var (
ErrTokenInvalid = errors.New("token 无效") ErrTokenInvalid = errors.New("token 无效")
ErrUserDisable = errors.New("用户被禁用")
) )
func Auth(ctx context.Context, t yggdrasil.ValidateToken, client *ent.Client, c cache.Cache, pubkey *rsa.PublicKey, tmpInvalid bool) (*model.TokenClaims, error) { func Auth(ctx context.Context, t yggdrasil.ValidateToken, client *ent.Client, c cache.Cache, pubkey *rsa.PublicKey, tmpInvalid bool) (*model.TokenClaims, error) {
@ -80,6 +82,63 @@ func Auth(ctx context.Context, t yggdrasil.ValidateToken, client *ent.Client, c
return claims, nil return claims, nil
} }
func CreateToken(ctx context.Context, u *ent.User, client *ent.Client, cache cache.Cache, jwtKey *rsa.PrivateKey, clientToken string) (string, error) {
if IsDisable(u.State) {
return "", fmt.Errorf("CreateToken: %w", ErrUserDisable)
}
var utoken *ent.UserToken
err := utils.WithTx(ctx, client, func(tx *ent.Tx) error {
var err error
utoken, err = tx.User.QueryToken(u).ForUpdate().First(ctx)
if err != nil {
var nf *ent.NotFoundError
if !errors.As(err, &nf) {
return err
}
}
if utoken == nil {
ut, err := tx.UserToken.Create().SetTokenID(1).SetUser(u).Save(ctx)
if err != nil {
return err
}
utoken = ut
}
return nil
})
if err != nil {
return "", fmt.Errorf("CreateToken: %w", err)
}
err = cache.Del([]byte("auth" + strconv.Itoa(u.ID)))
if err != nil {
return "", fmt.Errorf("CreateToken: %w", err)
}
t, err := NewJwtToken(jwtKey, strconv.FormatUint(utoken.TokenID, 10), clientToken, u.Edges.Profile.UUID, u.ID)
if err != nil {
return "", fmt.Errorf("CreateToken: %w", err)
}
return t, nil
}
func NewJwtToken(jwtKey *rsa.PrivateKey, tokenID, clientToken, UUID string, userID int) (string, error) {
claims := model.TokenClaims{
Tid: tokenID,
CID: clientToken,
UID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * 24 * time.Hour)),
Issuer: "authlib-skin",
Subject: UUID,
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
jwts, err := token.SignedString(jwtKey)
if err != nil {
return "", fmt.Errorf("newJwtToken: %w", err)
}
return jwts, nil
}
func IsAdmin(state int) bool { func IsAdmin(state int) bool {
return state&1 == 1 return state&1 == 1
} }

View File

@ -1,12 +1,15 @@
package service package service
import ( import (
"context"
"crypto/rsa" "crypto/rsa"
"fmt"
"net/http" "net/http"
"github.com/xmdhs/authlib-skin/config" "github.com/xmdhs/authlib-skin/config"
"github.com/xmdhs/authlib-skin/db/cache" "github.com/xmdhs/authlib-skin/db/cache"
"github.com/xmdhs/authlib-skin/db/ent" "github.com/xmdhs/authlib-skin/db/ent"
"github.com/xmdhs/authlib-skin/utils"
) )
type WebService struct { type WebService struct {
@ -26,3 +29,10 @@ func NewWebService(c config.Config, e *ent.Client, hc *http.Client, cache cache.
prikey: prikey, prikey: prikey,
} }
} }
func (w *WebService) validatePass(ctx context.Context, u *ent.User, password string) error {
if !utils.Argon2Compare(password, u.Password, u.Salt) {
return fmt.Errorf("validatePass: %w", ErrPassWord)
}
return nil
}

View File

@ -62,49 +62,19 @@ func (y *Yggdrasil) Authenticate(cxt context.Context, auth yggdrasil.Authenticat
return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", err) return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", err)
} }
if sutils.IsDisable(u.State) {
return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", ErrUserDisable)
}
clientToken := auth.ClientToken clientToken := auth.ClientToken
if clientToken == "" { if clientToken == "" {
clientToken = strings.ReplaceAll(uuid.New().String(), "-", "") clientToken = strings.ReplaceAll(uuid.New().String(), "-", "")
} }
var utoken *ent.UserToken jwts, err := sutils.CreateToken(cxt, u, y.client, y.cache, y.prikey, clientToken)
err = utils.WithTx(cxt, y.client, func(tx *ent.Tx) error {
utoken, err = tx.User.QueryToken(u).ForUpdate().First(cxt)
if err != nil {
var nf *ent.NotFoundError
if !errors.As(err, &nf) {
return err
}
}
if utoken == nil {
ut, err := tx.UserToken.Create().SetTokenID(1).SetUser(u).Save(cxt)
if err != nil {
return err
}
utoken = ut
}
return nil
})
if err != nil {
return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", err)
}
err = y.cache.Del([]byte("auth" + strconv.Itoa(u.ID)))
if err != nil { if err != nil {
return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", err) return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", err)
} }
if u.Edges.Profile == nil { if u.Edges.Profile == nil {
return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", ErrUserDisable) return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", ErrUserDisable)
} }
jwts, err := newJwtToken(y.prikey, strconv.FormatUint(utoken.TokenID, 10), clientToken, u.Edges.Profile.UUID, u.ID)
if err != nil {
return yggdrasil.Token{}, fmt.Errorf("Authenticate: %w", err)
}
p := yggdrasil.UserInfo{ p := yggdrasil.UserInfo{
ID: u.Edges.Profile.UUID, ID: u.Edges.Profile.UUID,
Name: u.Edges.Profile.Name, Name: u.Edges.Profile.Name,

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/golang-jwt/jwt/v5"
"github.com/xmdhs/authlib-skin/config" "github.com/xmdhs/authlib-skin/config"
"github.com/xmdhs/authlib-skin/db/cache" "github.com/xmdhs/authlib-skin/db/cache"
"github.com/xmdhs/authlib-skin/db/ent" "github.com/xmdhs/authlib-skin/db/ent"
@ -67,23 +66,7 @@ func putUint(n uint64, c cache.Cache, key []byte, d time.Duration) error {
} }
func newJwtToken(jwtKey *rsa.PrivateKey, tokenID, clientToken, UUID string, userID int) (string, error) { func newJwtToken(jwtKey *rsa.PrivateKey, tokenID, clientToken, UUID string, userID int) (string, error) {
claims := model.TokenClaims{ return sutils.NewJwtToken(jwtKey, tokenID, clientToken, UUID, userID)
Tid: tokenID,
CID: clientToken,
UID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * 24 * time.Hour)),
Issuer: "authlib-skin",
Subject: UUID,
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
jwts, err := token.SignedString(jwtKey)
if err != nil {
return "", fmt.Errorf("newJwtToken: %w", err)
}
return jwts, nil
} }
func (y *Yggdrasil) Auth(ctx context.Context, t yggdrasil.ValidateToken) (*model.TokenClaims, error) { func (y *Yggdrasil) Auth(ctx context.Context, t yggdrasil.ValidateToken) (*model.TokenClaims, error) {