验证码
This commit is contained in:
parent
f0e9045e98
commit
7376cffe61
@ -27,5 +27,6 @@ type Config struct {
|
||||
Captcha struct {
|
||||
Type string
|
||||
SiteKey string
|
||||
Secret string
|
||||
}
|
||||
}
|
||||
|
@ -15,18 +15,19 @@ export async function login(username: string, password: string) {
|
||||
return data as tokenData
|
||||
}
|
||||
|
||||
export async function register(email: string, username: string, password: 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", {
|
||||
method: "POST",
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
"Email": email,
|
||||
"Password": password,
|
||||
"Name": username
|
||||
"Name": username,
|
||||
"CaptchaToken": captchaToken
|
||||
})
|
||||
})
|
||||
const data = await v.json()
|
||||
if (!v.ok) {
|
||||
throw data
|
||||
throw data.msg
|
||||
}
|
||||
return
|
||||
}
|
@ -11,7 +11,7 @@ interface prop {
|
||||
onSuccess: ((token: string) => void)
|
||||
}
|
||||
|
||||
const TurnstileWidget = memo(({ onSuccess }: prop) => {
|
||||
function CaptchaWidget({ onSuccess }: prop) {
|
||||
const ref = useRef<TurnstileInstance>(null)
|
||||
const [key, setKey] = useState(1)
|
||||
const { data, error, isLoading } = useSWR<ApiCaptcha>(import.meta.env.VITE_APIADDR + '/api/v1/captcha')
|
||||
@ -21,18 +21,24 @@ const TurnstileWidget = memo(({ onSuccess }: prop) => {
|
||||
return <Alert severity="warning">{String(error)}</Alert>
|
||||
}
|
||||
if (isLoading) {
|
||||
return <Skeleton variant="rectangular" width={210} height={118} />
|
||||
return <Skeleton variant="rectangular" width={300} height={65} />
|
||||
}
|
||||
if (data?.code != 0) {
|
||||
console.warn(error)
|
||||
return <Alert severity="warning">{String(data?.msg)}</Alert>
|
||||
}
|
||||
if (data.data.type != "turnstile") {
|
||||
onSuccess("ok")
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Turnstile siteKey={data?.data.siteKey ?? ""} key={key} onSuccess={onSuccess} ref={ref} scriptOptions={{ async: true }} />
|
||||
<Button onClick={() => setKey(key + 1)}>刷新验证码</Button>
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default TurnstileWidget
|
||||
const CaptchaWidgetMemo = memo(CaptchaWidget)
|
||||
|
||||
export default CaptchaWidgetMemo
|
@ -16,12 +16,13 @@ import Alert from '@mui/material/Alert';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import Loading from '@/components/Loading'
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import TurnstileWidget from '@/components/TurnstileWidget';
|
||||
import CaptchaWidget from '@/components/CaptchaWidget';
|
||||
|
||||
export default function SignUp() {
|
||||
const [regErr, setRegErr] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [captchaToken, setCaptchaToken] = useState("");
|
||||
|
||||
|
||||
const checkList = React.useRef<Map<string, refType>>(new Map<string, refType>())
|
||||
@ -40,7 +41,11 @@ export default function SignUp() {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
register(d.email ?? "", d.username ?? "", d.password ?? "").
|
||||
if (captchaToken == "") {
|
||||
setLoading(false)
|
||||
setRegErr("验证码无效")
|
||||
}
|
||||
register(d.email ?? "", d.username ?? "", d.password ?? "", captchaToken).
|
||||
then(() => navigate("/login")).
|
||||
catch(v => [setRegErr(String(v)), console.warn(v)]).
|
||||
finally(() => setLoading(false))
|
||||
@ -121,7 +126,7 @@ export default function SignUp() {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TurnstileWidget onSuccess={v => console.log(v)} />
|
||||
<CaptchaWidget onSuccess={setCaptchaToken} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Button
|
||||
|
@ -1,6 +1,7 @@
|
||||
package handle
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@ -17,19 +18,26 @@ func (h *Handel) Reg() httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
ctx := r.Context()
|
||||
|
||||
ip, err := utils.GetIP(r, h.config.RaelIP)
|
||||
if err != nil {
|
||||
h.logger.InfoContext(ctx, err.Error())
|
||||
handleError(ctx, w, err.Error(), model.ErrInput, 400)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := utils.DeCodeBody[model.User](r.Body, h.validate)
|
||||
if err != nil {
|
||||
h.logger.InfoContext(ctx, err.Error())
|
||||
handleError(ctx, w, err.Error(), model.ErrInput, 400)
|
||||
return
|
||||
}
|
||||
rip, err := getPrefix(r, h.config.RaelIP)
|
||||
rip, err := getPrefix(ip)
|
||||
if err != nil {
|
||||
h.logger.WarnContext(ctx, err.Error())
|
||||
handleError(ctx, w, err.Error(), model.ErrUnknown, 500)
|
||||
return
|
||||
}
|
||||
err = h.webService.Reg(ctx, u, rip)
|
||||
err = h.webService.Reg(ctx, u, rip, ip)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrExistUser) {
|
||||
h.logger.DebugContext(ctx, err.Error())
|
||||
@ -45,14 +53,16 @@ func (h *Handel) Reg() httprouter.Handle {
|
||||
handleError(ctx, w, err.Error(), model.ErrService, 500)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(model.API[any]{
|
||||
Code: 0,
|
||||
Data: nil,
|
||||
Msg: "",
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func getPrefix(r *http.Request, fromHeader bool) (string, error) {
|
||||
ip, err := utils.GetIP(r, fromHeader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getPrefix: %w", err)
|
||||
}
|
||||
func getPrefix(ip string) (string, error) {
|
||||
ipa, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getPrefix: %w", err)
|
||||
|
@ -12,6 +12,7 @@ type User struct {
|
||||
Email string `validate:"required,email"`
|
||||
Password string `validate:"required,min=6,max=50"`
|
||||
Name string `validate:"required,min=3,max=16"`
|
||||
CaptchaToken string
|
||||
}
|
||||
|
||||
type TokenClaims struct {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
entsql "entgo.io/ent/dialect/sql"
|
||||
@ -96,4 +97,8 @@ func ProvidePubKey(pri *rsa.PrivateKey) (yggdrasil.PubRsaKey, error) {
|
||||
return yggdrasil.PubRsaKey(s), nil
|
||||
}
|
||||
|
||||
var Set = wire.NewSet(ProvideSlog, ProvideDB, ProvideEnt, ProvideValidate, ProvideCache, ProvidePriKey, ProvidePubKey)
|
||||
func ProvideHttpClient() *http.Client {
|
||||
return &http.Client{}
|
||||
}
|
||||
|
||||
var Set = wire.NewSet(ProvideSlog, ProvideDB, ProvideEnt, ProvideValidate, ProvideCache, ProvidePriKey, ProvidePubKey, ProvideHttpClient)
|
||||
|
@ -51,7 +51,8 @@ func InitializeRoute(ctx context.Context, c config.Config) (*http.Server, func()
|
||||
return nil, nil, err
|
||||
}
|
||||
yggdrasil3 := yggdrasil2.NewYggdrasil(logger, validate, yggdrasilYggdrasil, c, pubRsaKey)
|
||||
webService := service.NewWebService(c, client)
|
||||
httpClient := ProvideHttpClient()
|
||||
webService := service.NewWebService(c, client, httpClient)
|
||||
handel := handle.NewHandel(webService, validate, c, logger)
|
||||
router, err := route.NewRoute(yggdrasil3, handel)
|
||||
if err != nil {
|
||||
|
@ -1,7 +1,12 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/xmdhs/authlib-skin/model"
|
||||
)
|
||||
@ -12,3 +17,58 @@ func (w *WebService) GetCaptcha(ctx context.Context) model.Captcha {
|
||||
SiteKey: w.config.Captcha.SiteKey,
|
||||
}
|
||||
}
|
||||
|
||||
type turnstileRet struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorCodes []string `json:"error-codes"`
|
||||
}
|
||||
|
||||
type ErrTurnstile struct {
|
||||
ErrorCodes []string
|
||||
}
|
||||
|
||||
func (e ErrTurnstile) Error() string {
|
||||
return strings.Join(e.ErrorCodes, " ")
|
||||
}
|
||||
|
||||
func (w *WebService) verifyTurnstile(ctx context.Context, token, ip string) error {
|
||||
bw := &bytes.Buffer{}
|
||||
err := json.NewEncoder(bw).Encode(turnstileResponse{
|
||||
Secret: w.config.Captcha.Secret,
|
||||
Response: token,
|
||||
Remoteip: ip,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifyTurnstile: %w", err)
|
||||
}
|
||||
reqs, err := http.NewRequestWithContext(ctx, "POST", "https://challenges.cloudflare.com/turnstile/v0/siteverify", bw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifyTurnstile: %w", err)
|
||||
}
|
||||
reqs.Header.Set("Accept", "*/*")
|
||||
reqs.Header.Set("Content-Type", "application/json")
|
||||
rep, err := w.httpClient.Do(reqs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifyTurnstile: %w", err)
|
||||
}
|
||||
defer rep.Body.Close()
|
||||
|
||||
var t turnstileRet
|
||||
err = json.NewDecoder(rep.Body).Decode(&t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifyTurnstile: %w", err)
|
||||
}
|
||||
|
||||
if !t.Success {
|
||||
return fmt.Errorf("verifyTurnstile: %w", ErrTurnstile{
|
||||
ErrorCodes: t.ErrorCodes,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type turnstileResponse struct {
|
||||
Secret string `json:"secret"`
|
||||
Response string `json:"response"`
|
||||
Remoteip string `json:"remoteip"`
|
||||
}
|
||||
|
@ -21,17 +21,23 @@ var (
|
||||
ErrRegLimit = errors.New("超过注册 ip 限制")
|
||||
)
|
||||
|
||||
func (w *WebService) Reg(ctx context.Context, u model.User, ip string) error {
|
||||
func (w *WebService) Reg(ctx context.Context, u model.User, ipPrefix, ip string) error {
|
||||
var userUuid string
|
||||
if w.config.OfflineUUID {
|
||||
userUuid = utils.UUIDGen(u.Name)
|
||||
} else {
|
||||
userUuid = strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
}
|
||||
p, s := utils.Argon2ID(u.Password)
|
||||
|
||||
if w.config.Captcha.Type == "turnstile" {
|
||||
err := w.verifyTurnstile(ctx, u.CaptchaToken, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Reg: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if w.config.MaxIpUser != 0 {
|
||||
c, err := w.client.User.Query().Where(user.RegIPEQ(ip)).Count(ctx)
|
||||
c, err := w.client.User.Query().Where(user.RegIPEQ(ipPrefix)).Count(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Reg: %w", err)
|
||||
}
|
||||
@ -40,6 +46,8 @@ func (w *WebService) Reg(ctx context.Context, u model.User, ip string) error {
|
||||
}
|
||||
}
|
||||
|
||||
p, s := utils.Argon2ID(u.Password)
|
||||
|
||||
err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error {
|
||||
count, err := tx.User.Query().Where(user.EmailEQ(u.Email)).ForUpdate().Count(ctx)
|
||||
if err != nil {
|
||||
@ -60,7 +68,7 @@ func (w *WebService) Reg(ctx context.Context, u model.User, ip string) error {
|
||||
SetPassword(p).
|
||||
SetSalt(s).
|
||||
SetRegTime(time.Now().Unix()).
|
||||
SetRegIP(ip).
|
||||
SetRegIP(ipPrefix).
|
||||
SetState(0).Save(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1,6 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/xmdhs/authlib-skin/config"
|
||||
"github.com/xmdhs/authlib-skin/db/ent"
|
||||
)
|
||||
@ -8,11 +10,13 @@ import (
|
||||
type WebService struct {
|
||||
config config.Config
|
||||
client *ent.Client
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewWebService(c config.Config, e *ent.Client) *WebService {
|
||||
func NewWebService(c config.Config, e *ent.Client, hc *http.Client) *WebService {
|
||||
return &WebService{
|
||||
config: c,
|
||||
client: e,
|
||||
httpClient: hc,
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user