diff --git a/.gitignore b/.gitignore
index df30165..97cb863 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
.idea/
server
-sofaraum-server
\ No newline at end of file
+sofaraum-server
+config.yml
+config.yaml
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 1d5b6fe..5164d71 100644
--- a/go.mod
+++ b/go.mod
@@ -1 +1,25 @@
-module git.kolaente.de/sofaraum/server
+module code.sofaraum.de/server
+
+require (
+ code.vikunja.io/web v0.0.0-20181130231148-b061c20192fb
+ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
+ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
+ github.com/garyburd/redigo v1.6.0 // indirect
+ github.com/go-openapi/jsonreference v0.17.2 // indirect
+ github.com/go-openapi/spec v0.17.2 // indirect
+ github.com/go-sql-driver/mysql v1.4.1
+ github.com/go-xorm/core v0.6.0
+ github.com/go-xorm/xorm v0.7.1
+ github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2
+ github.com/labstack/echo v3.3.5+incompatible
+ github.com/mattn/go-sqlite3 v1.10.0
+ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
+ github.com/spf13/viper v1.3.0
+ github.com/swaggo/echo-swagger v0.0.0-20180315045949-97f46bb9e5a5
+ github.com/swaggo/files v0.0.0-20180215091130-49c8a91ea3fa // indirect
+ github.com/swaggo/swag v1.4.0
+ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
+ golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
+ golang.org/x/tools v0.0.0-20181205224935-3576414c54a4 // indirect
+)
diff --git a/main.go b/main.go
index ffe329f..7ba510b 100644
--- a/main.go
+++ b/main.go
@@ -17,8 +17,66 @@
package main
-import "fmt"
+import (
+ "code.sofaraum.de/server/docs"
+ "code.sofaraum.de/server/pkg/config"
+ "code.sofaraum.de/server/pkg/log"
+ "code.sofaraum.de/server/pkg/models"
+ "code.sofaraum.de/server/pkg/routes"
+ "context"
+ "github.com/spf13/viper"
+ "os"
+ "os/signal"
+ "time"
+)
+
+// Version sets the version to be printed to the user. Gets overwritten by "make release" or "make build" with last git commit or tag.
+var Version = "0.1"
func main() {
- fmt.Println("schinken")
+
+ // Init logging
+ log.InitLogger()
+
+ // Init Config
+ err := config.InitConfig()
+ if err != nil {
+ log.Log.Error(err.Error())
+ os.Exit(1)
+ }
+
+ // Set Engine
+ err = models.SetEngine()
+ if err != nil {
+ log.Log.Error(err.Error())
+ os.Exit(1)
+ }
+
+ // Version notification
+ log.Log.Infof("Sofaraum version %s", Version)
+
+ // Additional swagger information
+ docs.SwaggerInfo.Version = Version
+
+ // Start the webserver
+ e := routes.NewEcho()
+ routes.RegisterRoutes(e)
+ // Start server
+ go func() {
+ if err := e.Start(viper.GetString("service.interface")); err != nil {
+ e.Logger.Info("shutting down...")
+ }
+ }()
+
+ // Wait for interrupt signal to gracefully shutdown the server with
+ // a timeout of 10 seconds.
+ quit := make(chan os.Signal)
+ signal.Notify(quit, os.Interrupt)
+ <-quit
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ log.Log.Infof("Shutting down...")
+ if err := e.Shutdown(ctx); err != nil {
+ e.Logger.Fatal(err)
+ }
}
diff --git a/pkg/config/config.go b/pkg/config/config.go
new file mode 100644
index 0000000..5c9d4f9
--- /dev/null
+++ b/pkg/config/config.go
@@ -0,0 +1,90 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package config
+
+import (
+ "crypto/rand"
+ "fmt"
+ "github.com/spf13/viper"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// InitConfig initializes the config, sets defaults etc.
+func InitConfig() (err error) {
+
+ // Set defaults
+ // Service config
+ random, err := random(32)
+ if err != nil {
+ return err
+ }
+
+ // Service
+ viper.SetDefault("service.JWTSecret", random)
+ viper.SetDefault("service.interface", ":1073")
+ viper.SetDefault("service.frontendurl", "")
+ ex, err := os.Executable()
+ if err != nil {
+ panic(err)
+ }
+ exPath := filepath.Dir(ex)
+ viper.SetDefault("service.rootpath", exPath)
+ viper.SetDefault("service.pagecount", 50)
+ // Database
+ viper.SetDefault("database.type", "sqlite")
+ viper.SetDefault("database.host", "localhost")
+ viper.SetDefault("database.user", "sofaraum")
+ viper.SetDefault("database.password", "")
+ viper.SetDefault("database.database", "sofaraum")
+ viper.SetDefault("database.path", "./sofaraum.db")
+ viper.SetDefault("database.showqueries", false)
+ viper.SetDefault("database.openconnections", 100)
+ // Cacher
+ viper.SetDefault("cache.enabled", false)
+ viper.SetDefault("cache.type", "memory")
+ viper.SetDefault("cache.maxelementsize", 1000)
+ viper.SetDefault("cache.redishost", "localhost:6379")
+ viper.SetDefault("cache.redispassword", "")
+
+ // Init checking for environment variables
+ viper.SetEnvPrefix("sofaraum")
+ viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+ viper.AutomaticEnv()
+
+ // Load the config file
+ viper.AddConfigPath(".")
+ viper.SetConfigName("config")
+ err = viper.ReadInConfig()
+ if err != nil {
+ fmt.Println(err)
+ fmt.Println("Using defaults.")
+ }
+
+ return nil
+}
+
+func random(length int) (string, error) {
+ b := make([]byte, length)
+ if _, err := rand.Read(b); err != nil {
+ return "", err
+ }
+
+ return fmt.Sprintf("%X", b), nil
+}
diff --git a/pkg/log/logging.go b/pkg/log/logging.go
new file mode 100644
index 0000000..5611ae4
--- /dev/null
+++ b/pkg/log/logging.go
@@ -0,0 +1,37 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package log
+
+import (
+ "github.com/op/go-logging"
+ "os"
+)
+
+// Log is the handler for the logger
+var Log = logging.MustGetLogger("sofaraum-server")
+
+var format = logging.MustStringFormatter(
+ `%{color}%{time:2006-01-02 15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
+)
+
+// InitLogger initializes the global log handler
+func InitLogger() {
+ backend := logging.NewLogBackend(os.Stderr, "", 0)
+ backendFormatter := logging.NewBackendFormatter(backend, format)
+ logging.SetBackend(backendFormatter)
+}
diff --git a/pkg/models/error.go b/pkg/models/error.go
new file mode 100644
index 0000000..0f4080b
--- /dev/null
+++ b/pkg/models/error.go
@@ -0,0 +1,152 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package models
+
+import (
+ "code.vikunja.io/web"
+ "fmt"
+ "net/http"
+)
+
+// ErrInvalidData represents a "ErrInvalidData" kind of error. Used when a struct is invalid -> validation failed.
+type ErrInvalidData struct {
+ Message string
+}
+
+// IsErrInvalidData checks if an error is a ErrIDCannotBeZero.
+func IsErrInvalidData(err error) bool {
+ _, ok := err.(ErrInvalidData)
+ return ok
+}
+
+func (err ErrInvalidData) Error() string {
+ return fmt.Sprintf("Struct is invalid. %s", err.Message)
+}
+
+// ErrCodeInvalidData holds the unique world-error code of this error
+const ErrCodeInvalidData = 1000
+
+// HTTPError holds the http error description
+func (err ErrInvalidData) HTTPError() web.HTTPError {
+ return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeInvalidData, Message: err.Message}
+}
+
+// ValidationHTTPError is the http error when a validation fails
+type ValidationHTTPError struct {
+ web.HTTPError
+ InvalidFields []string `json:"invalid_fields"`
+}
+
+// Error implements the Error type (so we can return it as type error)
+func (err ValidationHTTPError) Error() string {
+ theErr := ErrInvalidData{
+ Message: err.Message,
+ }
+ return theErr.Error()
+}
+
+// =============
+// User Errors
+// =============
+
+// ErrUserDoesNotExist represents a "UserDoesNotExist" kind of error.
+type ErrUserDoesNotExist struct {
+ UserID int64
+}
+
+// IsErrUserDoesNotExist checks if an error is a ErrUserDoesNotExist.
+func IsErrUserDoesNotExist(err error) bool {
+ _, ok := err.(ErrUserDoesNotExist)
+ return ok
+}
+
+func (err ErrUserDoesNotExist) Error() string {
+ return fmt.Sprintf("User does not exist [user id: %d]", err.UserID)
+}
+
+// ErrCodeUserDoesNotExist holds the unique world-error code of this error
+const ErrCodeUserDoesNotExist = 2000
+
+// HTTPError holds the http error description
+func (err ErrUserDoesNotExist) HTTPError() web.HTTPError {
+ return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeUserDoesNotExist, Message: "The user does not exist."}
+}
+
+// ErrNoUsernamePassword represents a "NoUsernamePassword" kind of error.
+type ErrNoUsernamePassword struct{}
+
+// IsErrNoUsernamePassword checks if an error is a ErrNoUsernamePassword.
+func IsErrNoUsernamePassword(err error) bool {
+ _, ok := err.(ErrNoUsernamePassword)
+ return ok
+}
+
+func (err ErrNoUsernamePassword) Error() string {
+ return fmt.Sprintf("No username and password provided")
+}
+
+// ErrCodeNoUsernamePassword holds the unique world-error code of this error
+const ErrCodeNoUsernamePassword = 2001
+
+// HTTPError holds the http error description
+func (err ErrNoUsernamePassword) HTTPError() web.HTTPError {
+ return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeNoUsernamePassword, Message: "Please specify a username and a password."}
+}
+
+// ErrWrongUsernameOrPassword is an error where the email was not confirmed
+type ErrWrongUsernameOrPassword struct {
+}
+
+func (err ErrWrongUsernameOrPassword) Error() string {
+ return fmt.Sprintf("Wrong username or password")
+}
+
+// ErrCodeWrongUsernameOrPassword holds the unique world-error code of this error
+const ErrCodeWrongUsernameOrPassword = 2002
+
+// HTTPError holds the http error description
+func (err ErrWrongUsernameOrPassword) HTTPError() web.HTTPError {
+ return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeWrongUsernameOrPassword, Message: "Wrong username or password."}
+}
+
+// IsErrWrongUsernameOrPassword checks if an error is a IsErrEmailNotConfirmed.
+func IsErrWrongUsernameOrPassword(err error) bool {
+ _, ok := err.(ErrWrongUsernameOrPassword)
+ return ok
+}
+
+// ErrCouldNotGetUserID represents a "ErrCouldNotGetUserID" kind of error.
+type ErrCouldNotGetUserID struct{}
+
+// IsErrCouldNotGetUserID checks if an error is a ErrCouldNotGetUserID.
+func IsErrCouldNotGetUserID(err error) bool {
+ _, ok := err.(ErrCouldNotGetUserID)
+ return ok
+}
+
+func (err ErrCouldNotGetUserID) Error() string {
+ return fmt.Sprintf("Could not get user ID")
+}
+
+// ErrCodeCouldNotGetUserID holds the unique world-error code of this error
+const ErrCodeCouldNotGetUserID = 2003
+
+// HTTPError holds the http error description
+func (err ErrCouldNotGetUserID) HTTPError() web.HTTPError {
+ return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeCouldNotGetUserID, Message: "Could not get user id."}
+}
diff --git a/pkg/models/models.go b/pkg/models/models.go
new file mode 100644
index 0000000..c3a391b
--- /dev/null
+++ b/pkg/models/models.go
@@ -0,0 +1,110 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package models
+
+import (
+ "fmt"
+ _ "github.com/go-sql-driver/mysql" // Because.
+ "github.com/go-xorm/core"
+ "github.com/go-xorm/xorm"
+ xrc "github.com/go-xorm/xorm-redis-cache"
+ _ "github.com/mattn/go-sqlite3" // Because.
+
+ "encoding/gob"
+ "github.com/spf13/viper"
+)
+
+var (
+ x *xorm.Engine
+
+ tables []interface{}
+)
+
+func getEngine() (*xorm.Engine, error) {
+ // Use Mysql if set
+ if viper.GetString("database.type") == "mysql" {
+ connStr := fmt.Sprintf(
+ "%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true",
+ viper.GetString("database.user"),
+ viper.GetString("database.password"),
+ viper.GetString("database.host"),
+ viper.GetString("database.database"))
+ e, err := xorm.NewEngine("mysql", connStr)
+ e.SetMaxOpenConns(viper.GetInt("database.openconnections"))
+ return e, err
+ }
+
+ // Otherwise use sqlite
+ path := viper.GetString("database.path")
+ if path == "" {
+ path = "./db.db"
+ }
+ return xorm.NewEngine("sqlite3", path)
+}
+
+func init() {
+ tables = append(tables)
+}
+
+// SetEngine sets the xorm.Engine
+func SetEngine() (err error) {
+ x, err = getEngine()
+ if err != nil {
+ return fmt.Errorf("Failed to connect to database: %v", err)
+ }
+
+ // Cache
+ if viper.GetBool("cache.enabled") {
+ switch viper.GetString("cache.type") {
+ case "memory":
+ cacher := xorm.NewLRUCacher(xorm.NewMemoryStore(), viper.GetInt("cache.maxelementsize"))
+ x.SetDefaultCacher(cacher)
+ break
+ case "redis":
+ cacher := xrc.NewRedisCacher(viper.GetString("cache.redishost"), viper.GetString("cache.redispassword"), xrc.DEFAULT_EXPIRATION, x.Logger())
+ x.SetDefaultCacher(cacher)
+ gob.Register(tables)
+ break
+ default:
+ fmt.Println("Did not find a valid cache type. Caching disabled. Please refer to the docs for poosible cache types.")
+ }
+ }
+
+ x.SetMapper(core.GonicMapper{})
+
+ // Sync dat shit
+ if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil {
+ return fmt.Errorf("sync database struct error: %v", err)
+ }
+
+ x.ShowSQL(viper.GetBool("database.showqueries"))
+
+ return nil
+}
+
+func getLimitFromPageIndex(page int) (limit, start int) {
+
+ // Get everything when page index is -1
+ if page < 0 {
+ return 0, 0
+ }
+
+ limit = viper.GetInt("service.pagecount")
+ start = limit * (page - 1)
+ return
+}
diff --git a/pkg/models/user.go b/pkg/models/user.go
new file mode 100644
index 0000000..0b10814
--- /dev/null
+++ b/pkg/models/user.go
@@ -0,0 +1,157 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package models
+
+import (
+ "code.sofaraum.de/server/pkg/log"
+ "code.vikunja.io/web"
+ "fmt"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/labstack/echo"
+ "golang.org/x/crypto/bcrypt"
+ "reflect"
+)
+
+// UserLogin Object to recive user credentials in JSON format
+type UserLogin struct {
+ Username string `json:"username" form:"username"`
+ Password string `json:"password" form:"password"`
+}
+
+// User holds information about an user
+type User struct {
+ ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
+ Username string `xorm:"varchar(250) not null unique" json:"username" valid:"length(3|250)"`
+ Password string `xorm:"varchar(250) not null" json:"-"`
+ Email string `xorm:"varchar(250)" json:"email" valid:"email,length(0|250)"`
+ IsActive bool `json:"-"`
+
+ PasswordResetToken string `xorm:"varchar(450)" json:"-"`
+ EmailConfirmToken string `xorm:"varchar(450)" json:"-"`
+
+ Created int64 `xorm:"created" json:"created"`
+ Updated int64 `xorm:"updated" json:"updated"`
+
+ web.Auth `xorm:"-" json:"-"`
+}
+
+// AuthDummy implements the auth of the crud handler
+func (User) AuthDummy() {}
+
+// TableName returns the table name for users
+func (User) TableName() string {
+ return "users"
+}
+
+func getUserForRights(a web.Auth) *User {
+ u, err := getUserWithError(a)
+ if err != nil {
+ log.Log.Error(err.Error())
+ }
+ return u
+}
+
+func getUserWithError(a web.Auth) (*User, error) {
+ u, is := a.(*User)
+ if !is {
+ return &User{}, fmt.Errorf("user is not user element, is %s", reflect.TypeOf(a))
+ }
+ return u, nil
+}
+
+// APIUserPassword represents a user object without timestamps and a json password field.
+type APIUserPassword struct {
+ ID int64 `json:"id"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+ Email string `json:"email"`
+}
+
+// APIFormat formats an API User into a normal user struct
+func (apiUser *APIUserPassword) APIFormat() User {
+ return User{
+ ID: apiUser.ID,
+ Username: apiUser.Username,
+ Password: apiUser.Password,
+ Email: apiUser.Email,
+ }
+}
+
+// GetUserByID gets informations about a user by its ID
+func GetUserByID(id int64) (user *User, err error) {
+ // Apparently xorm does otherwise look for all users but return only one, which leads to returing one even if the ID is 0
+ if id < 1 {
+ return &User{}, ErrUserDoesNotExist{}
+ }
+
+ return GetUser(&User{ID: id})
+}
+
+// GetUser gets a user object
+func GetUser(user *User) (userOut *User, err error) {
+ userOut = user
+ exists, err := x.Get(&userOut)
+
+ if !exists {
+ return &User{}, ErrUserDoesNotExist{UserID: user.ID}
+ }
+
+ return userOut, err
+}
+
+// CheckUserCredentials checks user credentials
+func CheckUserCredentials(u *UserLogin) (*User, error) {
+ // Check if we have any credentials
+ if u.Password == "" || u.Username == "" {
+ return &User{}, ErrNoUsernamePassword{}
+ }
+
+ // Check if the user exists
+ user, err := GetUser(&User{Username: u.Username})
+ if err != nil {
+ return &User{}, err
+ }
+
+ // Check the users password
+ err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password))
+ if err != nil {
+ if err == bcrypt.ErrMismatchedHashAndPassword {
+ return &User{}, ErrWrongUsernameOrPassword{}
+ }
+ return &User{}, err
+ }
+
+ return user, nil
+}
+
+// GetCurrentUser returns the current user based on its jwt token
+func GetCurrentUser(c echo.Context) (user *User, err error) {
+ jwtinf := c.Get("user").(*jwt.Token)
+ claims := jwtinf.Claims.(jwt.MapClaims)
+ userID, ok := claims["id"].(float64)
+ if !ok {
+ return user, ErrCouldNotGetUserID{}
+ }
+ user = &User{
+ ID: int64(userID),
+ Email: claims["email"].(string),
+ Username: claims["username"].(string),
+ }
+
+ return
+}
diff --git a/pkg/routes/api/v1/login.go b/pkg/routes/api/v1/login.go
new file mode 100644
index 0000000..2b908e9
--- /dev/null
+++ b/pkg/routes/api/v1/login.go
@@ -0,0 +1,80 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package v1
+
+import (
+ "code.sofaraum.de/server/pkg/models"
+ "code.vikunja.io/web/handler"
+ "crypto/md5"
+ "encoding/hex"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/labstack/echo"
+ "github.com/spf13/viper"
+ "net/http"
+ "time"
+)
+
+// Token represents an authentification token
+type Token struct {
+ Token string `json:"token"`
+}
+
+// Login is the login handler
+// @Summary Login
+// @Description Logs a user in. Returns a JWT-Token to authenticate further requests.
+// @tags user
+// @Accept json
+// @Produce json
+// @Param credentials body models.UserLogin true "The login credentials"
+// @Success 200 {object} v1.Token
+// @Failure 400 {object} models.Message "Invalid user password model."
+// @Failure 403 {object} models.Message "Invalid username or password."
+// @Router /login [post]
+func Login(c echo.Context) error {
+ u := models.UserLogin{}
+ if err := c.Bind(&u); err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Please provide a username and password.")
+ }
+
+ // Check user
+ user, err := models.CheckUserCredentials(&u)
+ if err != nil {
+ return handler.HandleHTTPError(err, c)
+ }
+
+ // Create token
+ token := jwt.New(jwt.SigningMethodHS256)
+
+ // Set claims
+ claims := token.Claims.(jwt.MapClaims)
+ claims["username"] = user.Username
+ claims["email"] = user.Email
+ claims["id"] = user.ID
+ claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
+
+ avatar := md5.Sum([]byte(user.Email))
+ claims["avatar"] = hex.EncodeToString(avatar[:])
+
+ // Generate encoded token and send it as response.
+ t, err := token.SignedString([]byte(viper.GetString("service.JWTSecret")))
+ if err != nil {
+ return err
+ }
+
+ return c.JSON(http.StatusOK, Token{Token: t})
+}
diff --git a/pkg/routes/api/v1/token_check.go b/pkg/routes/api/v1/token_check.go
new file mode 100644
index 0000000..8291be9
--- /dev/null
+++ b/pkg/routes/api/v1/token_check.go
@@ -0,0 +1,34 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package v1
+
+import (
+ "fmt"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/labstack/echo"
+)
+
+// CheckToken checks prints a message if the token is valid or not. Currently only used for testing pourposes.
+func CheckToken(c echo.Context) error {
+
+ user := c.Get("user").(*jwt.Token)
+
+ fmt.Println(user.Valid)
+
+ return c.String(418, "🍵")
+}
diff --git a/pkg/routes/api/v1/user_show.go b/pkg/routes/api/v1/user_show.go
new file mode 100644
index 0000000..156c3cd
--- /dev/null
+++ b/pkg/routes/api/v1/user_show.go
@@ -0,0 +1,50 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package v1
+
+import (
+ "code.sofaraum.de/server/pkg/models"
+ "code.vikunja.io/web/handler"
+ "github.com/labstack/echo"
+ "net/http"
+)
+
+// UserShow gets all informations about the current user
+// @Summary Get user information
+// @Description Returns the current user object.
+// @tags user
+// @Accept json
+// @Produce json
+// @Security ApiKeyAuth
+// @Success 200 {object} models.User
+// @Failure 404 {object} code.vikunja.io/web.HTTPError "User does not exist."
+// @Failure 500 {object} models.Message "Internal server error."
+// @Router /user [get]
+func UserShow(c echo.Context) error {
+ userInfos, err := models.GetCurrentUser(c)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, "Error getting current user.")
+ }
+
+ user, err := models.GetUserByID(userInfos.ID)
+ if err != nil {
+ return handler.HandleHTTPError(err, c)
+ }
+
+ return c.JSON(http.StatusOK, user)
+}
diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go
new file mode 100644
index 0000000..3527a60
--- /dev/null
+++ b/pkg/routes/routes.go
@@ -0,0 +1,120 @@
+// Sofaraum server is the server which collects the statistics
+// for the sofaraum-heatmap application.
+// Copyright 2018 K.Langenberg and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+// @title Sofaraum Server API
+// @license.name GPLv3
+// @BasePath /api/v1
+
+// @securityDefinitions.apikey ApiKeyAuth
+// @in header
+// @name Authorization
+
+package routes
+
+import (
+ _ "code.sofaraum.de/server/docs" // To generate swagger docs
+ "code.sofaraum.de/server/pkg/log"
+ "code.sofaraum.de/server/pkg/models"
+ apiv1 "code.sofaraum.de/server/pkg/routes/api/v1"
+ "code.vikunja.io/web"
+ "github.com/asaskevich/govalidator"
+ "github.com/labstack/echo"
+ "github.com/labstack/echo/middleware"
+ "github.com/spf13/viper"
+ "github.com/swaggo/echo-swagger"
+)
+
+// CustomValidator is a dummy struct to use govalidator with echo
+type CustomValidator struct{}
+
+// Validate validates stuff
+func (cv *CustomValidator) Validate(i interface{}) error {
+ if _, err := govalidator.ValidateStruct(i); err != nil {
+
+ var errs []string
+ for field, e := range govalidator.ErrorsByField(err) {
+ errs = append(errs, field+": "+e)
+ }
+
+ httperr := models.ValidationHTTPError{
+ web.HTTPError{
+ Code: models.ErrCodeInvalidData,
+ Message: "Invalid Data",
+ },
+ errs,
+ }
+
+ return httperr
+ }
+ return nil
+}
+
+// NewEcho registers a new Echo instance
+func NewEcho() *echo.Echo {
+ e := echo.New()
+
+ e.HideBanner = true
+
+ // Logger
+ e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
+ Format: "${time_rfc3339_nano}: ${remote_ip} ${method} ${status} ${uri} ${latency_human} - ${user_agent}\n",
+ }))
+
+ // Validation
+ e.Validator = &CustomValidator{}
+
+ return e
+}
+
+// RegisterRoutes registers all routes for the application
+func RegisterRoutes(e *echo.Echo) {
+
+ // CORS_SHIT
+ e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
+ AllowOrigins: []string{"*"},
+ }))
+
+ // API Routes
+ a := e.Group("/api/v1")
+
+ // Swagger UI
+ a.GET("/swagger/*", echoSwagger.WrapHandler)
+
+ a.POST("/login", apiv1.Login)
+
+ // ===== Routes with Authetification =====
+ // Authetification
+ a.Use(middleware.JWT([]byte(viper.GetString("service.JWTSecret"))))
+
+ // Put the authprovider in the context to be able to use it later
+ e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ c.Set("AuthProvider", &web.Auths{
+ AuthObject: func(echo.Context) (web.Auth, error) {
+ return models.GetCurrentUser(c)
+ },
+ })
+ c.Set("LoggingProvider", log.Log)
+ return next(c)
+ }
+ })
+
+ a.POST("/tokenTest", apiv1.CheckToken)
+
+ // User stuff
+ a.GET("/user", apiv1.UserShow)
+}