setup.go 11 KB
Newer Older
Julien Schröter's avatar
Julien Schröter committed
1
package setup
2 3

import (
4
	"encoding/json"
5
	"errors"
6
	"flag"
7
	"fmt"
8
	"net/http"
9
	"net/mail"
10
	"time"
11

12
	"gitlab.akamu.de/akamu/game-server-go/endpoint"
13
	"gitlab.akamu.de/akamu/game-server-go/sendmail"
14
	"gitlab.akamu.de/akamu/game-server-go/token/otp"
15
	"gitlab.akamu.de/akamu/game-server-go/version"
16

17 18 19
	"gitlab.akamu.de/akamu/game-server-go/schemas"

	"github.com/getsentry/raven-go"
20 21
	ut "github.com/go-playground/universal-translator"
	validator "github.com/go-playground/validator/v10"
22

23
	"github.com/gin-gonic/gin"
24
	"github.com/gin-gonic/gin/binding"
25
	jwt "gitlab.akamu.de/akamu/gin-jwt"
Julien Schröter's avatar
Julien Schröter committed
26

Julien Schröter's avatar
Julien Schröter committed
27
	"gitlab.akamu.de/akamu/game-server-go/config"
28 29 30 31
	"gitlab.akamu.de/akamu/game-server-go/endpoint/avatar"
	"gitlab.akamu.de/akamu/game-server-go/endpoint/course"
	"gitlab.akamu.de/akamu/game-server-go/endpoint/duel"
	"gitlab.akamu.de/akamu/game-server-go/endpoint/flashcard"
Julien Schröter's avatar
Julien Schröter committed
32 33
	"gitlab.akamu.de/akamu/game-server-go/endpoint/friend"
	"gitlab.akamu.de/akamu/game-server-go/endpoint/pool"
34
	"gitlab.akamu.de/akamu/game-server-go/endpoint/refreshjwt"
Julien Schröter's avatar
Julien Schröter committed
35
	"gitlab.akamu.de/akamu/game-server-go/endpoint/resource"
36 37 38 39 40
	"gitlab.akamu.de/akamu/game-server-go/endpoint/subject"
	"gitlab.akamu.de/akamu/game-server-go/endpoint/title"
	"gitlab.akamu.de/akamu/game-server-go/endpoint/traininglist"
	"gitlab.akamu.de/akamu/game-server-go/endpoint/user"
	"gitlab.akamu.de/akamu/game-server-go/endpoint/usernameavailable"
41
	"gitlab.akamu.de/akamu/game-server-go/setting"
42 43
)

44 45
var ErrInternalServer = errors.New("internal server error")

46
func authenticate(userRepository user.UserQuery, jwtRepository refreshjwt.RefreshJWTQuery) func(ctx *gin.Context) (interface{}, error) {
47 48 49 50
	return func(ctx *gin.Context) (interface{}, error) {
		var credentials struct {
			Username string `json:"username"`
			Password string `json:"password"`
51
			Token    string `json:"firebase-token" binding:"-"`
52
		}
53
		// binding:"-" makes the firebase-token optional. Prevents BindJSON from an error when endpoint gets called without the argument.
54 55 56 57 58 59 60

		err := ctx.BindJSON(&credentials)

		if err != nil {
			return nil, jwt.ErrMissingLoginValues
		}

61
		if ctx.Request.Method == "OPTIONS" {
62
			return nil, nil
63
		}
64 65 66 67 68 69 70 71 72 73
		userID, err := userRepository.Authenticate(credentials.Username, credentials.Password)
		if err != nil {
			switch err {
			case user.ErrWrongCredentials:
				return nil, user.ErrWrongCredentials
			case user.ErrUnverified:
				return nil, user.ErrUnverified
			}
			raven.CaptureError(err, nil)
			return nil, ErrInternalServer
74
		}
75 76

		// Create new database session
77
		sessionID, errSession := jwtRepository.Insert(userID, credentials.Token)
78 79 80 81 82
		if errSession != nil {
			raven.CaptureError(errSession, nil)
			return nil, jwt.ErrFailedAuthentication
		}

83
		data := schemas.TokenPayload{UserID: userID, Username: credentials.Username, SessionID: sessionID, Version: 1}
84
		return data, nil
85 86 87
	}
}

88 89 90
func refreshClaims(repository refreshjwt.RefreshJWTQuery) func(jwt.MapClaims) (jwt.MapClaims, error) {
	return func(claims jwt.MapClaims) (jwt.MapClaims, error) {
		sessionID, ok1 := claims["SessionID"]
91
		version, ok2 := claims["Version"]
92

93
		if !(ok1 && ok2) {
94 95 96
			return nil, errors.New("failed to fetch claims from JWT payload")
		}

97
		version, err := repository.Refresh(uint32(sessionID.(float64)), uint32(version.(float64)))
98 99 100 101 102 103 104

		if err != nil {
			return nil, err
		}

		claims["Version"] = version

105
		return claims, nil
106 107 108
	}
}

