Skip to content

Commit 275b97e

Browse files
authored
Merge pull request #3 from zekroTJA/dev
release 1.3.0
2 parents af5dd4b + bc2a64e commit 275b97e

File tree

19 files changed

+1591
-301
lines changed

19 files changed

+1591
-301
lines changed

config/insomnia/insomnia-export.json

+1-1
Large diffs are not rendered by default.

docs/restapi-docs.md

+951-1
Large diffs are not rendered by default.

internal/database/middleware.go

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ type Middleware interface {
3838
DeleteSession(key string, sessionID snowflake.ID) error
3939
CleanupExpiredSessions() error
4040

41+
SetAPIToken(token *objects.APIToken) error
42+
GetAPIToken(uID snowflake.ID) (*objects.APIToken, error)
43+
ResetAPIToken(uID snowflake.ID) error
44+
VerifyAPIToken(tokenStr string) (*objects.User, error)
45+
4146
SetShare(share *objects.SharePage) error
4247
GetShare(ident string, uid, pageID snowflake.ID) (*objects.SharePage, error)
4348
DeleteShare(ident string, uid, pageID snowflake.ID) error

internal/database/mongodb.go

+34-4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type collections struct {
3737
users,
3838
pages,
3939
sessions,
40+
apitokens,
4041
shares *mongo.Collection
4142
}
4243

@@ -63,10 +64,11 @@ func (m *MongoDB) Connect(params interface{}) (err error) {
6364
m.db = m.client.Database(cfg.DataDB)
6465

6566
m.collections = &collections{
66-
users: m.db.Collection("users"),
67-
pages: m.db.Collection("pages"),
68-
sessions: m.db.Collection("sessions"),
69-
shares: m.db.Collection("shares"),
67+
users: m.db.Collection("users"),
68+
pages: m.db.Collection("pages"),
69+
sessions: m.db.Collection("sessions"),
70+
shares: m.db.Collection("shares"),
71+
apitokens: m.db.Collection("apitokens"),
7072
}
7173

7274
return err
@@ -313,6 +315,34 @@ func (m *MongoDB) CleanupExpiredSessions() error {
313315
return err
314316
}
315317

318+
func (m *MongoDB) SetAPIToken(token *objects.APIToken) error {
319+
return m.insertOrUpdate(m.collections.apitokens, &bson.M{"userid": token.UserID}, token)
320+
}
321+
322+
func (m *MongoDB) GetAPIToken(uID snowflake.ID) (*objects.APIToken, error) {
323+
token := new(objects.APIToken)
324+
ok, err := m.get(m.collections.apitokens, bson.M{"userid": uID}, token)
325+
if err != nil || !ok {
326+
return nil, err
327+
}
328+
return token, nil
329+
}
330+
331+
func (m *MongoDB) ResetAPIToken(uID snowflake.ID) error {
332+
_, err := m.collections.apitokens.DeleteOne(ctxTimeout(5*time.Second), bson.M{"userid": uID})
333+
return err
334+
}
335+
336+
func (m *MongoDB) VerifyAPIToken(tokenStr string) (*objects.User, error) {
337+
token := new(objects.APIToken)
338+
ok, err := m.get(m.collections.apitokens, bson.M{"token": tokenStr}, token)
339+
if err != nil || !ok {
340+
return nil, err
341+
}
342+
343+
return m.GetUser(token.UserID, "")
344+
}
345+
316346
func (m *MongoDB) SetShare(share *objects.SharePage) error {
317347
return m.insertOrUpdate(m.collections.shares, bson.M{
318348
"$or": bson.A{

internal/objects/apitoken.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package objects
2+
3+
import (
4+
"time"
5+
6+
"github.com/bwmarrin/snowflake"
7+
)
8+
9+
type APIToken struct {
10+
UserID snowflake.ID `json:"userid"`
11+
Token string `json:"token"`
12+
Created time.Time `json:"created"`
13+
}

internal/webserver/auth.go

+43-7
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import (
1717
)
1818

1919
const (
20+
attemptLimit = 5 * time.Minute
21+
attemptBurst = 5
2022
defCost = 12
23+
apiTokenLength = 64
2124
sessionKeyLength = 128
2225
sessionExpireDefault = 2 * time.Hour
2326
sessionExpireRemember = 30 * 24 * time.Hour
@@ -26,8 +29,10 @@ const (
2629
var (
2730
errBadRequest = errors.New("bad request")
2831
errUnauthorized = errors.New("unauthorized")
32+
errRateLimited = errors.New("rate limited")
2933

30-
setCookieHeader = []byte("Set-Cookie")
34+
setCookieHeader = []byte("Set-Cookie")
35+
authorizationHeader = []byte("Authorization")
3136
)
3237

3338
type loginRequest struct {
@@ -37,12 +42,14 @@ type loginRequest struct {
3742
}
3843

3944
type Authorization struct {
40-
db database.Middleware
45+
db database.Middleware
46+
rlm *RateLimitManager
4147
}
4248

43-
func NewAuthorization(db database.Middleware) (auth *Authorization) {
49+
func NewAuthorization(db database.Middleware, rlm *RateLimitManager) (auth *Authorization) {
4450
auth = new(Authorization)
4551
auth.db = db
52+
auth.rlm = rlm
4653
return
4754
}
4855

@@ -64,15 +71,23 @@ func (auth *Authorization) Login(ctx *routing.Context) bool {
6471
return jsonError(ctx, errBadRequest, fasthttp.StatusBadRequest) != nil
6572
}
6673

74+
limiter := auth.rlm.GetLimiter(fmt.Sprintf("loginAttempt#%s", getIPAddr(ctx)), attemptLimit, attemptBurst)
75+
76+
if limiter.Tokens() <= 0 {
77+
return jsonError(ctx, errRateLimited, fasthttp.StatusTooManyRequests) != nil
78+
}
79+
6780
user, err := auth.db.GetUser(snowflake.ID(-1), strings.ToLower(login.UserName))
6881
if err != nil {
6982
return jsonError(ctx, err, fasthttp.StatusInternalServerError) != nil
7083
}
7184
if user == nil {
85+
limiter.Allow()
7286
return jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized) != nil
7387
}
7488

7589
if !auth.CheckHash(user.PassHash, []byte(login.Password)) {
90+
limiter.Allow()
7691
return jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized) != nil
7792
}
7893

@@ -110,12 +125,31 @@ func (auth *Authorization) CreateSession(ctx *routing.Context, uid snowflake.ID,
110125
}
111126

112127
func (auth *Authorization) CheckRequestAuth(ctx *routing.Context) error {
113-
key := ctx.Request.Header.Cookie("__session")
114-
if key == nil || len(key) == 0 {
115-
return jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized)
128+
var user *objects.User
129+
var err error
130+
var keyStr string
131+
var apiToken string
132+
133+
apiTokenB := ctx.Request.Header.PeekBytes(authorizationHeader)
134+
if apiTokenB != nil && len(apiTokenB) > 0 {
135+
apiToken := string(apiTokenB)
136+
if !strings.HasPrefix(strings.ToLower(apiToken), "basic ") {
137+
return jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized)
138+
}
139+
140+
apiToken = apiToken[6:]
141+
user, err = auth.db.VerifyAPIToken(apiToken)
142+
} else {
143+
key := ctx.Request.Header.Cookie("__session")
144+
if key == nil || len(key) == 0 {
145+
return jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized)
146+
}
147+
148+
keyStr = string(key)
149+
150+
user, err = auth.db.GetSession(keyStr, getIPAddr(ctx))
116151
}
117152

118-
user, err := auth.db.GetSession(string(key), getIPAddr(ctx))
119153
if err != nil {
120154
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
121155
}
@@ -124,6 +158,8 @@ func (auth *Authorization) CheckRequestAuth(ctx *routing.Context) error {
124158
}
125159

126160
ctx.Set("user", user)
161+
ctx.Set("sessionkey", keyStr)
162+
ctx.Set("apitoken", apiToken)
127163

128164
return nil
129165
}

internal/webserver/handlers.go

+58-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strings"
66
"time"
77

8+
"github.com/zekroTJA/myrunes/pkg/random"
9+
810
"github.com/bwmarrin/snowflake"
911
"github.com/qiangxue/fasthttp-routing"
1012
"github.com/valyala/fasthttp"
@@ -286,17 +288,28 @@ func (ws *WebServer) handlerCheckUsername(ctx *routing.Context) error {
286288

287289
func (ws *WebServer) handlerGetSessions(ctx *routing.Context) error {
288290
user := ctx.Get("user").(*objects.User)
291+
sessionKey := ctx.Get("sessionkey").(string)
289292

290293
sessions, err := ws.db.GetSessions(user.UID)
291294
if err != nil {
292295
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
293296
}
294297

298+
var currentSession *objects.Session
295299
for _, s := range sessions {
300+
if sessionKey == s.Key {
301+
currentSession = s
302+
}
296303
s.Key = fmt.Sprintf("%s...%s", s.Key[:3], s.Key[len(s.Key)-3:])
297304
}
298305

299-
return jsonResponse(ctx, listResponse{N: len(sessions), Data: sessions}, fasthttp.StatusOK)
306+
return jsonResponse(ctx, sessionsResponse{
307+
listResponse: listResponse{
308+
N: len(sessions),
309+
Data: sessions,
310+
},
311+
CurrentlyConnectedID: currentSession.SessionID.String(),
312+
}, fasthttp.StatusOK)
300313
}
301314

302315
func (ws *WebServer) handlerDeleteSession(ctx *routing.Context) error {
@@ -544,3 +557,47 @@ func (ws *WebServer) handlerGetVersion(ctx *routing.Context) error {
544557
"release": static.Release,
545558
}, fasthttp.StatusOK)
546559
}
560+
561+
func (ws *WebServer) handlerPostAPIToken(ctx *routing.Context) error {
562+
user := ctx.Get("user").(*objects.User)
563+
var err error
564+
token := new(objects.APIToken)
565+
566+
if token.Token, err = random.GetRandBase64Str(apiTokenLength); err != nil {
567+
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
568+
}
569+
570+
token.UserID = user.UID
571+
token.Created = time.Now()
572+
573+
if err = ws.db.SetAPIToken(token); err != nil {
574+
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
575+
}
576+
577+
return jsonResponse(ctx, token, fasthttp.StatusOK)
578+
}
579+
580+
func (ws *WebServer) handlerGetAPIToken(ctx *routing.Context) error {
581+
user := ctx.Get("user").(*objects.User)
582+
583+
token, err := ws.db.GetAPIToken(user.UID)
584+
if err != nil {
585+
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
586+
}
587+
588+
if token == nil {
589+
return jsonError(ctx, errNotFound, fasthttp.StatusNotFound)
590+
}
591+
592+
return jsonResponse(ctx, token, fasthttp.StatusOK)
593+
}
594+
595+
func (ws *WebServer) handlerDeleteAPIToken(ctx *routing.Context) error {
596+
user := ctx.Get("user").(*objects.User)
597+
598+
if err := ws.db.ResetAPIToken(user.UID); err != nil {
599+
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
600+
}
601+
602+
return jsonResponse(ctx, nil, fasthttp.StatusOK)
603+
}

internal/webserver/helpers.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ func jsonResponse(ctx *routing.Context, v interface{}, status int) error {
5757
data = d
5858
}
5959
} else {
60-
data, err = json.MarshalIndent(v, "", " ")
60+
if static.Release != "TRUE" {
61+
data, err = json.MarshalIndent(v, "", " ")
62+
} else {
63+
data, err = json.Marshal(v)
64+
}
6165
if err != nil {
6266
return jsonError(ctx, err, fasthttp.StatusInternalServerError)
6367
}

internal/webserver/ratelimit.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (rlm *RateLimitManager) GetHandler(limit time.Duration, burst int) routing.
5353
rlh.handler = func(ctx *routing.Context) error {
5454
limiterID := fmt.Sprintf("%d#%s",
5555
rlh.id, getIPAddr(ctx))
56-
ok, res := rlm.getLimiter(limiterID, limit, burst).Reserve()
56+
ok, res := rlm.GetLimiter(limiterID, limit, burst).Reserve()
5757

5858
ctx.Response.Header.Set("X-RateLimit-Limit", fmt.Sprintf("%d", res.Burst))
5959
ctx.Response.Header.Set("X-RateLimit-Remaining", fmt.Sprintf("%d", res.Remaining))
@@ -75,11 +75,11 @@ func (rlm *RateLimitManager) GetHandler(limit time.Duration, burst int) routing.
7575
return rlh.handler
7676
}
7777

78-
// getLimiter tries to get an existent limiter
78+
// GetLimiter tries to get an existent limiter
7979
// from the limiter map. If there is no limiter
8080
// existent for this address, a new limiter
8181
// will be created and added to the map.
82-
func (rlm *RateLimitManager) getLimiter(addr string, limit time.Duration, burst int) *ratelimit.Limiter {
82+
func (rlm *RateLimitManager) GetLimiter(addr string, limit time.Duration, burst int) *ratelimit.Limiter {
8383
var ok bool
8484
var limiter *ratelimit.Limiter
8585

internal/webserver/structs.go

+6
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@ type shareResponse struct {
3333
Page *objects.Page `json:"page"`
3434
User *objects.User `json:"user"`
3535
}
36+
37+
type sessionsResponse struct {
38+
listResponse
39+
40+
CurrentlyConnectedID string `json:"currentlyconnectedid"`
41+
}

0 commit comments

Comments
 (0)