Skip to content

Commit 119e225

Browse files
authored
Merge pull request #25 from myrunes/dev
main 1.9
2 parents efd1d6f + 6eddf2a commit 119e225

File tree

10 files changed

+276
-81
lines changed

10 files changed

+276
-81
lines changed

docs/restapi-docs.md

+58
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ The MYRUNES backend provides a RESTful HTTP JSON API providing 100% of the funct
1818
- [**Resources**](#resources)
1919
- [Champions](#champions)
2020
- [Runes and Perks](#runes-and-perks)
21+
- [**Information**](#information)
22+
- [Version](#version)
23+
- [ReCAPTCHA](#recaptcha)
2124
- [**Endpoints**](#endpoints)
2225
- [Users](#users)
2326
- [Get Self User](#get-self-user)
@@ -343,6 +346,60 @@ The response will contain nested multidimensional arrays of runes available for
343346

344347
---
345348

349+
## Information
350+
351+
### Version
352+
353+
> `GET /api/version`
354+
355+
**Response**
356+
357+
```
358+
HTTP/1.1 200 OK
359+
Cache-Control: max-age=2592000; must-revalidate; proxy-revalidate; public
360+
Content-Type: application/json
361+
Date: Sun, 20 Sep 2020 08:02:45 GMT
362+
Etag: W/"3d17185be51edc954586c4feff03cf31c5d278e6"
363+
Server: MYRUNES v.1.7.1+26
364+
X-Ratelimit-limit: 50
365+
X-Ratelimit-remaining: 49
366+
X-Ratelimit-reset: 0
367+
Content-Length: 60
368+
```
369+
```json
370+
{
371+
"apiversion":"1.8.0",
372+
"release":"TRUE",
373+
"version":"1.7.1+26"
374+
}
375+
```
376+
377+
### Recaptcha
378+
379+
> `GET /api/recaptchainfo`
380+
381+
**Response**
382+
383+
```
384+
HTTP/1.1 200 OK
385+
Cache-Control: max-age=2592000; must-revalidate; proxy-revalidate; public
386+
Content-Type: application/json
387+
Date: Sun, 20 Sep 2020 08:02:45 GMT
388+
Etag: W/"0f3279816a04027124c7d6eaca3b967e15c2d166"
389+
Server: MYRUNES v.1.7.1+26
390+
X-Ratelimit-limit: 50
391+
X-Ratelimit-remaining: 49
392+
X-Ratelimit-reset: 0
393+
Content-Length: 54
394+
```
395+
```json
396+
{
397+
"sitekey":"6Le6IM4ZAAAAAL8iQ0akcye5Sw4I5JbBqRMyD0J8"
398+
}
399+
```
400+
401+
---
402+
346403
## Endpoints
347404

348405
### Authentication
@@ -473,6 +530,7 @@ X-Ratelimit-Reset: 0
473530
|------|------|-----|---------|-------------|
474531
| `username` | string | Body | | The username of the account |
475532
| `password` | string | Body | | The password of the given user |
533+
| `recaptcharesponse` | string | Body | | ReCAPTCHA verification response token. |
476534
| *`remember`* | boolean | Body | `false` | Sessions defaultly expire after 2 hours. Setting this to true, this duration will be expanded to 30 days. |
477535

478536
**Response**

go.mod

+12-8
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ module github.com/myrunes/backend
33
go 1.14
44

55
require (
6-
github.com/alexedwards/argon2id v0.0.0-20200420065805-90c52fcb498a
6+
github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b
7+
github.com/aws/aws-sdk-go v1.34.27 // indirect
78
github.com/bwmarrin/snowflake v0.3.0
89
github.com/dgrijalva/jwt-go v3.2.0+incompatible
910
github.com/ghodss/yaml v1.0.0
10-
github.com/go-ini/ini v1.60.2 // indirect
11-
github.com/go-ozzo/ozzo-routing v2.1.4+incompatible // indirect
11+
github.com/go-ini/ini v1.61.0 // indirect
1212
github.com/go-redis/redis v6.15.9+incompatible
13+
github.com/jmespath/go-jmespath v0.4.0 // indirect
14+
github.com/klauspost/compress v1.11.0 // indirect
1315
github.com/minio/minio-go v6.0.14+incompatible
14-
github.com/mitchellh/go-homedir v1.1.0 // indirect
1516
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
1617
github.com/qiangxue/fasthttp-routing v0.0.0-20160225050629-6ccdc2a18d87
17-
github.com/valyala/fasthttp v1.15.1
18+
github.com/valyala/fasthttp v1.16.0
1819
github.com/zekroTJA/ratelimit v0.0.0-20190321090824-219ca33049a5
19-
github.com/zekroTJA/shinpuru v0.0.0-20200829092918-bf62c98441a9
20+
github.com/zekroTJA/shinpuru v0.0.0-20200916163845-50e81cb0f86f
2021
github.com/zekroTJA/timedmap v1.2.0
21-
go.mongodb.org/mongo-driver v1.3.3
22-
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
22+
go.mongodb.org/mongo-driver v1.4.1
23+
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
24+
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
25+
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
26+
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect
2327
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
2428
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
2529
)

go.sum

+65-25
Large diffs are not rendered by default.

internal/static/static.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ const (
77
DiscordUserAgentPingHeaderVal = "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)"
88

99
// Current API specification version.
10-
APIVersion = "1.7.0"
10+
APIVersion = "1.8.0"
1111
)

internal/webserver/auth.go

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ var (
5858
// loginRequests describes the request
5959
// model of the login endpoint
6060
type loginRequest struct {
61+
reCaptchaResponse
62+
6163
UserName string `json:"username"`
6264
Password string `json:"password"`
6365
Remember bool `json:"remember"`

internal/webserver/handlers.go

+27-33
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ func (ws *WebServer) handlerCreateUser(ctx *routing.Context) error {
7777
return jsonError(ctx, err, fasthttp.StatusBadRequest)
7878
}
7979

80+
if ok, err := ws.validateReCaptcha(ctx, &data.reCaptchaResponse); !ok {
81+
return err
82+
}
83+
8084
if data.UserName == "" || data.Password == "" || len(data.Password) < 8 {
8185
return jsonError(ctx, errInvalidArguments, fasthttp.StatusBadRequest)
8286
}
@@ -271,6 +275,14 @@ func (ws *WebServer) handlerPostMail(ctx *routing.Context) error {
271275
return jsonResponse(ctx, nil, fasthttp.StatusOK)
272276
}
273277

278+
recUser, err := ws.db.GetUser(-1, mail.MailAddress)
279+
if err != nil {
280+
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
281+
}
282+
if recUser != nil && recUser.UID != user.UID {
283+
return jsonError(ctx, errEmailAlreadyTaken, fasthttp.StatusBadRequest)
284+
}
285+
274286
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
275287
token, err := random.String(16, charset)
276288
if err != nil {
@@ -374,6 +386,10 @@ func (ws *WebServer) handlerPostPwResetConfirm(ctx *routing.Context) error {
374386
return jsonError(ctx, fmt.Errorf("invalid token"), fasthttp.StatusBadRequest)
375387
}
376388

389+
if ok, err := ws.validateReCaptcha(ctx, &data.reCaptchaResponse); !ok {
390+
return err
391+
}
392+
377393
uID, ok := ws.pwReset.GetValue(data.Token).(snowflake.ID)
378394
if !ok {
379395
return jsonError(ctx, fmt.Errorf("wrong data struct in timedmap"), fasthttp.StatusInternalServerError)
@@ -388,39 +404,6 @@ func (ws *WebServer) handlerPostPwResetConfirm(ctx *routing.Context) error {
388404
return jsonError(ctx, fmt.Errorf("unknown user"), fasthttp.StatusBadRequest)
389405
}
390406

391-
errCheckFailed := fmt.Errorf("security check failed")
392-
if len(data.PageNames) < 3 || data.PageNames[0] == "" || data.PageNames[1] == "" || data.PageNames[2] == "" {
393-
return jsonError(ctx, errCheckFailed, fasthttp.StatusBadRequest)
394-
}
395-
396-
pages, err := ws.db.GetPages(uID, "", "", nil)
397-
if err != nil {
398-
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
399-
}
400-
401-
checkMap := make(map[string]interface{})
402-
for _, guess := range data.PageNames {
403-
if _, ok := checkMap[guess]; ok {
404-
return jsonError(ctx, errCheckFailed, fasthttp.StatusBadRequest)
405-
}
406-
checkMap[guess] = nil
407-
}
408-
409-
var guessed int
410-
411-
for _, page := range pages {
412-
for i, guess := range data.PageNames {
413-
if checkPageName(page.Title, guess, 0.2) {
414-
guessed++
415-
data.PageNames[i] = ""
416-
}
417-
}
418-
}
419-
420-
if guessed < 3 {
421-
return jsonError(ctx, errCheckFailed, fasthttp.StatusBadRequest)
422-
}
423-
424407
ws.pwReset.Remove(data.Token)
425408

426409
var passStr string
@@ -640,6 +623,17 @@ func (ws *WebServer) handlerGetVersion(ctx *routing.Context) error {
640623
}, fasthttp.StatusOK)
641624
}
642625

626+
// GET /recaptchainfo
627+
func (ws *WebServer) handlerGetReCaptchaInfo(ctx *routing.Context) error {
628+
if ws.config.ReCaptcha == nil || ws.config.ReCaptcha.SiteKey == "" {
629+
return jsonError(ctx, errors.New("not configured"), fasthttp.StatusBadRequest)
630+
}
631+
632+
return jsonCachableResponse(ctx, map[string]string{
633+
"sitekey": ws.config.ReCaptcha.SiteKey,
634+
}, fasthttp.StatusOK)
635+
}
636+
643637
// -----------------------------------------------------
644638
// --- FAVORITES ---
645639

internal/webserver/helpers.go

+19
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/myrunes/backend/internal/static"
12+
"github.com/myrunes/backend/pkg/recapatcha"
1213

1314
routing "github.com/qiangxue/fasthttp-routing"
1415
"github.com/valyala/fasthttp"
@@ -143,6 +144,24 @@ func (ws *WebServer) addHeaders(ctx *routing.Context) error {
143144
return nil
144145
}
145146

147+
func (ws *WebServer) validateReCaptcha(ctx *routing.Context, rcr *reCaptchaResponse) (bool, error) {
148+
if rcr.ReCaptchaResponse == "" {
149+
return false, jsonError(ctx, errMissingReCaptchaResponse, fasthttp.StatusBadRequest)
150+
}
151+
152+
rcRes, err := recapatcha.Validate(ws.config.ReCaptcha.SecretKey, rcr.ReCaptchaResponse)
153+
if err != nil {
154+
return false, jsonError(ctx, err, fasthttp.StatusInternalServerError)
155+
}
156+
if !rcRes.Success {
157+
return false, jsonError(ctx,
158+
fmt.Errorf("recaptcha challenge failed: %+v", rcRes.ErrorCodes),
159+
fasthttp.StatusBadRequest)
160+
}
161+
162+
return true, nil
163+
}
164+
146165
// checkPageName takes an actual pageName, a guess and
147166
// a float value for tollerance between 0 and 1.
148167
// Both, the pageName and guess will be lowercased and

internal/webserver/structs.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,10 @@ type passwordReset struct {
8585
// verification, the new password string and
8686
// the page name guesses.
8787
type confirmPasswordReset struct {
88-
Token string `json:"token"`
89-
NewPassword string `json:"new_password"`
90-
PageNames []string `json:"page_names"`
88+
reCaptchaResponse
89+
90+
Token string `json:"token"`
91+
NewPassword string `json:"new_password"`
9192
}
9293

9394
// mailConfirmtationData wraps the mail address
@@ -98,3 +99,9 @@ type mailConfirmationData struct {
9899
UserID snowflake.ID
99100
MailAddress string
100101
}
102+
103+
// reCaptchaResponse wraps a ReCAPTCHA response
104+
// token for ReCAPTCHA validation.
105+
type reCaptchaResponse struct {
106+
ReCaptchaResponse string `json:"recaptcharesponse"`
107+
}

internal/webserver/websrever.go

+22-11
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,41 @@ import (
1818

1919
// Error Objects
2020
var (
21-
errNotFound = errors.New("not found")
22-
errInvalidArguments = errors.New("invalid arguments")
23-
errUNameInUse = errors.New("user name already in use")
24-
errNoAccess = errors.New("access denied")
21+
errNotFound = errors.New("not found")
22+
errInvalidArguments = errors.New("invalid arguments")
23+
errUNameInUse = errors.New("user name already in use")
24+
errNoAccess = errors.New("access denied")
25+
errMissingReCaptchaResponse = errors.New("missing recaptcha challenge response")
26+
errEmailAlreadyTaken = errors.New("e-mail address is already taken by another account")
2527
)
2628

2729
// Config wraps properties for the
2830
// HTTP REST API server.
2931
type Config struct {
30-
Addr string `json:"addr"`
31-
PathPrefix string `json:"pathprefix"`
32-
TLS *TLSConfig `json:"tls"`
33-
PublicAddr string `json:"publicaddress"`
34-
EnableCors bool `json:"enablecors"`
35-
JWTKey string `json:"jwtkey"`
32+
Addr string `json:"addr"`
33+
PathPrefix string `json:"pathprefix"`
34+
TLS *TLSConfig `json:"tls"`
35+
ReCaptcha *ReCaptchaConfig `json:"recaptcha"`
36+
PublicAddr string `json:"publicaddress"`
37+
EnableCors bool `json:"enablecors"`
38+
JWTKey string `json:"jwtkey"`
3639
}
3740

38-
// TLSCOnfig wraps properties for
41+
// TLSConfig wraps properties for
3942
// TLS encryption.
4043
type TLSConfig struct {
4144
Enabled bool `json:"enabled"`
4245
Cert string `json:"certfile"`
4346
Key string `json:"keyfile"`
4447
}
4548

49+
// ReCaptchaConfig wraps key and secret
50+
// for ReCAPTCHA v2.
51+
type ReCaptchaConfig struct {
52+
SiteKey string `json:"sitekey"`
53+
SecretKey string `json:"secretkey"`
54+
}
55+
4656
// WebServer provices a HTTP REST
4757
// API router.
4858
type WebServer struct {
@@ -114,6 +124,7 @@ func (ws *WebServer) registerHandlers() {
114124
Post("/logout", ws.auth.CheckRequestAuth, ws.auth.Logout)
115125

116126
api.Get("/version", ws.handlerGetVersion)
127+
api.Get("/recaptchainfo", ws.handlerGetReCaptchaInfo)
117128

118129
assets := api.Group("/assets")
119130
assets.

0 commit comments

Comments
 (0)