109
func setCORS() gin.HandlerFunc {
110
	return func(c *gin.Context) {
111 112 113 114 115
		c.Header("Access-Control-Allow-Origin", "*")
		if c.Request.Method == "OPTIONS" {
			c.Header("Access-Control-Allow-Methods", "POST,GET,DELETE,PUT,PATCH,OPTIONS")
			c.Header("Access-Control-Allow-Headers", "*")
		}
116 117 118 119
		c.Next()
	}
}

120 121 122 123
func getRoot(ctx *gin.Context) {
	ctx.String(http.StatusOK, "AKAMU REST API")
}

124
func SetupRoutes(courseRepository course.CourseQuery, flashcardRepository flashcard.FlashcardQuery, subjectRepository subject.SubjectQuery, titleRepository title.TitleQuery, traininglistRepository traininglist.TrainingListQuery, userRepository user.UserQuery, poolRepository pool.PoolQuery, friendRepository friend.FriendQuery, avatarRepository avatar.AvatarQuery, duelRepository duel.DuelQuery, refreshjwtRepository refreshjwt.RefreshJWTQuery, resourceRepository resource.ResourceQuery, resourceServerConfig *config.ResourceServer, settings *setting.Settings, smtpTransport *sendmail.Transport, jwtSecret ...string) *gin.Engine {
125
	//gin.SetMode(gin.ReleaseMode)
126 127
	router := gin.Default()

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		// Register Password Validator
		v.RegisterValidation("password", func(fl validator.FieldLevel) bool {
			password, ok := fl.Field().Interface().(string)
			if ok {
				return user.CheckPasswordOK(password)
			}

			return false
		})

		t := endpoint.GetTranslator()

		// Register custom error messages
		v.RegisterTranslation("required", t, func(ut ut.Translator) error {
			return ut.Add("required", "{0} cannot be empty.", true)
		}, func(ut ut.Translator, fe validator.FieldError) string {
			m, _ := ut.T("required", fe.Field())
			return m
		})

		v.RegisterTranslation("eqfield", t, func(ut ut.Translator) error {
			return ut.Add("eqfield", "{0} must match the value of {1}.", true)
		}, func(ut ut.Translator, fe validator.FieldError) string {
			m, _ := ut.T("eqfield", fe.Field(), fe.Param())
			return m
		})

		v.RegisterTranslation("password", t, func(ut ut.Translator) error {
			return ut.Add("password", "A valid password must be at least 8 characters long and must contain at least one uppercase and one lowercase letter.", true)
		}, func(ut ut.Translator, fe validator.FieldError) string {
			m, _ := ut.T("password")
			return m
		})
	} else {
		panic("failed setting up password validator")
	}

166
	router.GET("/", getRoot)
167

168 169 170 171 172
	s := "secret"
	if jwtSecret != nil && len(jwtSecret) > 0 {
		s = jwtSecret[0]
	}

173
	authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
174 175 176 177 178
		Realm: "akamu app server",
		Key:   []byte(s),
		// The jwt can be refreshed until 2 years after the last signin. After this duration,
		// the user has to log in again with his username and password.
		MaxRefresh: time.Hour * 24 * 365 * 2,
179 180
		// Token has to be refreshed every 15 minutes.
		Timeout:       time.Minute * 15,
181
		Authenticator: authenticate(userRepository, refreshjwtRepository),
182
		Unauthorized: func(c *gin.Context, code int, message string, err error) {
183 184 185 186 187 188 189 190 191 192 193 194
			switch err {
			case ErrInternalServer:
				c.AbortWithError(http.StatusInternalServerError, err)
			case user.ErrBadRequest:
				c.AbortWithError(http.StatusBadRequest, err)
			case user.ErrUnverified:
				c.AbortWithError(http.StatusForbidden, err)
			case user.ErrWrongCredentials:
				c.AbortWithError(http.StatusUnauthorized, err)
			default:
				c.String(code, message)
			}
195
		},
196 197
		// function that takes the id from the Authenticator function and add it to the claims
		PayloadFunc: func(data interface{}) jwt.MapClaims {
198
			payload, ok := data.(schemas.TokenPayload)
199
			if !ok {
200
				return jwt.MapClaims{}
201 202
			}

203
			mapClaims := make(jwt.MapClaims)
204
			mapClaims["UserID"] = payload.UserID
205
			mapClaims["Username"] = payload.Username
206 207
			mapClaims["SessionID"] = payload.SessionID
			mapClaims["Version"] = payload.Version
208 209
			return mapClaims
		},
210 211
		IdentityHandler: func(c *gin.Context) interface{} {
			claims := jwt.ExtractClaims(c)
212 213
			return schemas.TokenPayload{
				UserID:    uint32(claims["UserID"].(float64)),
214
				Username:  claims["Username"].(string),
215 216
				SessionID: uint32(claims["SessionID"].(float64)),
				Version:   uint32(claims["Version"].(float64)),
217 218
			}
		},
