Commit c6c98382 authored by Martin/Geno's avatar Martin/Geno

init meetandeat

parents
Pipeline #350 passed with stages
in 6 minutes and 2 seconds
#!/bin/bash
result="$(gofmt -s -l . | grep -v '^vendor/' )"
if [ -n "$result" ]; then
echo "Go code is not formatted, run 'gofmt -s -w .'" >&2
echo "$result"
exit 1
fi
#!/usr/bin/env python
# checks if every desired package has test files
import os
import re
import sys
source_re = re.compile(".*\.go")
test_re = re.compile(".*_test\.go")
missing = False
for root, dirs, files in os.walk("."):
# ignore some paths
if root == "." or root.startswith("./vendor") or root.startswith("./."):
continue
# source files but not test files?
if len(filter(source_re.match, files)) > 0 and len(filter(test_re.match, files)) == 0:
print("no test files for {}".format(root))
missing = True
if missing:
sys.exit(1)
else:
print("every package has test files")
image: golang:latest
stages:
- build
- test
- deploy
before_script:
- mkdir -p "/go/src/dev.sum7.eu/$CI_PROJECT_NAMESPACE/"
- cp -R "/builds/$CI_PROJECT_PATH" "/go/src/dev.sum7.eu/$CI_PROJECT_NAMESPACE/"
- cd "/go/src/dev.sum7.eu/$CI_PROJECT_PATH"
- go get -d -t ./...
build-my-project:
stage: build
script:
- go install "dev.sum7.eu/$CI_PROJECT_PATH"
- mv "/go/bin/$CI_PROJECT_NAME" "/builds/$CI_PROJECT_PATH"
artifacts:
paths:
- config_example.conf
- "$CI_PROJECT_NAME"
test-my-project:
stage: test
script:
- go get github.com/client9/misspell/cmd/misspell
- find . -type f | grep -v webroot/assets | xargs misspell -error
- ./.ci/check-gofmt
- ./.ci/check-testfiles
- go test $(go list ./... | grep -v /vendor/) -v -coverprofile .testCoverage.txt
artifacts:
paths:
- .testCoverage.txt
test-race-my-project:
stage: test
script:
- go test -race ./...
deploy:
stage: deploy
only:
- master
script:
- go install "dev.sum7.eu/$CI_PROJECT_PATH"
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- 'which rsync || ( apt-get update -y && apt-get install rsync -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- rsync -e "ssh -6 -o StrictHostKeyChecking=no -p $SSH_PORT" -a --delete "/builds/$CI_PROJECT_PATH/webroot/" "$CI_PROJECT_NAME@$SSH_HOST":/opt/$CI_PROJECT_NAME/webroot/
- ssh -6 -o StrictHostKeyChecking=no -p $SSH_PORT "$CI_PROJECT_NAME@$SSH_HOST" sudo /usr/bin/systemctl stop $CI_PROJECT_NAME
- scp -6 -o StrictHostKeyChecking=no -P $SSH_PORT "/go/bin/$CI_PROJECT_NAME" "$CI_PROJECT_NAME@$SSH_HOST":/opt/$CI_PROJECT_NAME/bin
- ssh -6 -o StrictHostKeyChecking=no -p $SSH_PORT "$CI_PROJECT_NAME@$SSH_HOST" sudo /usr/bin/systemctl start $CI_PROJECT_NAME
# meetandeat
[![Build Status](https://dev.sum7.eu/genofire/meetandeat/badges/master/build.svg)](https://dev.sum7.eu/genofire/meetandeat/pipelines)
[![Go Report Card](https://goreportcard.com/badge/dev.sum7.eu/genofire/meetandeat)](https://goreportcard.com/report/dev.sum7.eu/genofire/meetandeat)
[![GoDoc](https://godoc.org/dev.sum7.eu/genofire/meetandeat?status.svg)](https://godoc.org/dev.sum7.eu/genofire/meetandeat)
## Get meetandeat
#### Download
Latest Build binary from ci here:
[Download All](https://dev.sum7.eu/genofire/meetandeat/-/jobs/artifacts/master/download/?job=build-my-project) (with config example)
[Download Binary](https://dev.sum7.eu/genofire/meetandeat/-/jobs/artifacts/master/raw/meetandeat?inline=false&job=build-my-project)
#### Build
```bash
go get -u dev.sum7.eu/genofire/meetandeat
```
## Configure
see `config_example.conf`
## Start / Boot
_/lib/systemd/system/meetandeat.service_ :
```
[Unit]
Description=meetandeat
After=network.target
[Service]
Type=simple
# User=notRoot
ExecStart=/opt/go/bin/meetandeat serve --config /etc/meetandeat.conf
Restart=always
RestartSec=5sec
[Install]
WantedBy=multi-user.target
```
Start: `systemctl start meetandeat`
Autostart: `systemctl enable meetandeat`
package cmd
import (
"github.com/bdlm/log"
"github.com/spf13/cobra"
)
var (
debug bool
timestamps bool
)
// RootCMD represents the base command when called without any subcommands
var RootCMD = &cobra.Command{
Use: "meetandeat",
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the RootCMD.
func Execute() {
if err := RootCMD.Execute(); err != nil {
log.Error(err)
}
}
func init() {
cobra.OnInitialize(func() {
if debug {
log.SetLevel(log.DebugLevel)
}
log.SetFormatter(&log.TextFormatter{
DisableTimestamp: timestamps,
})
log.Debug("show debug")
})
RootCMD.PersistentFlags().BoolVar(&debug, "v", false, "show debug log")
RootCMD.PersistentFlags().BoolVar(&timestamps, "timestamps", false, "Enables timestamps for log output")
}
package cmd
import (
"dev.sum7.eu/genofire/golang-lib/database"
"dev.sum7.eu/genofire/golang-lib/file"
"github.com/bdlm/log"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-contrib/static"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
"golang.org/x/crypto/acme/autocert"
"dev.sum7.eu/genofire/meetandeat/controller"
"dev.sum7.eu/genofire/meetandeat/runtime"
)
var configPath = "/etc/meetandeat.conf"
type ServeConfig struct {
RequestLog bool `toml:"request_log"`
RequestLimit int `toml:"request_limit"`
SessionSecret string `toml:"session_secret"`
Listen string `toml:"listen"`
Webroot string `toml:"webroot"`
Database database.Config `toml:"database"`
ACME struct {
Enable bool `toml:"enable"`
Domains []string `toml:"domains"`
Cache string `toml:"cache"`
} `toml:"acme"`
}
// serveCMD represents the query command
var serveCMD = &cobra.Command{
Use: "serve <interfaces>",
Short: "run webserver",
Example: `meetandeat serve "/etc/meetandeat.conf"`,
Run: func(cmd *cobra.Command, args []string) {
config := &ServeConfig{}
if err := file.ReadTOML(configPath, config); err != nil {
log.Panicf("open config file: %s", err)
}
if err := database.Open(config.Database); err != nil {
log.Panicf("no database connection: %s", err)
}
gin.SetMode(gin.ReleaseMode)
r := gin.New()
if config.RequestLog {
r.Use(gin.Logger())
log.Debug("request logging enabled")
}
r.Use(runtime.MaxAllowed(config.RequestLimit))
r.Use(gin.Recovery())
r.Use(sessions.Sessions("sid", cookie.NewStore([]byte(config.SessionSecret))))
controller.Bind(&r.RouterGroup)
r.Use(static.Serve("/", static.LocalFile(config.Webroot, false)))
if config.ACME.Enable {
if config.Listen != "" {
log.Panic("For ACME / Let's Encrypt it is not possible to set `listen`")
}
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(config.ACME.Domains...),
Cache: autocert.DirCache(config.ACME.Cache),
}
log.Fatal(autotls.RunWithManager(r, &m))
} else {
r.Run(config.Listen)
}
},
}
func init() {
serveCMD.PersistentFlags().StringVar(&configPath, "config", configPath, "path to config file")
RootCMD.AddCommand(serveCMD)
}
log_level = 50
request_log = true
request_limit = 10
session_secret = "secret"
webroot = "webroot"
listen = ":8080"
# if ACME / Let's Encrypt used, uncomment listen
[acme]
enable = false
domains = ["example1.com", "example2.com"]
cache = "/var/www/.cache"
[database]
type = "sqlite3"
logging = true
connection = "file::memory:?mode=memory&cache=shared"
# For Master-Slave cluster
# read_connection = ""
package api
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"dev.sum7.eu/genofire/golang-lib/database"
"dev.sum7.eu/genofire/meetandeat/models"
)
func getDateAll(c *gin.Context) {
list := []*models.Date{}
database.Read.Find(&list)
c.JSON(http.StatusOK, list)
}
func getDate(c *gin.Context) {
id, err := strconv.Atoi(c.Params.ByName("id"))
if err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
d := models.Date{
ID: id,
}
res := database.Read.Preload("Meets").First(&d)
if res.RecordNotFound() {
c.JSON(http.StatusNotFound, res.Error.Error())
return
}
if res.Error != nil {
c.JSON(http.StatusBadRequest, res.Error.Error())
return
}
c.JSON(http.StatusOK, d)
}
func addDate(c *gin.Context) {
var v time.Time
if err := c.BindJSON(&v); err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
if v.Before(time.Now()) {
c.JSON(http.StatusBadRequest, "date has to be in the future")
return
}
d := &models.Date{
Date: v,
}
database.Write.Save(d)
c.JSON(http.StatusOK, d)
}
func addDateMeet(c *gin.Context) {
var v models.DateMeet
if err := c.BindJSON(&v); err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
id, err := strconv.Atoi(c.Params.ByName("id"))
if err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
v.DateID = id
database.Write.Save(&v)
c.JSON(http.StatusOK, v)
}
package api
// Nothing to test
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"dev.sum7.eu/genofire/golang-lib/database"
"dev.sum7.eu/genofire/meetandeat/models"
)
func getJobAll(c *gin.Context) {
list := []*models.Job{}
database.Read.Find(&list)
c.JSON(http.StatusOK, list)
}
func addJob(c *gin.Context) {
var name string
if err := c.BindJSON(&name); err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
d := &models.Job{
Name: name,
}
database.Write.Save(d)
c.JSON(http.StatusOK, d)
}
package api
import (
"github.com/gin-gonic/gin"
)
func Bind(r *gin.RouterGroup) {
r.GET("/date", getDateAll)
r.POST("/date", addDate)
r.GET("/date/:id", getDate)
r.POST("/date/:id/meet", addDateMeet)
r.GET("/job", getJobAll)
r.POST("/job", addJob)
r.GET("/person", getPersonAll)
r.POST("/person", addPerson)
}
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"dev.sum7.eu/genofire/golang-lib/database"
"dev.sum7.eu/genofire/meetandeat/models"
)
func getPersonAll(c *gin.Context) {
list := []*models.Person{}
database.Read.Find(&list)
c.JSON(http.StatusOK, list)
}
func addPerson(c *gin.Context) {
var name string
if err := c.BindJSON(&name); err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
d := &models.Person{
Name: name,
}
database.Write.Save(d)
c.JSON(http.StatusOK, d)
}
package controller
// Nothing to test
package examples
// Nothing to test
package examples
import (
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func incr(c *gin.Context) {
session := sessions.Default(c)
var count int
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
session.Set("count", count)
session.Save()
c.JSON(http.StatusOK, gin.H{"count": count})
}
package examples
import (
"github.com/gin-gonic/gin"
)
func Bind(r *gin.RouterGroup) {
r.GET("/ping", ping)
r.GET("/incr", incr)
}
package examples
import (
"net/http"
"github.com/gin-gonic/gin"
)
func ping(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
}
package controller
import (
"github.com/gin-gonic/gin"
"dev.sum7.eu/genofire/meetandeat/controller/api"
"dev.sum7.eu/genofire/meetandeat/controller/examples"
)
func Bind(r *gin.RouterGroup) {
group := r.Group("/examples")
{
examples.Bind(group)
}
group = r.Group("/api")
{
api.Bind(group)
}
r.GET("/status", status)
}
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"dev.sum7.eu/genofire/golang-lib/database"
)
func status(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "running",
"database": map[string]interface{}{
"read": database.Read.DB().Ping() == nil,
"write": database.Write.DB().Ping() == nil,
},
})
}
package main
import "dev.sum7.eu/genofire/meetandeat/cmd"
func main() {
cmd.Execute()
}
package models
import (
"time"
"dev.sum7.eu/genofire/golang-lib/database"
)
type Date struct {
ID int `json:"id" gorm:"PRIMARY_KEY"`
Date time.Time `json:"date"`
Meets []DateMeet `gorm:"foreignkey:ID" json:"meets"`
}
// Function to initialize the database
func init() {
database.AddModel(&Date{})
}
package models
import (
"dev.sum7.eu/genofire/golang-lib/database"
)
type DateMeet struct {
ID int `gorm:"PRIMARY_KEY" json:"id"`
DateID int `sql:"type:bigint NOT NULL REFERENCES date(id);column:date_id" json:"dateID,omitempty"`
Date *Date `gorm:"association_autoupdate:false;foreignkey:DateID;" json:"date,omitempty"`
PersonID int `sql:"type:bigint NOT NULL REFERENCES person(id);column:person_id" json:"personID,omitempty"`
Person *Person `gorm:"association_autoupdate:false;foreignkey:PersonID;" json:"person,omitempty"`
JobID int `sql:"type:bigint NOT NULL REFERENCES job(id);column:job_id" json:"jobID,omitempty"`
Job *Job `gorm:"association_autoupdate:false;foreignkey:JobID;" json:"job,omitempty"`
}
// Function to initialize the database
func init() {
database.AddModel(&DateMeet{})
}
package models
package models
import (
"dev.sum7.eu/genofire/golang-lib/database"
)
type Job struct {
ID int `json:"id" gorm:"PRIMARY_KEY"`
Name string `json:"name"`
}
// Function to initialize the database
func init() {
database.AddModel(&Job{})
}
package models
import (
"dev.sum7.eu/genofire/golang-lib/database"
)
type Person struct {
ID int `json:"id" gorm:"PRIMARY_KEY"`
Name string `json:"name"`
}
// Function to initialize the database
func init() {
database.AddModel(&Person{})
}
package runtime
// Nothing to test
package runtime
import "golang.org/x/crypto/pbkdf2"
import "hash"
import "strconv"
import "encoding/base64"
import "crypto/sha1"
import "crypto/sha256"
import "crypto/sha512"
import "crypto/rand"
import "fmt"
import "strings"
const (
saltLength = 8
hashLength = 20
interations = 10000
hashfunc string = "sha256"
)
var hashlib = map[string]func() hash.Hash{
"sha1": sha1.New,
"sha256": sha256.New,
"sha512": sha512.New,
}
// HashValidate a password and a hash
func HashValidate(hash, password string) (output, replace bool) {
parts := strings.Split(hash, "$")
if len(parts) != 4 {
return false, false
}
curIter, err := strconv.Atoi(parts[1])
if err != nil {
return false, false
}
hashfuncC := strings.Split(parts[0], "_")[1]
replace = (hashfuncC != hashfunc)
dk := pbkdf2.Key([]byte(password), []byte(parts[2]), curIter, len(parts[3])-8, hashlib[hashfuncC])
x := fmt.Sprintf("pbkdf2_%s$%s$%s$%s", hashfuncC, parts[1], parts[2], base64.StdEncoding.EncodeToString(dk))
output = (x == hash)
return
}
// GenerateRandomString by length for key
func GenerateRandomString(n int) (string, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// NewHash of given password
func NewHash(password string) string {
salt, _ := GenerateRandomString(saltLength)
dk := pbkdf2.Key([]byte(password), []byte(salt), interations, hashLength, hashlib[hashfunc])
return fmt.Sprintf("pbkdf2_%s$%d$%s$%s", hashfunc, interations, salt, base64.StdEncoding.EncodeToString(dk))
}
package runtime
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestHash(t *testing.T) {
assert := assert.New(t)
password := "root"
x, err := HashValidate("pbkdf2_sha1$10000$a5viM+Paz3o=$orD4shu1Ss+1wPAhAt8hkZ/fH7Y=", password)
assert.True(x, "password is valid")
assert.True(err, "password hesh is outdated")
x, err = HashValidate("pbkdf2_sha256$10000$TnTxDT6fLiE=$v3EGDGIgmgSWXaLYvQYlSf8Lob0=", password)
assert.True(x, "password is valid")
assert.False(err, "password hash is outdated")
hashed := NewHash(password)
expectedBegin := "pbkdf2_sha256$10000$"