basic structure
This commit is contained in:
parent
52fcb8f157
commit
c1d1a59a61
|
@ -1,3 +1,5 @@
|
||||||
.idea/
|
.idea/
|
||||||
server
|
server
|
||||||
sofaraum-server
|
sofaraum-server
|
||||||
|
config.yml
|
||||||
|
config.yaml
|
26
go.mod
26
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
|
||||||
|
)
|
||||||
|
|
62
main.go
62
main.go
|
@ -17,8 +17,66 @@
|
||||||
|
|
||||||
package main
|
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() {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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."}
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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})
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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, "🍵")
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// @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)
|
||||||
|
}
|
Loading…
Reference in New Issue