219
		RefreshClaimsHandler: refreshClaims(refreshjwtRepository),
220 221
		// TokenLookup is a string in the form of "<source>:<name>" that is used
		// to extract token from the request.
222
		TokenLookup: "header: Authorization",
223 224 225 226
		// TokenHeadName is a string in the header. Default value is "Bearer"
		TokenHeadName: "Bearer",
		// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
		TimeFunc: time.Now,
227
	})
228

229 230 231 232
	if err != nil {
		panic(fmt.Sprintf("Error setting up authentication middleware. %s", err.Error()))
	}

233 234
	tokenFactory := otp.GetTokenFactory([]byte(s))

235 236 237 238 239 240 241 242 243 244
	userVerifyMail := &sendmail.TemplateMail{
		Transport: smtpTransport,
		From: mail.Address{
			Name:    "Akamu",
			Address: "noreply@akamu.de",
		},
		Subject:   "Verify your Akamu Account!",
		BodyFiles: []string{"assets/email/registrationVerification.tmpl"},
	}

245 246 247 248 249 250 251 252 253 254
	forgotPasswordMail := &sendmail.TemplateMail{
		Transport: smtpTransport,
		From: mail.Address{
			Name:    "Akamu",
			Address: "noreply@akamu.de",
		},
		Subject:   "Your Request for a Password Reset!",
		BodyFiles: []string{"assets/email/forgotPassword.tmpl"},
	}

255
	//Middleware
256
	router.Use(setCORS())
257

258 259 260 261 262
	// Do not load while testing
	if flag.Lookup("test.v") == nil {
		router.LoadHTMLGlob("assets/web/*")
	}

263 264
	authGroup := router.Group("")

265
	authGroup.Use(authMiddleware.MiddlewareFunc())
266 267

	router.POST("/login", authMiddleware.LoginHandler)
268 269 270 271
	router.POST("/register", user.RegisterUser(userRepository, tokenFactory, userVerifyMail))

	router.GET("/verify", user.Verify(userRepository, tokenFactory, userVerifyMail))

272 273 274 275
	router.OPTIONS("/login", func(ctx *gin.Context) {
		ctx.Header("Allow", "POST")
		ctx.Status(200)
	})
276

277 278 279 280 281
	router.POST("/forgotpassword", user.ForgotPassword(userRepository, tokenFactory, forgotPasswordMail))
	router.OPTIONS("/forgotpassword", func(ctx *gin.Context) {
		ctx.Header("Allow", "POST")
		ctx.Status(200)
	})
282 283 284 285

	router.GET("/forgotpassword/reset", user.GetResetPassword(userRepository, tokenFactory))
	router.POST("/forgotpassword/reset", user.PostResetPassword(userRepository, tokenFactory))

286 287 288 289 290 291
	router.PATCH("/refreshjwt", authMiddleware.RefreshHandler)
	router.OPTIONS("/refreshjwt", func(ctx *gin.Context) {
		ctx.Header("Allow", "PATCH")
		ctx.Status(200)
	})

292 293 294 295 296 297 298 299 300 301
	// make settings available
	router.GET("/settings", func(ctx *gin.Context) {
		settingJSON, err := json.Marshal(settings)
		if err != nil {
			ctx.Status(500)
		} else {
			ctx.JSON(200, string(settingJSON))
		}
	})

302 303 304 305
	router.GET("/version", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, version.Version)
	})

306
	usernameavailable.SetupUsernameavailableRoutes(router.Group("/usernameavailable"), userRepository)
307 308 309 310 311
	user.SetupUserRoutes(authGroup.Group("/user"), userRepository, titleRepository)
	flashcard.SetupFlashcardRoutes(authGroup.Group("/flashcard"), flashcardRepository)
	traininglist.SetupTrainingListRoutes(authGroup.Group("/traininglist"), traininglistRepository)
	subject.SetupSubjectRoutes(authGroup.Group("/subject"), subjectRepository)
	course.SetupCourseRoutes(authGroup.Group("/course"), courseRepository)
312
	title.SetupTitleRoutes(authGroup.Group("/title"), titleRepository)
Julien Schröter's avatar
Julien Schröter committed
313
	pool.SetupPoolRoutes(authGroup.Group("/pool"), poolRepository)
314
	friend.SetupFriendRoutes(authGroup.Group("/friend"), friendRepository)
315
	avatar.SetupAvatarRoutes(authGroup.Group("/avatar"), avatarRepository)
316
	duel.SetupDuelRoutes(authGroup.Group("/duel"), duelRepository, titleRepository, refreshjwtRepository, poolRepository)
317
	resource.SetupResourceRoutes(authGroup.Group("/resource"), resourceRepository, resourceServerConfig)
318
	refreshjwt.SetupRefreshJWTRoutes(authGroup.Group("/session"), refreshjwtRepository)
319

320 321
	return router
}