├── .env.sample ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Dockerrun.aws.json ├── README.md ├── cmd └── genmigration │ └── genmigration.go ├── config ├── config.go ├── context.go └── sigfoxApiKeys.go ├── controllers ├── authentication.go ├── base.go ├── export.go ├── group.go ├── organization.go └── user.go ├── docker-compose.yaml ├── go.mod ├── go.sum ├── helpers ├── error.go ├── jwt.go ├── params │ └── params.go └── random_string.go ├── lumen-logo.png ├── main.go ├── middlewares ├── authentication.go ├── authorization.go ├── config.go ├── cors.go ├── email.go ├── error.go ├── store.go └── text.go ├── migrations.md ├── migrations ├── 201911060735_users.go ├── migrator.go └── template.tmpl ├── models ├── generic.go ├── group.go ├── notificationdata.go ├── organization.go └── user.go ├── nginx ├── conf-https-step-1 └── conf-https-step-2 ├── prod.sh ├── robots.txt ├── server ├── api.go ├── database.go ├── index.go ├── router.go ├── seeder.go └── viper.go ├── services ├── email.go └── text.go ├── store ├── authorization.go ├── base.go ├── context.go ├── mongodb │ ├── generic.go │ └── mongo.go ├── postgresql │ ├── generic.go │ ├── group.go │ ├── organization.go │ ├── postgre.go │ └── user.go ├── signals.go └── store.go ├── templates └── html │ ├── page_account_activated.html │ └── page_define_password.html ├── tests ├── auth_controller_test.go ├── helpers_test.go ├── main_test.go ├── router_test.go └── user_controller_test.go ├── update.sh └── utils └── utils.go /.env.sample: -------------------------------------------------------------------------------- 1 | SAM_HOST_ADDRESS=localhost:4000 2 | SAM_API_URL=localhost:4000 3 | SAM_FRONT_URL=localhost:3000 4 | 5 | SAM_DB_TYPE=postgresql 6 | 7 | SAM_MONGO_DB_PREFIX=mongodb:// 8 | SAM_MONGO_DB_HOST=localhost:27017 9 | SAM_MONGO_DB_NAME=lumen-api 10 | 11 | SAM_POSTGRES_DB_NAME=postgres 12 | SAM_POSTGRES_DB_ADDR=localhost 13 | SAM_POSTGRES_DB_PORT=32768 14 | SAM_POSTGRES_DB_USER=postgres 15 | SAM_POSTGRES_DB_PASSWORD=postgrespw 16 | 17 | SAM_MAIL_SENDER_ADDRESS=no-reply@plugblocks.com 18 | SAM_MAIL_SENDER_NAME=Things 19 | SAM_AWS_API_ID= 20 | SAM_AWS_API_KEY= 21 | 22 | SAM_API_KEY=GKcMFJDdTKyUxvxhQGXDExfYzJbpXxZQcZfdWwwUvdnuMEqqCA 23 | 24 | SAM_RSA_PRIVATE=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS2dJQkFBS0NBZ0VBcnNRZ2tPcEk3NGZHdExpbGFHczJUSkNsbThrWXp4Y0RZeWs5V3d4QXRMWU9aN1NsCmFJR1l0c1FNbHZ4OEF6RkxZZXZwcTFZYWd4WXBHZDJuM1dGSGZhVzJzNEhtblU1QktWSnJzRHl4dGZaZXRmeVEKODlYMXRrQWEwemxHRkR0RXZrWEVaYTRMUzRDY3hkR1RFd3R2UGpiMzhsNW5xc3VpSTRpTWxVL3pub3o1N2FjNAplM01YR2JpUGltaXhmclBUWUwvdGhUSkEvNEN0VUNBUFN6bkN2YlRGSCtrRENwaXBwdW9LaEZMNlVJUE5Hd3YrClljL2h4Mk1iZ2ZaZnBiMWtGV1ZVYkY4VHV4SjZxTzZNWC94SzFPcmRYWVBBNUNiYXhRMmV1MHRXV2FQZGlsSWYKN0VwMkNyRUVZZTY5STBDTXVwci9tQzZkaHNQZytoK1l0RVM5YkduWVpPYnlSMjBXRmovVG5rdVhUZDhCZmxUVApjNUsrSmcwaE9lWUZNc0pXa0k5Wm5FdHd5U2RRZk9pTnB3SmJxZ21HL1NEK1Q4T3dNUDhmb0F1eXNEM09iRG1NClBiWGxCSUc1Q2toV09QS003Y1ZqS0dvZGRaM3RKYjYwSURicFdEQUFIci91YTVBYmROaG5RWG8wN0VZQjV0aW4KWDNlZ0F5aHNvbjVwNGdhR0x2bkh6TGNybDQ3Z3h5dGdwckpWakpoTjhVNVFoSStOYVkyalhxcXpBRlpLbE44MAoxbktVRFZoRjRYeUpqam80dFNQYkUzaDEwcjZPQytZdGZoc3FZRlhBWldxa242YmRqVHkzU0VIL2lXd0xnWkhOCmdGN3hzMXpUdUVIYnI5TUFmQ0tJbFpBNWpUUGFzeEhORHNncVowQlZFYWdHdE5vMWN5dGJjSDhJaEUwQ0F3RUEKQVFLQ0FnRUFoVWt6QlJLSk9aVTFxSkxDSTliUWZLZzhoWWxqRUYyZXlidWovWkprcnUzQ0lybVZCUlVCb1RIMwo0QmNEaFhQTTZBdUNGSmdBZEVVc1ozdXpFSldtYmw4NTdjRitYaTVXWit0aUVmRFlIOHljQXZOeW9XUm1sdTVoCkJ3TEJQYURPRnNjaXQrRjYzOFJnekVXL2cyRFBJSVhQcDlyeitVQ0FrZFVNVXJ1OU13aVZlL2h5alVRRE5DeWIKM0pWbitSZXdlRE1vTk5OVTJ3Z0tKZmV0Y2xQRS8rV2tDV2d1VjZDQ2tyZ2YrVGRIakt4WWV1NDB2ZWhMYTBOYwpzVXR6T3ZZc0pGMDVlOFV2U1JjWTBVbk5hVThDeTB1c05Ddk5xck12V2VXL1QxeHZxTUhlcmlWZ0QxZmltdGpTClhOWTFEWE9uWUpIaHFQQzBWeG1BdFlIdmRseU9rYUxQV0xkZFlhZ2xkYUEvazFwNWY2bnpWdmxFakJvRDhqSDIKVmRjRTZSQUw3QTBJY1JMbklmUmx5THoxUHhmYzliano4UTF3dkJ1WTdEVHd3eW1uRjE5TitxUUxyckRDRTEzegpTRzhLZ3V6U3pQZWM3QUViL1NWcXhZMEdVYnJ6R1N0dEpUS3I5WkVHcDZsL2ZSVW84OGhVTk83UG9TNGJnbW1mCkxkcjB0ZmNPSVdaaFBTc1MvMDl2N256em9TQkZLcXVKOWU5UHhrZU0vdk4xbFFUNXZra1NrVVZwWVRTSmlCM2cKd3VKOWwyOUhyYWVSb3hXdy9kbzNzZ1MrZDd0V3huYVp3QUkvY3UvU0VGd1JxMVJKTXY3ZTVKS21zcEpnL2dHagoxWjJPVHMrZzIybHpydlV4dWpqcis4c3RJVGxac1pHUWE4U1pxLysrM3RKRkpZdlpkWUVDZ2dFQkFPT1kydGF0CmlaYitDV2RsTWRzYUdMSXU5TWppQjl3SXFLM05wcklONjRLdk5ETGpHSENnQzA4UWZpeXMvNmtVdW1pbnlHY3kKSURjUHZYZ2dEbnI1aUpUQ21aTlJlWG9PWXNVZDVlSVlLR1Noc3d0SHJJVWxjWjd2OFh6clNqSUJoNXpKUGU5UgpiK2VDWVliK1lwWkJYNDM5emQrK2J1MUtWeXVUaUF0RVZEcWhZaW5uNXd4S00weTE3MEhYOWtqTjFWWFEydnNtCnkrQVp3cU9kN1k1Qlh6OTB2WFREWWZtK1ppSGd0Z2kxMlM2aGNiOEl3ZlNiV0ZrVEc2c3c5c1JnNGk1K2Q1WE8KeVgyaDNnRm5KdmtIaUNqazN4ekpQenJMVkFWZnJXRkFkSjE5ZXJ1a0ZFTmhSTUNSMm5BQ0d2UnF2VHpEZkZYZwpqQmRtdjIzWFd6RzZRekVDZ2dFQkFNU1RkVUZHR1VZYytvSy9PbE80YjU0KzRVcDlBOFNqM3BoK1o2WnZiN1RRCjVnQXZidjNnNURoUkRpTDRXTnNkbFp0NXBtYzBMcFd2NlBZd1J0M0JKbVg1VFhOV1ZybjVxaUJOOUR0ZUplZlcKNEZNR2xCdythSzh1VVZIQlRaTzFZbXdIMktHUmVtaXZ1eUVhMGk5WVFDUUQ2QjF0QUZQRjgwSHB3SUp3Qi9HQwpFYTErVnF5Y1c0SVU1RDI1Nk4yZXo0Qy9IRWU0NWtpUllHb3RZOGY5c2VwWHRZemVielVKdjh6a0ZjcDNOUVBrClpMaHNoZ2NUSGlmeTNzc2MxYmJtcUZjTnNYNysrQm9tT1JuT1JoeC9pdmZFcUJWTnA0cTJRTmZKZW9sa1dwM1QKTFhBZDFqOGdBeVlCVDVZRW4xTVNzZHFxYXRzSE1JSVg5a21tVnN1Rjg5MENnZ0VCQU1ReFNjUHhTRC8rc01DTwpmUkd3UjBXaU1LVFNJNUJMYm95VVVIUUFoOW5aaWhlcnA2Kytac0U3S0dqK29FeHliOVl6ZkE5cEtGZ09tM2RuCnV0UExNeTk1S01YVFgvSVZZSk1uR0xsenRhNDVyWXUxdDBQM2FTQm5HalppNVV5Q0FSTG9ieGxUakRGM05QY1gKWm9pN3hkRXl6anRuZjF2Uno1N2l5RTFlRTNXUEZIMm5TSVpSbURNeVNoTHFSWVd3Mll2bVJENW56U3RiU1d1ZApnZUEwL2hSSFgrRzlZMEhVSFZwcXlNQnRjZEErbnp6Y0ljWG4rNXFkWENhUVNNZ3o5QVc4UXYzQ2lmSmdqRzJOCkxBMVRyczRCclVxMU9HcElTQjl4d0pXcTZGdXloUkFuZXNneW12K2RPejErc3htcXdZSVZ5YUtGdEgyZnRyakwKQWZPMTZxRUNnZ0VBVWFSNHQ1SjRuc0VjWGo4ejUxK2JwQnF6a1M5WTZFdXpBSnpIU2IzUXBFVjZzU3NDS3hsRApVQXlDRlk0VndYT0pGbGl3L3Y0U053TW5lMUJUcm5neERYczhhcUc2UldWbS9pSENSUUgyTmxMdE9pNXFSMHk0ClI4R3g3b1dkUmJLNDNPdHBVcmxNSUx5VHVNMGQ3S0gzaWIzQm9xOEp5c0ZWSmRRQ3Eyb0NNcEQxN0p1alBlOTIKcGhFdE5tSEhVSHhMN0VuelllcHVZa2RXeHVKeEpiZTlNL29Yb2hra0VPQXFzOXNySGJyaTN1bjExdzdqbjM2VwpNTHRUUXdLSDFIMzUvcVhtU1R0MktjNGtPdzZMMmZ3eTZITFo2SGNuRlpwYVZnVU5DcEVPZmF3ZW5Ba1lXWUZmCkxXS3YvanVQUEg1d05jUS91eFpDVkZYRmFaTlhTeElvVlFLQ0FRRUF6SjdLNUFGYjc1eVkwSUY0SzF6dUJRTUQKbk5ENXN2RXYzd1hPdW5NckxpTWJpd2VVNVNKMkFVMnRkWHNXVmRnZHFqMWdCelU1ZURDWERvSFA0VDBSejQwNwpTQk0wMnZwL3MyNGZ1T0oxYWZMK0dsS0c3dXZUd3M1cWZHbWxVcGN0WGIvQ1U5bmlvYURBMVBjbDdNZEpKaTQ1CldzdmlxVE43SDEvL1o1MEVGbjZDczlGN2tlbUdyVlI2Z09IYjVydUJXSlplc3F6dE92amMxUTdsNjdsamxxZTIKWUpIeVZqdzQvd2tiWVp1dVZlYmc5eFQxd3BncUhqMXZhNlEvdDhkSTNiQWZIa1lhUHovSmJ5NmdLd0gwV3JJMApJeDF0V1FQREQxVU9oczhxdFFCZUZETjFKMjdGckR6SUFQYzRDcXU2Uzc5ZmxVa3RYd2cyY2Z5STBFRGNLUT09Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== 25 | 26 | SAM_PROJECT_NAME=Jump 27 | 28 | SAM_ADMIN_FIRSTNAME=Adrien 29 | SAM_ADMIN_LASTNAME=Chapelet 30 | SAM_ADMIN_PASSWORD=testPassword1; 31 | SAM_ADMIN_EMAIL=adrien3d@gmail.com 32 | SAM_ADMIN_PHONE=+33671174203 33 | 34 | SAM_USER1_FIRSTNAME=Kevin 35 | SAM_USER1_LASTNAME=Findus 36 | SAM_USER1_PASSWORD=testPassword1; 37 | SAM_USER1_EMAIL=kevin@findus.fr 38 | SAM_USER1_BALANCE=492.97 39 | 40 | SAM_USER2_FIRSTNAME=Laura 41 | SAM_USER2_LASTNAME=Biding 42 | SAM_USER2_PASSWORD=testPassword1; 43 | SAM_USER2_EMAIL=laura@biding.fr 44 | SAM_USER2_BALANCE=5200.60 45 | 46 | SAM_DEBUG=true 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lumen-api 2 | .idea/ 3 | base.rsa 4 | base.rsa.pub 5 | .env 6 | .env.prod 7 | .env.local 8 | .env.testing 9 | .env.server.* 10 | 11 | *.rsa 12 | 13 | # Elastic Beanstalk Files 14 | .elasticbeanstalk/* 15 | !.elasticbeanstalk/*.cfg.yml 16 | !.elasticbeanstalk/*.global.yml 17 | 18 | vendor/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - master 5 | 6 | services: 7 | - mongodb 8 | 9 | script: BASEAPI_ENV=testing go test ./... 10 | 11 | go_import_path: github.com/github.com/go-lumen/lumen-api 12 | 13 | notifications: 14 | email: false 15 | 16 | before_install: 17 | - openssl genrsa -out base.rsa 1024 18 | - openssl rsa -in base.rsa -pubout > base.rsa.pub 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | ENV SAM_ENV prod 4 | ENV GIN_MODE release 5 | ENV GO111MODULE on 6 | 7 | RUN mkdir -p /var/www/uploads 8 | 9 | WORKDIR /app 10 | COPY go.mod . 11 | COPY go.sum .é 12 | RUN go mod download 13 | COPY . . 14 | 15 | RUN go version 16 | RUN go build -o main 17 | 18 | CMD ["/app/main"] -------------------------------------------------------------------------------- /Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "AWSEBDockerrunVersion": "1", 4 | "Ports": [ 5 | { 6 | "ContainerPort": "4000" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lumen-api 2 | 3 | # To run 4 | export SAM_ENV=prod 5 | cp .env.sample .env.prod 6 | go run main.go 7 | 8 | # Improvements for generic 9 | 10 | - [ ] Generic store 11 | - [ ] Better helpers/error 12 | - [ ] Check PSql 13 | - [ ] gRPC 14 | - [ ] Implement KPI generation 15 | - [ ] Microservices (Kafka/SQS) -------------------------------------------------------------------------------- /cmd/genmigration/genmigration.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path" 8 | "strings" 9 | "time" 10 | 11 | "bytes" 12 | "github.com/gedex/inflector" 13 | "github.com/go-lumen/lumen-api/utils" 14 | "github.com/serenize/snaker" 15 | "go/format" 16 | "gopkg.in/godo.v2/util" 17 | "io/ioutil" 18 | "path/filepath" 19 | 20 | "text/template" 21 | 22 | "github.com/abiosoft/ishell" 23 | ) 24 | 25 | // FirstCharLower lowers first char of string 26 | func FirstCharLower(s string) string { 27 | ss := strings.Split(s, "") 28 | ss[0] = strings.ToLower(ss[0]) 29 | 30 | return strings.Join(ss, "") 31 | } 32 | 33 | // FirstCharUpper uppers first char of string 34 | func FirstCharUpper(s string) string { 35 | ss := strings.Split(s, "") 36 | ss[0] = strings.ToUpper(ss[0]) 37 | 38 | return strings.Join(ss, "") 39 | } 40 | 41 | // GetFirstChar returns the first char of string 42 | func GetFirstChar(s string) string { 43 | return s[0:1] 44 | } 45 | 46 | // FuncMap is a set of functions to use in templates 47 | var FuncMap = template.FuncMap{ 48 | "pluralize": inflector.Pluralize, 49 | "singularize": inflector.Singularize, 50 | "title": strings.Title, 51 | "firstLower": FirstCharLower, 52 | "toLower": strings.ToLower, 53 | "toSnakeCase": snaker.CamelToSnake, 54 | "firstChar": GetFirstChar, 55 | } 56 | 57 | func generateFile(outputPath string, data interface{}) { 58 | path := filepath.Join("migrations/template.tmpl") 59 | body, _ := ioutil.ReadFile(path) 60 | tmpl := template.Must(template.New("model").Option("missingkey=error").Funcs(FuncMap).Parse(string(body))) 61 | 62 | var buf bytes.Buffer 63 | err := tmpl.Execute(&buf, data) 64 | utils.CheckErr(err) 65 | 66 | src, _ := format.Source(buf.Bytes()) 67 | dstPath := filepath.Join(outputPath) 68 | 69 | if !util.FileExists(filepath.Dir(dstPath)) { 70 | if err := os.Mkdir(filepath.Dir(dstPath), 0644); err != nil { 71 | fmt.Println(err) 72 | } 73 | } 74 | if err := ioutil.WriteFile(dstPath, src, 0644); err != nil { 75 | fmt.Println(err) 76 | } 77 | } 78 | 79 | // SelectedModel is a structure that holds model name and selected methods 80 | type SelectedModel struct { 81 | Namespace string 82 | ModelName string 83 | Methods []string 84 | MigrationID string 85 | } 86 | 87 | // SelectModels asks user which models and methods to generate 88 | func SelectModels() (selectedModels []SelectedModel) { 89 | shell := ishell.New() 90 | 91 | shell.Println("Generating migrations") 92 | 93 | files, err := ioutil.ReadDir("models") 94 | if err != nil { 95 | fmt.Println(err) 96 | } 97 | 98 | var fileNames []string 99 | for _, file := range files { 100 | fileNames = append(fileNames, strings.Title(inflector.Singularize(strings.TrimRight(file.Name(), ".go")))) 101 | } 102 | 103 | // Step 1: Ask user which models to use 104 | choices := shell.Checklist(fileNames, 105 | "Please select models you want to generate the matching store:", 106 | nil) 107 | if len(choices) == 0 { 108 | shell.Println("Please choose at least one model (by pressing spacebar on each one you want to select)") 109 | return nil 110 | } 111 | 112 | for _, file := range choices { 113 | var selectedModel SelectedModel 114 | selectedModel.ModelName = FirstCharUpper(inflector.Pluralize(fileNames[file])) 115 | selectedModels = append(selectedModels, selectedModel) 116 | } 117 | 118 | return selectedModels 119 | } 120 | 121 | func main() { 122 | workingDirectory, err := os.Getwd() 123 | if err != nil { 124 | log.Fatal(err) 125 | } 126 | 127 | nowTimeString := time.Now().Format("200601021504") // current date 128 | 129 | selectedModels := SelectModels() 130 | 131 | for _, selectedModel := range selectedModels { 132 | fmt.Println(selectedModel) 133 | fileName := fmt.Sprintf("migrations/%s_%s.go", nowTimeString, strings.ToLower(selectedModel.ModelName)) 134 | longFileName := path.Join(workingDirectory, fileName) 135 | selectedModel.MigrationID = nowTimeString 136 | generateFile(longFileName, selectedModel) 137 | 138 | fmt.Printf("Successfully created migration %s\n", fileName) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // Conf type holds viper 10 | type Conf struct { 11 | *viper.Viper 12 | } 13 | 14 | // New allows to create a viper configuration 15 | func New(viper *viper.Viper) *Conf { 16 | return &Conf{viper} 17 | } 18 | 19 | // GetString allows to retrieve a specific string 20 | func GetString(c context.Context, key string) string { 21 | return FromContext(c).GetString(key) 22 | } 23 | 24 | // GetBool allows to retrieve a specific bool 25 | func GetBool(c context.Context, key string) bool { 26 | return FromContext(c).GetBool(key) 27 | } 28 | 29 | // GetInt allows to retrieve a specific int 30 | func GetInt(c context.Context, key string) int { 31 | return FromContext(c).GetInt(key) 32 | } 33 | 34 | // Set allows to set a value 35 | func Set(c context.Context, key string, value interface{}) { 36 | FromContext(c).Set(key, value) 37 | } 38 | -------------------------------------------------------------------------------- /config/context.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "context" 4 | 5 | // StoreKey is configuration storage key 6 | const ( 7 | StoreKey = "config" 8 | ) 9 | 10 | // Setter interface to set a string 11 | type Setter interface { 12 | Set(string, interface{}) 13 | } 14 | 15 | // FromContext to get value from context 16 | func FromContext(c context.Context) *Conf { 17 | return c.Value(StoreKey).(*Conf) 18 | } 19 | 20 | // ToContext to set value to context 21 | func ToContext(c Setter, conf *Conf) { 22 | c.Set(StoreKey, conf) 23 | } 24 | -------------------------------------------------------------------------------- /config/sigfoxApiKeys.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/awserr" 9 | "github.com/aws/aws-sdk-go/aws/credentials" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/secretsmanager" 12 | "github.com/sirupsen/logrus" 13 | "io/ioutil" 14 | ) 15 | 16 | // SigfoxAPIKey struct holds informations about Sigfox API Key 17 | type SigfoxAPIKey struct { 18 | Name string `json:"name"` 19 | DeviceTypeIDs []string `json:"deviceTypeIDs"` 20 | SigfoxIDs []string `json:"sigfoxIDs"` 21 | SigfoxKey string `json:"sigfoxKey"` 22 | SigfoxSecret string `json:"sigfoxSecret"` 23 | } 24 | 25 | // SigfoxAPIKeys struct is an array of SigfoxAPIKey 26 | type SigfoxAPIKeys struct { 27 | Keys []SigfoxAPIKey `json:"data"` 28 | } 29 | 30 | // RetrieveSigfoxAPIKey allows to retrieve a SigfoxAPIKey from a Sigfox Device ID 31 | func RetrieveSigfoxAPIKey(apiKeys SigfoxAPIKeys, sigfoxID, fleetSigfoxDeviceTypeID string) SigfoxAPIKey { 32 | var ret SigfoxAPIKey 33 | //apiKeys := ExtractSigfoxAPIKeyFromFile(GetString(c, "API_KEYS_FILENAME")) 34 | 35 | for _, key := range apiKeys.Keys { 36 | for _, sigID := range key.SigfoxIDs { 37 | if sigID == sigfoxID { 38 | return key 39 | } 40 | } 41 | for _, sfxDevTypID := range key.DeviceTypeIDs { 42 | if sfxDevTypID == fleetSigfoxDeviceTypeID { 43 | return key 44 | } 45 | } 46 | } 47 | 48 | return ret 49 | } 50 | 51 | // GetSigfoxAPIKeysFromAWS allows parsing secret file from AWS to retrieve SigfoxAPIKeys 52 | func GetSigfoxAPIKeysFromAWS(awsAPIId, awsAPIKey, fileName string) SigfoxAPIKeys { 53 | var data SigfoxAPIKeys 54 | 55 | svc := secretsmanager.New(session.New(), &aws.Config{Credentials: credentials.NewStaticCredentials(awsAPIId, awsAPIKey, ""), Region: aws.String("eu-west-3")}) 56 | input := &secretsmanager.GetSecretValueInput{ 57 | SecretId: aws.String(fileName), 58 | VersionStage: aws.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified 59 | } 60 | result, err := svc.GetSecretValue(input) 61 | if err != nil { 62 | logrus.Errorln("GetSecretValue Error:", err) 63 | } 64 | if err != nil { 65 | if aerr, ok := err.(awserr.Error); ok { 66 | switch aerr.Code() { 67 | case secretsmanager.ErrCodeDecryptionFailure: 68 | // Secrets Manager can't decrypt the protected secret text using the provided KMS key. 69 | fmt.Println(secretsmanager.ErrCodeDecryptionFailure, aerr.Error()) 70 | 71 | case secretsmanager.ErrCodeInternalServiceError: 72 | // An error occurred on the server side. 73 | fmt.Println(secretsmanager.ErrCodeInternalServiceError, aerr.Error()) 74 | 75 | case secretsmanager.ErrCodeInvalidParameterException: 76 | // You provided an invalid value for a parameter. 77 | fmt.Println(secretsmanager.ErrCodeInvalidParameterException, aerr.Error()) 78 | 79 | case secretsmanager.ErrCodeInvalidRequestException: 80 | // You provided a parameter value that is not valid for the current state of the resource. 81 | fmt.Println(secretsmanager.ErrCodeInvalidRequestException, aerr.Error()) 82 | 83 | case secretsmanager.ErrCodeResourceNotFoundException: 84 | // We can't find the resource that you asked for. 85 | fmt.Println(secretsmanager.ErrCodeResourceNotFoundException, aerr.Error()) 86 | } 87 | } else { 88 | fmt.Println(err.Error()) 89 | } 90 | } 91 | 92 | var secretString, decodedBinarySecret string 93 | if result.SecretString != nil { 94 | secretString = *result.SecretString 95 | err = json.Unmarshal([]byte(secretString), &data) 96 | if err != nil { 97 | logrus.Errorln(err) 98 | } 99 | } else { 100 | decodedBinarySecretBytes := make([]byte, base64.StdEncoding.DecodedLen(len(result.SecretBinary))) 101 | len, err := base64.StdEncoding.Decode(decodedBinarySecretBytes, result.SecretBinary) 102 | if err != nil { 103 | logrus.Errorln("Base64 Decode Error:", err) 104 | } 105 | decodedBinarySecret = string(decodedBinarySecretBytes[:len]) 106 | err = json.Unmarshal([]byte(decodedBinarySecret), &data) 107 | if err != nil { 108 | logrus.Errorln(err) 109 | } 110 | } 111 | 112 | return data 113 | } 114 | 115 | // ExtractSigfoxAPIKeyFromFile allows parsing JSON file to retrieve SigfoxAPIKeys 116 | func ExtractSigfoxAPIKeyFromFile(fileName string) SigfoxAPIKeys { 117 | var data SigfoxAPIKeys 118 | 119 | file, err := ioutil.ReadFile(fileName) 120 | if err != nil { 121 | fmt.Printf("File error: %v\n", err) 122 | } 123 | 124 | err = json.Unmarshal(file, &data) 125 | if err != nil { 126 | logrus.Errorln(err) 127 | panic(err) 128 | } 129 | 130 | return data 131 | } 132 | -------------------------------------------------------------------------------- /controllers/authentication.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-lumen/lumen-api/config" 7 | "github.com/go-lumen/lumen-api/helpers" 8 | "github.com/go-lumen/lumen-api/models" 9 | "github.com/go-lumen/lumen-api/store" 10 | "github.com/go-lumen/lumen-api/utils" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "golang.org/x/crypto/bcrypt" 13 | "net/http" 14 | "time" 15 | ) 16 | 17 | // AuthController structure 18 | type AuthController struct { 19 | BaseController 20 | } 21 | 22 | // NewAuthController instantiates the controller 23 | func NewAuthController() AuthController { 24 | return AuthController{} 25 | } 26 | 27 | func (ac AuthController) returnToken(c *gin.Context, encodedKey []byte, dbUser *models.User) { 28 | ctx := store.AuthContext(c) 29 | 30 | accessToken, err := helpers.GenerateToken(encodedKey, dbUser.ID, "access", 4320) //3d in min 31 | if err != nil { 32 | utils.CheckErr(err) 33 | ac.AbortWithError(c, helpers.ErrorTokenGenAccess(err)) 34 | return 35 | } 36 | refreshToken, err := helpers.GenerateToken(encodedKey, dbUser.ID, "refresh", 10080) //7d in min 37 | if err != nil { 38 | utils.CheckErr(err) 39 | ac.AbortWithError(c, helpers.ErrorTokenGenRefresh(err)) 40 | return 41 | } 42 | 43 | // Get group : orga & role 44 | var group models.Group 45 | var organization models.Organization 46 | if ac.Error(c, ctx.Store.Find(ctx, bson.M{"_id": dbUser.GroupID}, &group), helpers.ErrorResourceNotFound) { 47 | return 48 | } 49 | if ac.Error(c, ctx.Store.Find(ctx, bson.M{"_id": group.OrganizationID}, &organization), helpers.ErrorResourceNotFound) { 50 | return 51 | } 52 | 53 | err = ctx.Store.Update(ctx, store.ID(dbUser.ID), &models.User{LastLogin: time.Now().Unix()}, store.OnlyFields([]string{"last_login"})) 54 | if ac.ErrorInternal(c, err) { 55 | return 56 | } 57 | 58 | c.JSON(http.StatusOK, gin.H{"token": accessToken, "refresh_token": refreshToken, store.RoleUser: dbUser.Sanitize(group.Role, organization.ID, organization.Name)}) 59 | } 60 | 61 | // TokensGeneration to authenticate the user and generate a new token 62 | // @Summary Returns a token from username and password 63 | // @Produce json 64 | // @Param userAuth body models.UserAuth true "Query Params" 65 | // @Success 200 {object} models.User 66 | // @Router /v1/auth [post] 67 | // @Security ApiKeyAuth 68 | // @Tags Authentication 69 | func (ac AuthController) TokensGeneration(c *gin.Context) { 70 | ctx := store.AuthContext(c) 71 | userInput := models.User{} 72 | if err := c.Bind(&userInput); err != nil { 73 | ac.AbortWithError(c, helpers.ErrorInvalidInput(err)) 74 | return 75 | } 76 | 77 | var dbUser models.User 78 | if ac.Error(c, ctx.Store.Find(ctx, bson.M{"email": userInput.Email}, &dbUser), helpers.ErrorUserNotExist) { 79 | return 80 | } 81 | 82 | err := bcrypt.CompareHashAndPassword([]byte(dbUser.Password), []byte(userInput.Password)) 83 | if err != nil { 84 | ac.AbortWithError(c, helpers.ErrorUserWrongPassword(err)) 85 | utils.Log(c, "info", "CompareHashAndPassword err:", err) 86 | return 87 | } 88 | /*if dbUser.Password != userInput.Password { 89 | utils.Log(c, "info", "CompareHashAndPassword er") 90 | return 91 | }*/ 92 | 93 | if dbUser.Status == "created" { 94 | ac.AbortWithError(c, helpers.ErrorUserNotActivated(nil)) 95 | return 96 | } 97 | 98 | //Read base64 private key 99 | encodedKey := []byte(config.GetString(c, "rsa_private")) 100 | ac.returnToken(c, encodedKey, &dbUser) 101 | } 102 | 103 | // TokenRenewal to renew a refresh token 104 | func (ac AuthController) TokenRenewal(c *gin.Context) { 105 | ctx := store.AuthContext(c) 106 | 107 | type UserInput struct { 108 | RefreshToken string `json:"refresh_token"` 109 | } 110 | var input UserInput 111 | if err := c.ShouldBind(&input); err != nil { 112 | ac.AbortWithError(c, helpers.ErrorInvalidInput(err)) 113 | return 114 | } 115 | encodedKey := []byte(config.GetString(c, "rsa_private")) 116 | refreshTokenClaims, err := helpers.ValidateJwtToken(input.RefreshToken, encodedKey, "refresh") 117 | if err != nil { 118 | ac.AbortWithError(c, helpers.ErrorTokenRefreshInvalid(err)) 119 | return 120 | } 121 | 122 | var dbUser models.User 123 | if ac.Error(c, ctx.Store.Find(ctx, bson.M{"_id": fmt.Sprintf("%v", refreshTokenClaims["sub"])}, &dbUser), helpers.ErrorUserNotExist) { 124 | return 125 | } 126 | 127 | ac.returnToken(c, encodedKey, &dbUser) 128 | } 129 | -------------------------------------------------------------------------------- /controllers/base.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-lumen/lumen-api/helpers" 7 | "github.com/go-lumen/lumen-api/models" 8 | "github.com/go-lumen/lumen-api/store" 9 | "github.com/sirupsen/logrus" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "net/http" 12 | "reflect" 13 | ) 14 | 15 | // BaseController hold common controller actions 16 | type BaseController struct{} 17 | 18 | // AbortWithError abort current request with a standardized output 19 | func (BaseController) AbortWithError(c *gin.Context, apiError helpers.Error) { 20 | _ = c.AbortWithError(apiError.HTTPCode, apiError) 21 | logrus.WithError(apiError.Trace).WithField("path", c.FullPath()).Debugln("request aborted") 22 | } 23 | 24 | // ErrorInternal is a shortcut to test and abort if there if an internal error. 25 | // Returns true if the caller should return. 26 | func (bc BaseController) ErrorInternal(c *gin.Context, err error) bool { 27 | if err != nil { 28 | bc.AbortWithError(c, helpers.ErrorInternal(err)) 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | // Error is a shortcut to test and abort if there if an error. 35 | // Param message is optional and override error message. 36 | // Returns true if the caller should return. 37 | func (bc BaseController) Error(c *gin.Context, err error, apiError helpers.NoCtxError, message ...string) bool { 38 | if err != nil { 39 | renderedError := apiError(err) 40 | if len(message) == 1 { 41 | renderedError.Message = message[0] 42 | } 43 | 44 | bc.AbortWithError(c, renderedError) 45 | return true 46 | } 47 | return false 48 | } 49 | 50 | // BindJSONError is a shortcut for BindJSON and AbortWithError in case of error. 51 | // Returns true if the caller should return. 52 | func (bc BaseController) BindJSONError(c *gin.Context, obj interface{}) bool { 53 | if err := c.BindJSON(obj); err != nil { 54 | bc.AbortWithError(c, helpers.ErrorInvalidInput(err)) 55 | return true 56 | } 57 | return false 58 | } 59 | 60 | // LoggedUser returns logged user, group and a bool indicating is someone is logged in. 61 | // If no used authenticated, current request is aborted with ErrorUserUnauthorized. 62 | func (bc BaseController) LoggedUser(c *gin.Context) (store.User, store.Group, bool) { 63 | storedUser, userExists := c.Get(store.CurrentUserKey) 64 | if !userExists || storedUser == nil { 65 | bc.AbortWithError(c, helpers.ErrorUserUnauthorized) 66 | return nil, nil, false 67 | } 68 | user := storedUser.(*models.User) 69 | 70 | storedUserGroup, userGroupExists := c.Get(store.CurrentUserGroupKey) 71 | if !userGroupExists || storedUserGroup == nil { 72 | bc.AbortWithError(c, helpers.ErrorUserUnauthorized) 73 | return user, nil, false 74 | } 75 | return user, storedUserGroup.(*models.Group), true 76 | } 77 | 78 | // ParamID is a shortcut for `bson.M{"_id": c.Param("id")}` 79 | func (bc BaseController) ParamID(c *gin.Context) bson.M { 80 | return bson.M{"_id": c.Param("id")} 81 | } 82 | 83 | // ShouldBeLogged is an helper method to check if the IsLogged flag and send an http error code accordingly 84 | func (bc BaseController) ShouldBeLogged(ctx *store.Context) bool { 85 | if !ctx.IsLogged { 86 | bc.AbortWithError(ctx.C, helpers.ErrorUserUnauthorized) 87 | return false 88 | } 89 | return true 90 | } 91 | 92 | func sliceToBaseModelSlice(slice interface{}) []store.Model { 93 | s := reflect.ValueOf(slice) 94 | ret := make([]store.Model, s.Len()) 95 | for i := 0; i < s.Len(); i++ { 96 | ret[i] = s.Index(i).Interface().(store.Model) 97 | } 98 | return ret 99 | } 100 | 101 | // CRUDController implements generic CRUD actions 102 | type CRUDController struct { 103 | BaseController 104 | modelType reflect.Type 105 | } 106 | 107 | // GetModel implements generic find one by ID 108 | func (cc CRUDController) GetModel(c *gin.Context) { 109 | ctx := store.AuthContext(c) 110 | if !cc.ShouldBeLogged(ctx) { 111 | return 112 | } 113 | 114 | // create an empty model 115 | model := reflect.New(cc.modelType).Interface().(store.Model) 116 | if cc.Error(c, ctx.Store.Find(ctx, cc.ParamID(c), model), helpers.ErrorResourceNotFound) { 117 | return 118 | } 119 | 120 | /*if user, group, ok := cc.LoggedUser(c); ok { 121 | if model.CanBeRead(user, group) { 122 | c.JSON(http.StatusOK, model) 123 | return 124 | } 125 | 126 | cc.AbortWithError(c, helpers.ErrorUserUnauthorized) 127 | }*/ 128 | } 129 | 130 | func (cc CRUDController) makeSlice() reflect.Value { 131 | slice := reflect.MakeSlice(reflect.SliceOf(reflect.PtrTo(cc.modelType)), 0, 0) 132 | results := reflect.New(slice.Type()) 133 | results.Elem().Set(slice) 134 | return results 135 | } 136 | 137 | // fetchModels returns a list of models with query filters 138 | // Automatically set errors in gin context. 139 | func (cc CRUDController) fetchModels(c *gin.Context) ([]store.Model, error) { 140 | ctx := store.AuthContext(c) 141 | if !cc.ShouldBeLogged(ctx) { 142 | return nil, errors.New("should be logged") 143 | } 144 | 145 | results := cc.makeSlice().Elem() 146 | filters := bson.M{} // TODO: generic way to get filter from query args ? 147 | if cc.Error(c, ctx.Store.FindAll(ctx, filters, results.Addr().Interface()), helpers.ErrorResourceNotFound) { 148 | return nil, errors.New("not found") 149 | } 150 | //resTyped := sliceToBaseModelSlice(results.Interface()) 151 | readableModels := make([]store.Model, 0) 152 | /*if user, group, ok := cc.LoggedUser(c); ok { 153 | for _, m := range resTyped { 154 | if m.CanBeRead(user, group) { 155 | readableModels = append(readableModels, m) 156 | } 157 | } 158 | }*/ 159 | return readableModels, nil 160 | } 161 | 162 | // GetModels implements generic find several with filters 163 | func (cc CRUDController) GetModels(c *gin.Context) { 164 | results, err := cc.fetchModels(c) 165 | if err != nil { 166 | return 167 | } 168 | c.JSON(http.StatusOK, results) 169 | } 170 | 171 | // CreateModel implements generic model creation 172 | func (cc CRUDController) CreateModel(c *gin.Context) { 173 | ctx := store.AuthContext(c) 174 | if !cc.ShouldBeLogged(ctx) { 175 | return 176 | } 177 | 178 | model := reflect.New(cc.modelType).Interface().(store.Model) 179 | if cc.BindJSONError(c, model) { 180 | return 181 | } 182 | 183 | /*if user, group, ok := cc.LoggedUser(c); ok && model.CanBeCreated(user, group) { 184 | if cc.ErrorInternal(c, ctx.Store.Create(ctx, model)) { 185 | return 186 | } 187 | c.JSON(http.StatusCreated, model) 188 | } else { 189 | cc.AbortWithError(c, helpers.ErrorUserUnauthorized) 190 | }*/ 191 | } 192 | 193 | // DeleteModel implements generic delete one by ID 194 | func (cc CRUDController) DeleteModel(c *gin.Context) { 195 | ctx := store.AuthContext(c) 196 | if !cc.ShouldBeLogged(ctx) { 197 | return 198 | } 199 | 200 | model := reflect.New(cc.modelType).Interface().(store.Model) 201 | if cc.Error(c, ctx.Store.Find(ctx, cc.ParamID(c), model), helpers.ErrorResourceNotFound) { 202 | return 203 | } 204 | 205 | /*if user, group, ok := cc.LoggedUser(c); ok { 206 | if model.CanBeDeleted(user, group) { 207 | if cc.ErrorInternal(c, ctx.Store.Delete(ctx, c.Param("id"), model)) { 208 | return 209 | } 210 | c.JSON(http.StatusOK, model) 211 | return 212 | } 213 | 214 | cc.AbortWithError(c, helpers.ErrorUserUnauthorized) 215 | }*/ 216 | } 217 | 218 | // UpdateModel implements generic update one 219 | func (cc CRUDController) UpdateModel(c *gin.Context) { 220 | ctx := store.AuthContext(c) 221 | if !cc.ShouldBeLogged(ctx) { 222 | return 223 | } 224 | 225 | model := reflect.New(cc.modelType).Interface().(store.Model) 226 | if cc.Error(c, ctx.Store.Find(ctx, cc.ParamID(c), model), helpers.ErrorResourceNotFound) { 227 | return 228 | } 229 | 230 | newModel := reflect.New(cc.modelType).Interface().(store.Model) 231 | if cc.BindJSONError(c, newModel) { 232 | return 233 | } 234 | 235 | /*if user, group, ok := cc.LoggedUser(c); ok { 236 | if model.CanBeUpdated(user, group) { 237 | if cc.ErrorInternal(c, ctx.Store.Update(ctx, store.ID(c.Param("id")), newModel)) { 238 | return 239 | } 240 | c.JSON(http.StatusOK, newModel) 241 | return 242 | } 243 | 244 | cc.AbortWithError(c, helpers.ErrorUserUnauthorized) 245 | }*/ 246 | } 247 | -------------------------------------------------------------------------------- /controllers/export.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "github.com/gin-gonic/gin" 7 | "github.com/go-lumen/lumen-api/config" 8 | "github.com/go-lumen/lumen-api/helpers" 9 | "github.com/go-lumen/lumen-api/models" 10 | "github.com/go-lumen/lumen-api/store" 11 | "github.com/go-lumen/lumen-api/utils" 12 | "github.com/tealeg/xlsx/v3" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "net/http" 15 | "time" 16 | ) 17 | 18 | // ExportController holds all controller functions related to the export 19 | type ExportController struct { 20 | BaseController 21 | } 22 | 23 | // NewExportController instantiates the controller 24 | func NewExportController() ExportController { 25 | return ExportController{} 26 | } 27 | 28 | func addExcelHeader(cellsContent []string, r *xlsx.Row) { 29 | var hStyle = xlsx.NewStyle() 30 | font := *xlsx.NewFont(10, "Arial") 31 | hStyle.Font.Bold = true 32 | hStyle.Font = font 33 | fill := *xlsx.NewFill("solid", "00C8D9EE", "FF000000") 34 | hStyle.Fill = fill 35 | border := *xlsx.NewBorder("thin", "thin", "thin", "thin") 36 | hStyle.Border = border 37 | hStyle.ApplyBorder = true 38 | hStyle.ApplyFill = true 39 | for _, item := range cellsContent { 40 | cell := r.AddCell() 41 | cell.SetStyle(hStyle) 42 | cell.Value = item 43 | } 44 | r.SetHeight(20) 45 | } 46 | 47 | // ExportDeviceMessages allows to export device messages 48 | func (ec ExportController) ExportDeviceMessages(c *gin.Context) { 49 | ctx := store.AuthContext(c) 50 | var queryParams models.QueryParams 51 | if err := c.ShouldBind(&queryParams); err == nil { 52 | if queryParams.Limit == 0 { 53 | queryParams.Limit = 2000 54 | } 55 | if queryParams.Order == 0 { 56 | queryParams.Order = -1 57 | } 58 | } else { 59 | ec.AbortWithError(c, helpers.ErrorInvalidInput(err)) 60 | return 61 | } 62 | if queryParams.StartTime < 0 { 63 | queryParams.StartTime = 0 64 | } 65 | if queryParams.EndTime < 100000 { 66 | queryParams.EndTime = 2000000000 67 | } 68 | 69 | encodedKey := []byte(config.GetString(c, "rsa_private")) 70 | claims, err := helpers.ValidateJwtToken(c.Param("token"), encodedKey, "access") 71 | if err != nil { 72 | ec.AbortWithError(c, helpers.ErrorInvalidToken(err)) 73 | return 74 | } 75 | 76 | user, _ := models.GetUser(ctx, bson.M{"_id": claims["sub"].(string)}) 77 | 78 | group, err := models.GetGroup(ctx, bson.M{"_id": user.GroupID}) 79 | if err != nil { 80 | ec.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 81 | return 82 | } 83 | if group.Role == store.RoleCustomer { 84 | ec.AbortWithError(c, helpers.ErrorUserUnauthorized) 85 | return 86 | } 87 | 88 | layout := "-messages-export-20060102-150405.csv" 89 | fileName := time.Now().UTC().Format(layout) 90 | b := &bytes.Buffer{} // creates IO Writer 91 | wr := csv.NewWriter(b) // creates a csv writer that uses the io buffer. 92 | 93 | var header []string 94 | header = append(header, "User name", "User Email", "Group role", "Group name") 95 | wr.Write(header) 96 | //for _, user := range dbUsers { 97 | var line []string 98 | line = append(line, user.FirstName+" "+user.LastName) 99 | line = append(line, user.Email) 100 | line = append(line, group.Role) 101 | line = append(line, group.Name) 102 | wr.Write(line) 103 | utils.CheckErr(err) 104 | //} 105 | wr.Flush() // writes the csv writer data to the buffered data io writer(b(bytes.buffer)) 106 | 107 | c.Header("Content-Description", "File Transfer") 108 | c.Header("Content-Type", "text/csv") 109 | c.Header("Content-Disposition", "attachment; filename="+fileName) 110 | c.Data(http.StatusOK, "application/octet-stream", b.Bytes()) 111 | } 112 | -------------------------------------------------------------------------------- /controllers/group.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/helpers" 6 | "github.com/go-lumen/lumen-api/models" 7 | "github.com/go-lumen/lumen-api/store" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "net/http" 10 | "sort" 11 | ) 12 | 13 | // GroupController holds all controller functions related to the group entity 14 | type GroupController struct { 15 | BaseController 16 | } 17 | 18 | // NewGroupController instantiates the controller 19 | func NewGroupController() GroupController { 20 | return GroupController{} 21 | } 22 | 23 | // CreateGroup to create a new group 24 | func (gc GroupController) CreateGroup(c *gin.Context) { 25 | ctx := store.AuthContext(c) 26 | group := &models.Group{} 27 | 28 | if err := c.BindJSON(group); err != nil { 29 | gc.AbortWithError(c, helpers.ErrorInvalidInput(err)) 30 | return 31 | } 32 | 33 | if len(group.OrganizationID) < 20 { 34 | organization, err := models.GetOrganization(ctx, bson.M{"index": group.OrganizationID}) 35 | if err == nil { 36 | group.OrganizationID = organization.ID 37 | } 38 | } 39 | 40 | if _, userGroup, ok := gc.LoggedUser(c); ok { 41 | switch userGroup.GetRole() { 42 | case store.RoleGod: 43 | if err := models.CreateGroup(ctx, group); err != nil { 44 | gc.AbortWithError(c, helpers.ErrorInternal(err)) 45 | return 46 | } 47 | c.JSON(http.StatusCreated, group) 48 | case store.RoleAdmin, store.RoleUser, store.RoleCustomer: 49 | gc.AbortWithError(c, helpers.ErrorUserUnauthorized) 50 | } 51 | } 52 | } 53 | 54 | // GetGroups to get all groups 55 | func (gc GroupController) GetGroups(c *gin.Context) { 56 | ctx := store.AuthContext(c) 57 | dbGroups, err := models.GetGroups(ctx, bson.M{}) 58 | if gc.ErrorInternal(c, err) { 59 | return 60 | } 61 | 62 | dbOrgas, err := models.GetOrganizations(ctx, bson.M{}) 63 | if gc.ErrorInternal(c, err) { 64 | return 65 | } 66 | 67 | if _, userGroup, ok := gc.LoggedUser(c); ok { 68 | switch userGroup.GetRole() { 69 | case store.RoleGod: // Get all 70 | for _, group := range dbGroups { 71 | orga, err := models.FindOrganization(dbOrgas, group.OrganizationID) 72 | if err == nil { 73 | group.OrganizationID = orga.Name 74 | } 75 | } 76 | sort.Slice(dbGroups, func(i, j int) bool { return dbGroups[i].Name < dbGroups[j].Name }) 77 | c.JSON(http.StatusOK, dbGroups) 78 | case store.RoleAdmin, store.RoleUser: // Get all from devices with same group ID (and group for admin) 79 | var retGroups []*models.Group 80 | for _, group := range dbGroups { 81 | if group.OrganizationID == userGroup.GetOrgID() { 82 | orga, err := models.FindOrganization(dbOrgas, group.OrganizationID) 83 | if err == nil { 84 | group.OrganizationID = orga.Name 85 | } 86 | retGroups = append(retGroups, group) 87 | } 88 | } 89 | sort.Slice(retGroups, func(i, j int) bool { return retGroups[i].Name < retGroups[j].Name }) 90 | c.JSON(http.StatusOK, retGroups) 91 | case store.RoleCustomer: 92 | gc.AbortWithError(c, helpers.ErrorUserUnauthorized) 93 | } 94 | } 95 | } 96 | 97 | // GetUserGroup to get group from user stored in context 98 | func (gc GroupController) GetUserGroup(c *gin.Context) { 99 | if _, group, ok := gc.LoggedUser(c); ok { 100 | c.JSON(http.StatusOK, group) 101 | } 102 | } 103 | 104 | // GetGroup allows to get a specific Group 105 | func (gc GroupController) GetGroup(c *gin.Context) { 106 | ctx := store.AuthContext(c) 107 | group, err := models.GetGroup(ctx, gc.ParamID(c)) 108 | if gc.Error(c, err, helpers.ErrorResourceNotFound) { 109 | return 110 | } 111 | 112 | if _, userGroup, ok := gc.LoggedUser(c); ok { 113 | switch userGroup.GetRole() { 114 | case store.RoleGod: 115 | c.JSON(http.StatusOK, group) 116 | case store.RoleAdmin, store.RoleUser: 117 | if group.OrganizationID == userGroup.GetOrgID() { 118 | c.JSON(http.StatusOK, group) 119 | return 120 | } 121 | gc.AbortWithError(c, helpers.ErrorUserUnauthorized) 122 | case store.RoleCustomer: 123 | gc.AbortWithError(c, helpers.ErrorUserUnauthorized) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /controllers/organization.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/helpers" 6 | "github.com/go-lumen/lumen-api/models" 7 | "github.com/go-lumen/lumen-api/store" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "net/http" 10 | ) 11 | 12 | // OrganizationController holds all controller functions related to the organization entity 13 | type OrganizationController struct { 14 | BaseController 15 | } 16 | 17 | // NewOrganizationController instantiates the controller 18 | func NewOrganizationController() OrganizationController { 19 | return OrganizationController{} 20 | } 21 | 22 | // CreateOrganization to create a new organization 23 | func (oc OrganizationController) CreateOrganization(c *gin.Context) { 24 | ctx := store.AuthContext(c) 25 | organization := &models.Organization{} 26 | 27 | if oc.BindJSONError(c, organization) { 28 | return 29 | } 30 | 31 | if _, group, ok := oc.LoggedUser(c); ok { 32 | switch group.GetRole() { 33 | case store.RoleGod: 34 | if oc.ErrorInternal(c, models.CreateOrganization(ctx, organization)) { 35 | return 36 | } 37 | c.JSON(http.StatusCreated, organization) 38 | case store.RoleAdmin, store.RoleUser, store.RoleCustomer: 39 | oc.AbortWithError(c, helpers.ErrorUserUnauthorized) 40 | } 41 | } 42 | } 43 | 44 | // GetOrganizations to get all organizations 45 | func (oc OrganizationController) GetOrganizations(c *gin.Context) { 46 | ctx := store.AuthContext(c) 47 | dbOrganizations, err := models.GetOrganizations(ctx, bson.M{}) 48 | if oc.ErrorInternal(c, err) { 49 | return 50 | } 51 | 52 | if _, group, ok := oc.LoggedUser(c); ok { 53 | switch group.GetRole() { 54 | case store.RoleGod: // Get all 55 | c.JSON(http.StatusOK, dbOrganizations) 56 | case store.RoleAdmin, store.RoleUser: // Get all from devices with same group ID (and organization for admin) 57 | userOrganization, err := models.GetOrganization(ctx, bson.M{"_id": group.GetOrgID()}) 58 | if oc.Error(c, err, helpers.ErrorResourceNotFound) { 59 | return 60 | } 61 | c.JSON(http.StatusOK, []*models.Organization{userOrganization}) 62 | case store.RoleCustomer: 63 | oc.AbortWithError(c, helpers.ErrorUserUnauthorized) 64 | } 65 | } 66 | } 67 | 68 | // GetOrganization allows to get a specific Organization 69 | func (oc OrganizationController) GetOrganization(c *gin.Context) { 70 | ctx := store.AuthContext(c) 71 | organization, err := models.GetOrganization(ctx, oc.ParamID(c)) 72 | if oc.Error(c, err, helpers.ErrorResourceNotFound) { 73 | return 74 | } 75 | 76 | if _, group, ok := oc.LoggedUser(c); ok { 77 | switch group.GetRole() { 78 | case store.RoleGod: 79 | c.JSON(http.StatusOK, organization) 80 | case store.RoleAdmin, store.RoleUser: 81 | if organization.ID == group.GetOrgID() { 82 | c.JSON(http.StatusOK, organization) 83 | return 84 | } 85 | oc.AbortWithError(c, helpers.ErrorUserUnauthorized) 86 | case store.RoleCustomer: 87 | oc.AbortWithError(c, helpers.ErrorUserUnauthorized) 88 | } 89 | } 90 | } 91 | 92 | // GetOrganizationGroups allows to get groups from an organization 93 | func (oc OrganizationController) GetOrganizationGroups(c *gin.Context) { 94 | ctx := store.AuthContext(c) 95 | organization, err := models.GetOrganization(ctx, oc.ParamID(c)) 96 | if oc.Error(c, err, helpers.ErrorResourceNotFound) { 97 | return 98 | } 99 | 100 | groups, err := models.GetGroups(ctx, bson.M{"organization_id": c.Param("id")}) 101 | if oc.Error(c, err, helpers.ErrorResourceNotFound) { 102 | return 103 | } 104 | 105 | if _, group, ok := oc.LoggedUser(c); ok { 106 | switch group.GetRole() { 107 | case store.RoleGod: 108 | c.JSON(http.StatusOK, groups) 109 | case store.RoleAdmin, store.RoleUser: 110 | if organization.ID == group.GetOrgID() { 111 | c.JSON(http.StatusOK, groups) 112 | return 113 | } 114 | oc.AbortWithError(c, helpers.ErrorUserUnauthorized) 115 | case store.RoleCustomer: 116 | oc.AbortWithError(c, helpers.ErrorUserUnauthorized) 117 | } 118 | } 119 | } 120 | 121 | // GetUserOrganization to get organization from user stored in context 122 | func (oc OrganizationController) GetUserOrganization(c *gin.Context) { 123 | ctx := store.AuthContext(c) 124 | if _, group, ok := oc.LoggedUser(c); ok { 125 | userOrganization, err := models.GetOrganization(ctx, bson.M{"_id": group.GetOrgID()}) 126 | if oc.Error(c, err, helpers.ErrorResourceNotFound) { 127 | return 128 | } 129 | 130 | switch group.GetRole() { 131 | case store.RoleGod: // Get all 132 | c.JSON(http.StatusOK, userOrganization) 133 | case store.RoleAdmin, store.RoleUser: // Get all from devices with same group ID (and organization for admin) 134 | c.JSON(http.StatusOK, userOrganization.Sanitize()) 135 | case store.RoleCustomer: 136 | c.JSON(http.StatusOK, userOrganization.Sanitize()) 137 | } 138 | } 139 | } 140 | 141 | // GetOrganizationByAppKey to get organization from AppKey 142 | func (oc OrganizationController) GetOrganizationByAppKey(c *gin.Context) { 143 | ctx := store.AuthContext(c) 144 | var org models.Organization 145 | if oc.Error(c, ctx.Store.Find(ctx, models.OrgByAppKey(c.Param("id")), &org), helpers.ErrorResourceNotFound) { 146 | return 147 | } 148 | 149 | c.JSON(http.StatusOK, org) 150 | } 151 | -------------------------------------------------------------------------------- /controllers/user.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/dgrijalva/jwt-go" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-lumen/lumen-api/config" 7 | "github.com/go-lumen/lumen-api/helpers" 8 | "github.com/go-lumen/lumen-api/models" 9 | "github.com/go-lumen/lumen-api/services" 10 | "github.com/go-lumen/lumen-api/store" 11 | "github.com/go-lumen/lumen-api/utils" 12 | "github.com/sirupsen/logrus" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "golang.org/x/crypto/bcrypt" 15 | "net/http" 16 | "time" 17 | ) 18 | 19 | // UserController holds all controller functions related to the user entity 20 | type UserController struct { 21 | BaseController 22 | } 23 | 24 | // NewUserController instantiates of the controller 25 | func NewUserController() UserController { 26 | return UserController{} 27 | } 28 | 29 | // GetUser allows to retrieve a user from id (in context) 30 | // @Summary Retrieves user based on given ID 31 | // @Produce json 32 | // @Param id path string true "User ID" 33 | // @Success 200 {object} models.User 34 | // @Router /v1/users/{id} [get] 35 | // @Security ApiKeyAuth 36 | // @Tags User 37 | func (uc UserController) GetUser(c *gin.Context) { 38 | ctx := store.AuthContext(c) 39 | if loggedUser, loggedGroup, ok := uc.LoggedUser(c); ok { 40 | switch loggedGroup.GetRole() { 41 | case store.RoleGod: 42 | user, err := models.GetUser(ctx, uc.ParamID(c)) 43 | 44 | if err != nil { 45 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 46 | return 47 | } 48 | c.JSON(http.StatusOK, user) 49 | case store.RoleAdmin, store.RoleUser: 50 | user, err := models.GetUser(ctx, uc.ParamID(c)) 51 | if user.GroupID == loggedUser.GetGroupID() { 52 | c.JSON(http.StatusOK, user) 53 | return 54 | } 55 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 56 | return 57 | 58 | case store.RoleCustomer: 59 | uc.AbortWithError(c, helpers.ErrorUserUnauthorized) 60 | return 61 | } 62 | } 63 | } 64 | 65 | // GetUserMe from id (in request) 66 | func (uc UserController) GetUserMe(c *gin.Context) { 67 | ctx := store.AuthContext(c) 68 | if user, group, ok := uc.LoggedUser(c); ok { 69 | organization, err := models.GetOrganization(ctx, bson.M{"_id": group.GetOrgID()}) 70 | if err != nil { 71 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 72 | return 73 | } 74 | 75 | var dbUSer models.User 76 | if uc.ErrorInternal(c, ctx.Store.Find(ctx, store.ID(user.GetID()), &dbUSer)) { 77 | return 78 | } 79 | 80 | c.JSON(http.StatusOK, dbUSer.Sanitize(group.GetRole(), organization.ID, organization.Name)) 81 | } else { 82 | uc.AbortWithError(c, helpers.ErrorUserUnauthorized) 83 | } 84 | } 85 | 86 | // ChangeUserGroup from id (in request) 87 | func (uc UserController) ChangeUserGroup(c *gin.Context) { 88 | ctx := store.AuthContext(c) 89 | if !uc.ShouldBeLogged(ctx) { 90 | return 91 | } 92 | desiredGroupID := c.Param("groupID") 93 | 94 | userOrganization, err := models.GetOrganization(ctx, bson.M{"_id": ctx.Group.GetOrgID()}) 95 | if err != nil { 96 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 97 | return 98 | } 99 | desiredGroup, err := models.GetGroup(ctx, bson.M{"_id": desiredGroupID}) 100 | if err != nil { 101 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 102 | return 103 | } 104 | 105 | var dbUser models.User 106 | if uc.ErrorInternal(c, ctx.Store.Find(ctx, store.ID(ctx.User.GetID()), &dbUser)) { 107 | return 108 | } 109 | 110 | switch ctx.Role { 111 | case store.RoleGod: 112 | dbUser.GroupID = desiredGroupID 113 | err := models.UpdateUser(ctx, dbUser.ID, &dbUser) 114 | if err != nil { 115 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 116 | return 117 | } 118 | c.JSON(http.StatusOK, dbUser) 119 | case store.RoleAdmin, store.RoleUser: 120 | if desiredGroup.OrganizationID == userOrganization.ID { 121 | dbUser.GroupID = desiredGroupID 122 | err := models.UpdateUser(ctx, dbUser.ID, &dbUser) 123 | if err != nil { 124 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 125 | return 126 | } 127 | c.JSON(http.StatusOK, dbUser) 128 | } else { 129 | uc.AbortWithError(c, helpers.ErrorUserUnauthorized) 130 | } 131 | case store.RoleCustomer: 132 | uc.AbortWithError(c, helpers.ErrorUserUnauthorized) 133 | } 134 | } 135 | 136 | // GetUserDetails from id (in request) 137 | func (uc UserController) GetUserDetails(c *gin.Context) { 138 | ctx := store.AuthContext(c) 139 | if !uc.ShouldBeLogged(ctx) { 140 | return 141 | } 142 | 143 | organization, err := models.GetOrganization(ctx, bson.M{"_id": ctx.Group.GetOrgID()}) 144 | if err != nil { 145 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 146 | return 147 | } 148 | 149 | var dbUser models.User 150 | if uc.ErrorInternal(c, ctx.Store.Find(ctx, store.ID(ctx.User.GetID()), &dbUser)) { 151 | return 152 | } 153 | 154 | c.JSON(http.StatusOK, dbUser.Detail(ctx.Role, organization.Name)) 155 | } 156 | 157 | // CreateUser to create a new user 158 | func (uc UserController) CreateUser(c *gin.Context) { 159 | ctx := store.AuthContext(c) 160 | user := &models.User{} 161 | 162 | if err := c.BindJSON(user); err != nil { 163 | uc.AbortWithError(c, helpers.ErrorInvalidInput(err)) 164 | return 165 | } 166 | 167 | userGroup, err := models.GetGroup(ctx, bson.M{"_id": user.GroupID}) 168 | if err != nil { 169 | uc.AbortWithError(c, helpers.ErrorInvalidInput(err)) 170 | return 171 | } 172 | 173 | organization, err := models.GetOrganization(ctx, bson.M{"_id": userGroup.OrganizationID}) 174 | if err != nil { 175 | uc.AbortWithError(c, helpers.ErrorInvalidInput(err)) 176 | return 177 | } 178 | 179 | user.LastModification = time.Now().Unix() 180 | err = models.CreateUser(ctx, user) 181 | if err != nil { 182 | uc.AbortWithError(c, helpers.ErrorInvalidInput(err)) 183 | return 184 | } 185 | 186 | dbUser, err := models.GetUser(ctx, bson.M{"email": user.Email}) 187 | utils.CheckErr(err) 188 | 189 | encodedKey := []byte(config.GetString(c, "rsa_private")) 190 | privateKey, err := helpers.GetRSAPrivateKey(encodedKey) 191 | utils.CheckErr(err) 192 | 193 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ 194 | "sub": "define", 195 | "userId": dbUser.ID, 196 | "userKey": dbUser.Key, 197 | "userEmail": dbUser.Email, 198 | "userFirstname": dbUser.FirstName, 199 | "userLastname": dbUser.LastName, 200 | }) 201 | tokenStr, err := token.SignedString(privateKey) 202 | utils.CheckErr(err) 203 | 204 | apiURL := config.GetString(c, "http_scheme") + `://` + config.GetString(c, "front_url") + `/update-account/` + tokenStr 205 | frontURL := config.GetString(c, "front_url") 206 | appName := config.GetString(c, "mail_sender_name") 207 | 208 | s := services.GetEmailSender(c) 209 | 210 | err = s.SendActivationEmail(dbUser, apiURL, appName, frontURL) 211 | if err != nil { 212 | logrus.Infoln(err) 213 | c.AbortWithError(http.StatusUnauthorized, helpers.ErrorWithCode("mail_sending_error", "ErrorInternal when sending mail", err)) 214 | return 215 | } 216 | 217 | c.JSON(http.StatusCreated, user.Sanitize(userGroup.Role, organization.ID, organization.Name)) 218 | } 219 | 220 | // DeleteUser to delete an existing user 221 | func (uc UserController) DeleteUser(c *gin.Context) { 222 | ctx := store.AuthContext(c) 223 | err := models.DeleteUser(ctx, c.Param("id")) 224 | 225 | if err != nil { 226 | uc.AbortWithError(c, helpers.ErrorInternal(err)) 227 | return 228 | } 229 | 230 | c.JSON(http.StatusOK, nil) 231 | } 232 | 233 | // GetUsers to get all users 234 | func (uc UserController) GetUsers(c *gin.Context) { 235 | ctx := store.AuthContext(c) 236 | if !uc.ShouldBeLogged(ctx) { 237 | return 238 | } 239 | 240 | dbGroups, err := models.GetGroups(ctx, bson.M{}) 241 | utils.CheckErr(err) 242 | 243 | switch ctx.Role { 244 | case store.RoleGod: 245 | users, err := models.GetUsers(ctx, bson.M{}) 246 | for _, user := range users { 247 | group, err := models.FindGroup(dbGroups, user.GroupID) 248 | if err == nil { 249 | user.GroupID = group.Name 250 | } 251 | } 252 | if err != nil { 253 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 254 | return 255 | } 256 | c.JSON(http.StatusOK, users) 257 | case store.RoleAdmin, store.RoleUser: 258 | users, err := models.GetUsers(ctx, bson.M{"group_id": ctx.User.GetGroupID()}) 259 | for _, user := range users { 260 | group, err := models.FindGroup(dbGroups, user.GroupID) 261 | if err == nil { 262 | user.GroupID = group.Name 263 | } 264 | } 265 | if err != nil { 266 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 267 | return 268 | } 269 | c.JSON(http.StatusOK, users) 270 | case store.RoleCustomer: 271 | uc.AbortWithError(c, helpers.ErrorUserUnauthorized) 272 | } 273 | } 274 | 275 | // ResetPasswordRequest allows to send the user an email to reset his password 276 | func (UserController) ResetPasswordRequest(c *gin.Context) { 277 | ctx := store.AuthContext(c) 278 | dbUser, err := models.GetUser(ctx, bson.M{"email": c.Param("email")}) 279 | if err != nil { 280 | utils.Log(c, "warn", "ResetPasswordRequest failed with input:", c.Param("email")) 281 | return 282 | } 283 | 284 | encodedKey := []byte(config.GetString(c, "rsa_private")) 285 | privateKey, err := helpers.GetRSAPrivateKey(encodedKey) 286 | utils.CheckErr(err) 287 | 288 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ 289 | "sub": "define", 290 | "userId": dbUser.ID, 291 | "userKey": dbUser.Key, 292 | "userEmail": dbUser.Email, 293 | "userFirstname": dbUser.FirstName, 294 | "userLastname": dbUser.LastName, 295 | }) 296 | tokenStr, err := token.SignedString(privateKey) 297 | utils.CheckErr(err) 298 | 299 | apiURL := `https://` + config.GetString(c, "front_url") + `/update-account/` + tokenStr 300 | frontURL := config.GetString(c, "front_url") 301 | appName := config.GetString(c, "mail_sender_name") 302 | 303 | s := services.GetEmailSender(c) 304 | 305 | err = s.SendResetEmail(dbUser, apiURL, appName, frontURL) 306 | if err != nil { 307 | utils.Log(c, "info", "ErrorInternal when sending mail", err) 308 | c.AbortWithError(http.StatusUnauthorized, helpers.ErrorWithCode("mail_sending_error", "ErrorInternal when sending mail", err)) 309 | return 310 | } 311 | } 312 | 313 | // UpdateUser allows to update a user 314 | func (uc UserController) UpdateUser(c *gin.Context) { 315 | ctx := store.AuthContext(c) 316 | 317 | type UserInput struct { 318 | ID string `form:"id" json:"id"` 319 | Key string `form:"key" json:"key"` 320 | Password string `form:"password" json:"password"` 321 | Firstname string `form:"firstname" json:"firstname"` 322 | Lastname string `form:"lastname" json:"lastname"` 323 | } 324 | var userInput UserInput 325 | if err := c.ShouldBind(&userInput); err != nil { 326 | uc.AbortWithError(c, helpers.ErrorInvalidInput(err)) 327 | return 328 | } 329 | 330 | dbUser, err := models.GetUser(ctx, bson.M{"_id": userInput.ID}) 331 | if err != nil { 332 | uc.AbortWithError(c, helpers.ErrorResourceNotFound(err)) 333 | return 334 | } 335 | 336 | if userInput.Key != dbUser.Key { 337 | uc.AbortWithError(c, helpers.ErrorUserUnauthorized) 338 | return 339 | } 340 | 341 | dbUser.Key = helpers.RandomString(40) 342 | 343 | if dbUser.Status == "created" { 344 | dbUser.Status = "activated" 345 | } else if dbUser.Status == "activated" { 346 | dbUser.Status = "modified" 347 | } 348 | 349 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(userInput.Password), bcrypt.DefaultCost) 350 | if err != nil { 351 | uc.AbortWithError(c, helpers.ErrorInternal(err)) 352 | return 353 | } 354 | dbUser.Password = string(hashedPassword) 355 | 356 | if userInput.Firstname != "" { 357 | dbUser.FirstName = userInput.Firstname 358 | } 359 | if userInput.Lastname != "" { 360 | dbUser.LastName = userInput.Lastname 361 | } 362 | 363 | dbUser.LastModification = time.Now().Unix() 364 | err = models.UpdateUser(ctx, dbUser.ID, dbUser) 365 | utils.CheckErr(err) 366 | 367 | c.JSON(http.StatusOK, "User well modified") 368 | } 369 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | mongo-db: 4 | image: mongo:latest 5 | api: 6 | container_name: lumen-api 7 | build: . 8 | ports: 9 | - "4000:4000" 10 | environment: 11 | LUMEN_ENV: prod 12 | links: 13 | - mongo-db:mongo 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lumen/lumen-api 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/abiosoft/ishell v2.0.0+incompatible 7 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 8 | github.com/aws/aws-sdk-go v1.55.6 9 | github.com/chidiwilliams/flatbson v0.3.0 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 11 | github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 12 | github.com/gin-gonic/gin v1.10.0 13 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 14 | github.com/joho/godotenv v1.5.1 15 | github.com/matcornic/hermes/v2 v2.1.0 16 | github.com/pkg/errors v0.9.1 17 | github.com/sahilm/fuzzy v0.1.1 18 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e 19 | github.com/sirupsen/logrus v1.9.3 20 | github.com/snwfdhmp/errlog v0.0.0-20230627145903-cdffd0ba9ea9 21 | github.com/spf13/viper v1.19.0 22 | github.com/stretchr/testify v1.10.0 23 | github.com/tealeg/xlsx/v3 v3.3.11 24 | go.mongodb.org/mongo-driver v1.17.2 25 | golang.org/x/crypto v0.32.0 26 | golang.org/x/net v0.34.0 27 | gopkg.in/godo.v2 v2.0.9 28 | gopkg.in/gormigrate.v1 v1.6.0 29 | gorm.io/driver/postgres v1.5.11 30 | gorm.io/gorm v1.25.12 31 | ) 32 | 33 | require ( 34 | github.com/Masterminds/semver v1.4.2 // indirect 35 | github.com/Masterminds/sprig v2.16.0+incompatible // indirect 36 | github.com/MichaelTJones/walk v0.0.0-20161122175330-4748e29d5718 // indirect 37 | github.com/PuerkitoBio/goquery v1.5.0 // indirect 38 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect 39 | github.com/andybalholm/cascadia v1.0.0 // indirect 40 | github.com/aokoli/goutils v1.0.1 // indirect 41 | github.com/bytedance/sonic v1.11.6 // indirect 42 | github.com/bytedance/sonic/loader v0.1.1 // indirect 43 | github.com/chzyer/test v1.0.0 // indirect 44 | github.com/cloudwego/base64x v0.1.4 // indirect 45 | github.com/cloudwego/iasm v0.2.0 // indirect 46 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 47 | github.com/fatih/color v1.14.1 // indirect 48 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect 49 | github.com/frankban/quicktest v1.14.6 // indirect 50 | github.com/fsnotify/fsnotify v1.7.0 // indirect 51 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 52 | github.com/gin-contrib/sse v0.1.0 // indirect 53 | github.com/go-playground/locales v0.14.1 // indirect 54 | github.com/go-playground/universal-translator v0.18.1 // indirect 55 | github.com/go-playground/validator/v10 v10.20.0 // indirect 56 | github.com/goccy/go-json v0.10.2 // indirect 57 | github.com/golang/snappy v0.0.4 // indirect 58 | github.com/google/btree v1.0.0 // indirect 59 | github.com/google/go-cmp v0.6.0 // indirect 60 | github.com/google/uuid v1.4.0 // indirect 61 | github.com/gorilla/css v1.0.0 // indirect 62 | github.com/hashicorp/hcl v1.0.0 // indirect 63 | github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef // indirect 64 | github.com/huandu/xstrings v1.2.0 // indirect 65 | github.com/imdario/mergo v0.3.6 // indirect 66 | github.com/jackc/pgpassfile v1.0.0 // indirect 67 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 68 | github.com/jackc/pgx/v5 v5.5.5 // indirect 69 | github.com/jackc/puddle/v2 v2.2.1 // indirect 70 | github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect 71 | github.com/jinzhu/gorm v1.9.2 // indirect 72 | github.com/jinzhu/inflection v1.0.0 // indirect 73 | github.com/jinzhu/now v1.1.5 // indirect 74 | github.com/jmespath/go-jmespath v0.4.0 // indirect 75 | github.com/json-iterator/go v1.1.12 // indirect 76 | github.com/klauspost/compress v1.17.2 // indirect 77 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 78 | github.com/kr/pretty v0.3.1 // indirect 79 | github.com/kr/text v0.2.0 // indirect 80 | github.com/kylelemons/godebug v1.1.0 // indirect 81 | github.com/leodido/go-urn v1.4.0 // indirect 82 | github.com/magiconair/properties v1.8.7 // indirect 83 | github.com/mattn/go-colorable v0.1.13 // indirect 84 | github.com/mattn/go-isatty v0.0.20 // indirect 85 | github.com/mattn/go-runewidth v0.0.3 // indirect 86 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 87 | github.com/mgutz/str v1.2.0 // indirect 88 | github.com/mitchellh/mapstructure v1.5.0 // indirect 89 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 90 | github.com/modern-go/reflect2 v1.0.2 // indirect 91 | github.com/montanaflynn/stats v0.7.1 // indirect 92 | github.com/olekukonko/tablewriter v0.0.1 // indirect 93 | github.com/onsi/ginkgo v1.16.5 // indirect 94 | github.com/onsi/gomega v1.36.2 // indirect 95 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 96 | github.com/peterbourgon/diskv/v3 v3.0.1 // indirect 97 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 98 | github.com/rogpeppe/fastuuid v1.2.0 // indirect 99 | github.com/rogpeppe/go-internal v1.9.0 // indirect 100 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 101 | github.com/sagikazarmark/locafero v0.4.0 // indirect 102 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 103 | github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect 104 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 105 | github.com/sourcegraph/conc v0.3.0 // indirect 106 | github.com/spf13/afero v1.11.0 // indirect 107 | github.com/spf13/cast v1.6.0 // indirect 108 | github.com/spf13/pflag v1.0.5 // indirect 109 | github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect 110 | github.com/subosito/gotenv v1.6.0 // indirect 111 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 112 | github.com/ugorji/go/codec v1.2.12 // indirect 113 | github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect 114 | github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe // indirect 115 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 116 | github.com/xdg-go/scram v1.1.2 // indirect 117 | github.com/xdg-go/stringprep v1.0.4 // indirect 118 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 119 | go.uber.org/atomic v1.9.0 // indirect 120 | go.uber.org/multierr v1.9.0 // indirect 121 | golang.org/x/arch v0.8.0 // indirect 122 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 123 | golang.org/x/sync v0.10.0 // indirect 124 | golang.org/x/sys v0.29.0 // indirect 125 | golang.org/x/term v0.28.0 // indirect 126 | golang.org/x/text v0.21.0 // indirect 127 | google.golang.org/protobuf v1.36.1 // indirect 128 | gopkg.in/ini.v1 v1.67.0 // indirect 129 | gopkg.in/yaml.v2 v2.3.0 // indirect 130 | gopkg.in/yaml.v3 v3.0.1 // indirect 131 | ) 132 | -------------------------------------------------------------------------------- /helpers/error.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // Error type 9 | type Error struct { 10 | Code string `json:"code"` 11 | Message string `json:"message"` 12 | Trace error `json:"trace"` 13 | HTTPCode int `json:"-"` 14 | } 15 | 16 | // Pretty print of the error 17 | func (e Error) Error() string { 18 | return fmt.Sprintf("%v: %v", e.Code, e.Message) 19 | } 20 | 21 | // ErrorTrace is tracing the error 22 | func (e Error) ErrorTrace() error { 23 | return e.Trace 24 | } 25 | 26 | // ErrorWithCode is creating an error with code 27 | func ErrorWithCode(code string, message string, trace error) Error { 28 | return Error{Code: code, Message: message, Trace: trace} 29 | } 30 | 31 | // NewError is creating an error with code and HTTP code 32 | func NewError(httpCode int, code string, message string, trace error) Error { 33 | return Error{Code: code, Message: message, HTTPCode: httpCode, Trace: trace} 34 | } 35 | 36 | // NoCtxError defines an error that can be contextualized with a trace 37 | type NoCtxError func(error) Error 38 | 39 | // declare is used to declare const errors 40 | func declare(code string, httpCode int, message string) NoCtxError { 41 | return func(err error) Error { 42 | return Error{Code: code, HTTPCode: httpCode, Message: message, Trace: err} 43 | } 44 | } 45 | 46 | var ( 47 | // ErrorInvalidInput occurs when input is invalid 48 | ErrorInvalidInput = declare("invalid_input", http.StatusBadRequest, "Failed to bind the body data") 49 | // ErrorInternal occurs when there is internal server error 50 | ErrorInternal = declare("internal_error", http.StatusBadRequest, "Internal error") 51 | // ValidationError is for body validation 52 | ValidationError = declare("validation_error", http.StatusBadRequest, "Validation error") 53 | 54 | // ErrorTokenGenAccess occurs when access token failed to be generated 55 | ErrorTokenGenAccess = declare("token_generation_failed", http.StatusInternalServerError, "Could not generate the access token") 56 | // ErrorTokenGenRefresh occurs when refresh token failed to be generated 57 | ErrorTokenGenRefresh = declare("token_generation_failed", http.StatusInternalServerError, "Could not generate the refresh token") 58 | // ErrorTokenRefreshInvalid occurs when the refresh token is invalid 59 | ErrorTokenRefreshInvalid = declare("refresh_token_invalid", http.StatusBadRequest, "Refresh token invalid") 60 | 61 | // ErrorResourceNotFound occurs 62 | ErrorResourceNotFound = declare("resource_not_found", http.StatusNotFound, "Resource does not exist") 63 | 64 | // ErrorUserUnauthorized occurs when user doesn't have enough permissions to access au resource 65 | ErrorUserUnauthorized = Error{Code: "user_unauthorized", HTTPCode: http.StatusUnauthorized, Message: "Insufficient permissions to access this resource"} 66 | // ErrorUserUpdate occurs when user failed to be updated 67 | ErrorUserUpdate = declare("update_user_failed", http.StatusInternalServerError, "Could not update the user") 68 | // ErrorUserNotExist occurs when user doesn't exist 69 | ErrorUserNotExist = declare("user_does_not_exist", http.StatusNotFound, "User does not exist") 70 | // ErrorUserWrongPassword occurs when provided password is incorrect 71 | ErrorUserWrongPassword = declare("incorrect_password", http.StatusUnauthorized, "Password is not correct") 72 | // ErrorUserNotActivated occurs when user needs to activate its account via email 73 | ErrorUserNotActivated = declare("user_needs_activation", http.StatusNotFound, "User needs to be activated via email") 74 | 75 | // ErrorInvalidToken occurs when token is invalid (for example expired) 76 | ErrorInvalidToken = declare("invalid_token", http.StatusBadRequest, "The given token is invalid") 77 | 78 | // ErrorFileOpening occurs when file opening fails 79 | ErrorFileOpening = declare("file_opening_error", http.StatusNotAcceptable, "File opening error") 80 | // ErrorFileParsing occurs when file parsing fails 81 | ErrorFileParsing = declare("file_parsing_error", http.StatusNotAcceptable, "File parsing error") 82 | 83 | //ErrorUnprocessableEntity occurs when an internal rule fails to process entity 84 | ErrorUnprocessableEntity = declare("unprocessable_entity", http.StatusUnprocessableEntity, "Fail to process entity") 85 | ) 86 | -------------------------------------------------------------------------------- /helpers/jwt.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/rsa" 5 | "encoding/base64" 6 | "errors" 7 | "github.com/dgrijalva/jwt-go" 8 | "github.com/go-lumen/lumen-api/utils" 9 | "time" 10 | ) 11 | 12 | // GetRSAPrivateKey retrieves the RSA private key 13 | func GetRSAPrivateKey(encodedKey []byte) (*rsa.PrivateKey, error) { 14 | //Decode base64 key 15 | base64Text := make([]byte, base64.StdEncoding.DecodedLen(len(encodedKey))) 16 | _, err := base64.StdEncoding.Decode(base64Text, []byte(encodedKey)) 17 | utils.CheckErr(err) 18 | 19 | //Parse RSA key 20 | privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(base64Text) 21 | if err != nil { 22 | utils.CheckErr(err) 23 | return nil, err 24 | } 25 | 26 | return privateKey, nil 27 | } 28 | 29 | // GenerateToken Generates an access token 30 | func GenerateToken(encodedKey []byte, userID string, audience string, expiration int64) (*string, error) { 31 | privateKey, err := GetRSAPrivateKey(encodedKey) 32 | if err != nil { 33 | utils.CheckErr(err) 34 | return nil, err 35 | } 36 | 37 | access := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ 38 | "sub": userID, 39 | "aud": audience, 40 | "iat": time.Now().Unix(), 41 | "exp": time.Now().Add(time.Minute * time.Duration(expiration)).Unix(), 42 | }) 43 | 44 | accessString, err := access.SignedString(privateKey) 45 | if err != nil { 46 | utils.CheckErr(err) 47 | return nil, err 48 | } 49 | 50 | return &accessString, nil 51 | } 52 | 53 | // ValidateJwtToken Validates a JWT token 54 | func ValidateJwtToken(token string, encodedKey []byte, audience string) (jwt.MapClaims, error) { 55 | privateKey, err := GetRSAPrivateKey(encodedKey) 56 | if err != nil { 57 | utils.CheckErr(err) 58 | return nil, err 59 | } 60 | publicKey := privateKey.PublicKey 61 | 62 | rawToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { 63 | return &publicKey, nil 64 | }) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | //validate algorithm 70 | if rawToken.Header["alg"] != jwt.SigningMethodRS256.Alg() { 71 | return nil, err 72 | } 73 | 74 | //validate signature 75 | if !rawToken.Valid { 76 | return nil, errors.New("token in invalid (wrong signature)") 77 | } 78 | 79 | claims, ok := rawToken.Claims.(jwt.MapClaims) 80 | if !ok { 81 | return nil, errors.New("could not parse the claims") 82 | } 83 | 84 | //validate exp 85 | tokenExp := claims["exp"].(float64) 86 | if tokenExp < float64(time.Now().Unix()) { 87 | return nil, errors.New("token in invalid (expired)") 88 | } 89 | 90 | //validate aud 91 | tokenAud := claims["aud"].(string) 92 | if tokenAud != audience { 93 | return nil, errors.New("token in invalid (wrong audience)") 94 | } 95 | 96 | return claims, nil 97 | } 98 | -------------------------------------------------------------------------------- /helpers/params/params.go: -------------------------------------------------------------------------------- 1 | package params 2 | 3 | // M string interface 4 | type M map[string]interface{} 5 | 6 | // GetMap allows to retrieve string map 7 | func GetMap(m M) map[string]interface{} { 8 | return m 9 | } 10 | -------------------------------------------------------------------------------- /helpers/random_string.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 9 | const ( 10 | letterIDxBits = 6 11 | letterIDxMask = 1<= 0; { 20 | if remain == 0 { 21 | cache, remain = src.Int63(), letterIDxMax 22 | } 23 | if idx := int(cache & letterIDxMask); idx < len(letterBytes) { 24 | b[i] = letterBytes[idx] 25 | i-- 26 | } 27 | cache >>= letterIDxBits 28 | remain-- 29 | } 30 | 31 | return string(b) 32 | } 33 | -------------------------------------------------------------------------------- /lumen-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-lumen/lumen-api/5041dba8c4b140cf2231c7f60221a7874e00c37a/lumen-logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/models" 6 | "github.com/go-lumen/lumen-api/server" 7 | "github.com/go-lumen/lumen-api/services" 8 | "github.com/go-lumen/lumen-api/utils" 9 | "github.com/sirupsen/logrus" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | func main() { 14 | api := &server.API{Router: gin.Default(), Config: viper.New()} 15 | 16 | // Configuration setup 17 | err := api.SetupViper() 18 | utils.CheckErr(err) 19 | 20 | // Email sender setup 21 | api.EmailSender = services.NewEmailSender(api.Config) 22 | api.TextSender = services.NewTextSender(api.Config) 23 | 24 | // Database setup 25 | dbType := api.Config.GetString("db_type") 26 | switch dbType { 27 | case "mongo": 28 | _, err := api.SetupMongoDatabase() 29 | if err == nil { 30 | utils.Log(nil, "info", "SetupMongoDatabase OK") 31 | } else { 32 | utils.Log(nil, "error", "SetupMongoDatabase KO:", err) 33 | } 34 | utils.CheckErr(err) 35 | //defer session.Close() 36 | 37 | err = api.SetupMongoIndexes() 38 | if err == nil { 39 | utils.Log(nil, "info", "SetupMongoIndexes OK") 40 | } else { 41 | utils.Log(nil, "error", "SetupMongoIndexes KO:", err) 42 | } 43 | utils.CheckErr(err) 44 | 45 | // Seeds setup 46 | err = api.SetupMongoSeeds() 47 | utils.CheckErr(err) 48 | if err == nil { 49 | utils.Log(nil, "info", "SetupMongoSeeds OK") 50 | } else { 51 | utils.Log(nil, "error", "SetupMongoSeeds KO:", err) 52 | } 53 | utils.CheckErr(err) 54 | 55 | case "postgresql": 56 | db, err := api.SetupPostgreDatabase() 57 | utils.CheckErr(err) 58 | 59 | db.AutoMigrate(&models.Organization{}) 60 | db.AutoMigrate(&models.Group{}) 61 | db.AutoMigrate(&models.User{}) 62 | 63 | err = api.SetupPostgreSeeds() 64 | utils.CheckErr(err) 65 | } 66 | 67 | // Router setup 68 | api.SetupRouter() 69 | 70 | logrus.Infoln("SetupRouter OK") 71 | err = api.Router.Run(api.Config.GetString("host_address")) 72 | if err != nil { 73 | logrus.Infoln("api.Router.Run OK") 74 | } 75 | utils.CheckErr(err) 76 | } 77 | -------------------------------------------------------------------------------- /middlewares/authentication.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-lumen/lumen-api/config" 7 | "github.com/go-lumen/lumen-api/helpers" 8 | "github.com/go-lumen/lumen-api/models" 9 | "github.com/go-lumen/lumen-api/store" 10 | "github.com/go-lumen/lumen-api/utils" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "net/http" 13 | "strings" 14 | ) 15 | 16 | // AuthenticationMiddleware allows to analyze the token and check that it is valid 17 | func AuthenticationMiddleware() gin.HandlerFunc { 18 | return func(c *gin.Context) { 19 | tokenReader := c.Request.Header.Get("Authorization") 20 | 21 | authHeaderParts := strings.Split(tokenReader, " ") 22 | if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" { 23 | c.AbortWithError(http.StatusBadRequest, errors.New("Authorization header format must be Bearer {token}")) 24 | return 25 | } 26 | 27 | encodedKey := []byte(config.GetString(c, "rsa_private")) 28 | claims, err := helpers.ValidateJwtToken(authHeaderParts[1], encodedKey, "access") 29 | if err != nil { 30 | c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("invalid_token", "the given token is invalid", err)) 31 | return 32 | } 33 | 34 | ctx := store.AuthContext(c) 35 | user, _ := models.GetUser(ctx, bson.M{"id": claims["sub"].(string)}) 36 | //logrus.Infoln("looking for: " + claims["sub"].(string) + " Got user: " + fmt.Sprint(user)) 37 | c.Set(store.CurrentUserKey, user) 38 | group, err := models.GetGroup(ctx, bson.M{"id": user.GroupID}) 39 | if err != nil { 40 | utils.Log(c, "error", "Group not found") 41 | } 42 | c.Set(store.CurrentUserGroupKey, group) 43 | /*if err := store.UpdateUser(c, string(user.ID), bson.M{"$set": bson.M{"last_access": time.Now().Unix()}}); err != nil { 44 | println(err) 45 | }*/ 46 | 47 | /*if user.LastPasswordUpdate > int64(claims["iat"].(float64)) { 48 | c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("invalid_token_new_password", "the given token is invalid due to new password", err)) 49 | }*/ 50 | 51 | c.Next() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /middlewares/authorization.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/config" 6 | "github.com/go-lumen/lumen-api/helpers" 7 | "github.com/go-lumen/lumen-api/models" 8 | "github.com/go-lumen/lumen-api/store" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "strings" 11 | ) 12 | 13 | // AuthorizationMiddleware allows granting access to a resource or not 14 | func AuthorizationMiddleware() gin.HandlerFunc { 15 | return func(c *gin.Context) { 16 | tokenReader := c.Request.Header.Get("Authorization") 17 | 18 | authHeaderParts := strings.Split(tokenReader, " ") 19 | encodedKey := []byte(config.GetString(c, "rsa_private")) 20 | claims, _ := helpers.ValidateJwtToken(authHeaderParts[1], encodedKey, "access") 21 | ctx := store.AuthContext(c) 22 | user, _ := models.GetUser(ctx, bson.M{"_id": claims["sub"]}) 23 | c.Set(store.CurrentUserKey, user) 24 | 25 | c.Next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /middlewares/config.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/config" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // ConfigMiddleware allows to use viper configuration parameters set in .env files 10 | func ConfigMiddleware(viper *viper.Viper) gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | config.ToContext(c, config.New(viper)) 13 | c.Next() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /middlewares/cors.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | /* 4 | This code implements the flow chart that can be found here. 5 | http://www.html5rocks.com/static/images/cors_server_flowchart.png 6 | 7 | A Default Config for example is below: 8 | 9 | cors.Config{ 10 | Origins: "*", 11 | Methods: "GET, PUT, POST, DELETE", 12 | RequestHeaders: "Origin, Authorization, Content-Type", 13 | ExposedHeaders: "", 14 | MaxAge: 1 * time.Minute, 15 | Credentials: true, 16 | ValidateHeaders: false, 17 | } 18 | */ 19 | 20 | import ( 21 | "fmt" 22 | "github.com/gin-gonic/gin" 23 | "strings" 24 | "time" 25 | ) 26 | 27 | const ( 28 | // AllowOriginKey string key 29 | AllowOriginKey string = "Access-Control-Allow-Origin" 30 | // AllowCredentialsKey string key 31 | AllowCredentialsKey = "Access-Control-Allow-Credentials" 32 | // AllowHeadersKey string key 33 | AllowHeadersKey = "Access-Control-Allow-Headers" 34 | // AllowMethodsKey string key 35 | AllowMethodsKey = "Access-Control-Allow-Methods" 36 | // MaxAgeKey string key 37 | MaxAgeKey = "Access-Control-Max-Age" 38 | 39 | // OriginKey string key 40 | OriginKey = "Origin" 41 | // RequestMethodKey string key 42 | RequestMethodKey = "Access-Control-Request-Method" 43 | // RequestHeadersKey string key 44 | RequestHeadersKey = "Access-Control-Request-Headers" 45 | // ExposeHeadersKey string key 46 | ExposeHeadersKey = "Access-Control-Expose-Headers" 47 | ) 48 | 49 | const ( 50 | optionsMethod = "OPTIONS" 51 | ) 52 | 53 | // Config defines the configuration options available to control how the CORS middleware should function. 54 | type Config struct { 55 | // Enabling this causes us to compare Request-Method and Request-Headers to confirm they contain a subset of the Allowed Methods and Allowed Headers 56 | // The spec however allows for the server to always match, and simply return the allowed methods and headers. Either is supported in this middleware. 57 | ValidateHeaders bool 58 | 59 | // Comma delimited list of origin domains. Wildcard "*" is also allowed, and matches all origins. 60 | // If the origin does not match an item in the list, then the request is denied. 61 | Origins string 62 | origins []string 63 | 64 | // This are the headers that the resource supports, and will accept in the request. 65 | // Default is "Authorization". 66 | RequestHeaders string 67 | requestHeaders []string 68 | 69 | // These are headers that should be accessible by the CORS client, they are in addition to those defined by the spec as "simple response headers" 70 | // Cache-Control 71 | // Content-Language 72 | // Content-Type 73 | // Expires 74 | // Last-Modified 75 | // Pragma 76 | ExposedHeaders string 77 | 78 | // Comma delimited list of acceptable HTTP methods. 79 | Methods string 80 | methods []string 81 | 82 | // The amount of time in seconds that the client should cache the Preflight request 83 | MaxAge time.Duration 84 | maxAge string 85 | 86 | // If true, then cookies and Authorization headers are allowed along with the request. This 87 | // is passed to the browser, but is not enforced. 88 | Credentials bool 89 | credentials string 90 | } 91 | 92 | // One time, do the conversion from our the public facing Configuration, 93 | // to all the formats we use internally strings for headers.. slices for looping 94 | func (config *Config) prepare() { 95 | config.origins = strings.Split(config.Origins, ", ") 96 | config.methods = strings.Split(config.Methods, ", ") 97 | config.requestHeaders = strings.Split(config.RequestHeaders, ", ") 98 | config.maxAge = fmt.Sprintf("%.f", config.MaxAge.Seconds()) 99 | 100 | // Generates a boolean of value "true". 101 | config.credentials = fmt.Sprintf("%t", config.Credentials) 102 | 103 | // Convert to lower-case once as request headers are supposed to be a case-insensitive match 104 | for idx, header := range config.requestHeaders { 105 | config.requestHeaders[idx] = strings.ToLower(header) 106 | } 107 | } 108 | 109 | // CorsMiddleware generates a middleware handler function that works inside of a Gin request to set the correct CORS headers. It accepts a cors.Options struct for configuration. 110 | func CorsMiddleware(config Config) gin.HandlerFunc { 111 | forceOriginMatch := false 112 | 113 | if config.Origins == "" { 114 | panic("You must set at least a single valid origin. If you don't want CORS, to apply, simply remove the middleware.") 115 | } 116 | 117 | if config.Origins == "*" { 118 | forceOriginMatch = true 119 | } 120 | 121 | config.prepare() 122 | 123 | // Create the Middleware function 124 | return func(context *gin.Context) { 125 | // Read the Origin header from the HTTP request 126 | currentOrigin := context.Request.Header.Get(OriginKey) 127 | context.Writer.Header().Add("Vary", OriginKey) 128 | 129 | // CORS headers are added whenever the browser request includes an "Origin" header 130 | // However, if no Origin is supplied, they should never be added. 131 | if currentOrigin == "" { 132 | return 133 | } 134 | 135 | originMatch := false 136 | if !forceOriginMatch { 137 | originMatch = matchOrigin(currentOrigin, config) 138 | } 139 | 140 | if forceOriginMatch || originMatch { 141 | valid := false 142 | preflight := false 143 | 144 | if context.Request.Method == optionsMethod { 145 | requestMethod := context.Request.Header.Get(RequestMethodKey) 146 | if requestMethod != "" { 147 | preflight = true 148 | valid = handlePreflight(context, config, requestMethod) 149 | } 150 | } 151 | 152 | if !preflight { 153 | valid = handleRequest(context, config) 154 | } 155 | 156 | if valid { 157 | 158 | if config.Credentials { 159 | context.Writer.Header().Set(AllowCredentialsKey, config.credentials) 160 | // Allowed origins cannot be the string "*" cannot be used for a resource that supports credentials. 161 | context.Writer.Header().Set(AllowOriginKey, currentOrigin) 162 | } else if forceOriginMatch { 163 | context.Writer.Header().Set(AllowOriginKey, "*") 164 | } else { 165 | context.Writer.Header().Set(AllowOriginKey, currentOrigin) 166 | } 167 | 168 | //If this is a preflight request, we are finished, quit. 169 | //Otherwise this is a normal request and operations should proceed at normal 170 | if preflight { 171 | context.AbortWithStatus(200) 172 | } 173 | return 174 | } 175 | } 176 | 177 | //If it reaches here, it was not a valid request 178 | context.Abort() 179 | } 180 | } 181 | 182 | func handlePreflight(context *gin.Context, config Config, requestMethod string) bool { 183 | if ok := validateRequestMethod(requestMethod, config); ok == false { 184 | return false 185 | } 186 | 187 | if ok := validateRequestHeaders(context.Request.Header.Get(RequestHeadersKey), config); ok == true { 188 | context.Writer.Header().Set(AllowMethodsKey, config.Methods) 189 | context.Writer.Header().Set(AllowHeadersKey, config.RequestHeaders) 190 | 191 | if config.maxAge != "0" { 192 | context.Writer.Header().Set(MaxAgeKey, config.maxAge) 193 | } 194 | 195 | return true 196 | } 197 | 198 | return false 199 | } 200 | 201 | func handleRequest(context *gin.Context, config Config) bool { 202 | if config.ExposedHeaders != "" { 203 | context.Writer.Header().Set(ExposeHeadersKey, config.ExposedHeaders) 204 | } 205 | 206 | return true 207 | } 208 | 209 | // Case-sensitive match of origin header 210 | func matchOrigin(origin string, config Config) bool { 211 | for _, value := range config.origins { 212 | if value == origin { 213 | return true 214 | } 215 | } 216 | return false 217 | } 218 | 219 | // Case-sensitive match of request method 220 | func validateRequestMethod(requestMethod string, config Config) bool { 221 | if !config.ValidateHeaders { 222 | return true 223 | } 224 | 225 | if requestMethod != "" { 226 | for _, value := range config.methods { 227 | if value == requestMethod { 228 | return true 229 | } 230 | } 231 | } 232 | 233 | return false 234 | } 235 | 236 | // Case-insensitive match of request headers 237 | func validateRequestHeaders(requestHeaders string, config Config) bool { 238 | if !config.ValidateHeaders { 239 | return true 240 | } 241 | 242 | headers := strings.Split(requestHeaders, ",") 243 | 244 | for _, header := range headers { 245 | match := false 246 | header = strings.ToLower(strings.Trim(header, " \t\r\n")) 247 | 248 | for _, value := range config.requestHeaders { 249 | if value == header { 250 | match = true 251 | break 252 | } 253 | } 254 | 255 | if !match { 256 | return false 257 | } 258 | } 259 | 260 | return true 261 | } 262 | -------------------------------------------------------------------------------- /middlewares/email.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/services" 6 | ) 7 | 8 | // EmailMiddleware allows to retrieve EmailSender 9 | func EmailMiddleware(emailSender services.EmailSender) gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | c.Set("emailSender", emailSender) 12 | c.Next() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /middlewares/error.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/helpers" 6 | ) 7 | 8 | // ErrorMiddleware with logging if there is an error 9 | func ErrorMiddleware() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | c.Next() 12 | 13 | errorToPrint := c.Errors.Last() 14 | if errorToPrint != nil { 15 | original, ok := errorToPrint.Err.(helpers.Error) 16 | if ok { 17 | if original.HTTPCode == 0 { 18 | original.HTTPCode = -1 19 | } 20 | c.JSON(original.HTTPCode, gin.H{"errors": gin.H{"message": original.Message, "code": original.Code}}) 21 | } else { 22 | c.JSON(-1, gin.H{"errors": gin.H{"message": errorToPrint.Error(), "code": "unknown"}}) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /middlewares/store.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/go-lumen/lumen-api/config" 5 | "github.com/go-lumen/lumen-api/store" 6 | "github.com/go-lumen/lumen-api/store/mongodb" 7 | "github.com/go-lumen/lumen-api/store/postgresql" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | "gorm.io/gorm" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | // StoreMongoMiddleware allows to setup MongoDB database 15 | func StoreMongoMiddleware(db *mongo.Database) gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | //fmt.Println("Store mongo middleware:", db, c, mongodb.New(db, "things", c)) 18 | //store.ToContext(c, mongodb.New(db, config.GetString(c, "mongo_db_name"), c)) 19 | store.ToContext(c, mongodb.New(c, db, config.GetString(c, "mongo_db_name"))) 20 | //c.Set(store.AppKey, user) 21 | c.Next() 22 | } 23 | } 24 | 25 | // StorePostgreMiddleware allows to setup SQL database 26 | func StorePostgreMiddleware(db *gorm.DB) gin.HandlerFunc { 27 | return func(c *gin.Context) { 28 | store.ToContext(c, postgresql.New(c, db, config.GetString(c, "postgres_db_name"))) 29 | c.Next() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /middlewares/text.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/services" 6 | ) 7 | 8 | // TextMiddleware allows to retrieve the TextSender 9 | func TextMiddleware(textSender services.TextSender) gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | c.Set("textSender", textSender) 12 | c.Next() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /migrations.md: -------------------------------------------------------------------------------- 1 | le premier fichier va dans /cmd/genmigration/genmigration.go 2 | et tu l'installe en faisant "go install cmd/genmigration/genmigration.go" 3 | le second fichier tu le met dans un dossier "migrations" 4 | 5 | genmigration users 6 | 7 | https://github.com/go-gormigrate/gormigrate 8 | https://github.com/jinzhu/gorm 9 | https://github.com/jinzhu/gorm/blob/master/migration_test.go 10 | http://gorm.io/docs/models.html#Struct-tags 11 | https://gist.github.com/maxencehenneron/3aa2206988179840068e3f7465238cda -------------------------------------------------------------------------------- /migrations/201911060735_users.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "gopkg.in/gormigrate.v1" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | func init() { 9 | migrations = append(migrations, &gormigrate.Migration{ 10 | ID: "201911060735", 11 | Migrate: func(tx *gorm.DB) error { 12 | type User struct { 13 | gorm.Model 14 | Name string `json:"name"` 15 | FirstName string `json:"first_name"` 16 | LastName string `json:"last_name"` 17 | Password string `json:"password" valid:"required"` 18 | Email string `json:"email" valid:"email,required"` 19 | Phone string `json:"phone"` 20 | Language string `json:"language"` 21 | ActivationKey string `json:"activation_key"` 22 | ResetKey string `json:"reset_key"` 23 | Active bool `json:"active"` 24 | Admin bool `json:"admin"` 25 | LastModification int64 `json:"last_access"` 26 | LastPasswordUpdate int64 `json:"last_password_update"` 27 | } 28 | return tx.AutoMigrate(&User{}).Error 29 | }, 30 | Rollback: func(tx *gorm.DB) error { 31 | return tx.DropTable("users").Error 32 | }, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /migrations/migrator.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/go-lumen/lumen-api/server" 5 | "gopkg.in/gormigrate.v1" 6 | ) 7 | 8 | var ( 9 | migrations []*gormigrate.Migration 10 | ) 11 | 12 | type migrator struct { 13 | api *server.API 14 | } 15 | 16 | func New(api *server.API) *migrator { 17 | return &migrator{ 18 | api: api, 19 | } 20 | } 21 | 22 | // Run migration 23 | func (m *migrator) Migrate() error { 24 | gm := gormigrate.New(m.api.PostgreDatabase, gormigrate.DefaultOptions, migrations) 25 | 26 | if err := gm.Migrate(); err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | // MigrateTo executes all migrations that did not run yet up to 33 | // the migration that matches `migrationID`. 34 | func (m *migrator) MigrateTo(migrationID string) error { 35 | gm := gormigrate.New(m.api.PostgreDatabase, gormigrate.DefaultOptions, migrations) 36 | 37 | if err := gm.MigrateTo(migrationID); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | // RollbackLast undo the last migration 44 | func (m *migrator) RollbackLast() error { 45 | gm := gormigrate.New(m.api.PostgreDatabase, gormigrate.DefaultOptions, migrations) 46 | if err := gm.RollbackLast(); err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | // RollbackTo undoes migrations up to the given migration that matches the `migrationID`. 53 | // Migration with the matching `migrationID` is not rolled back. 54 | func (m *migrator) RollbackTo(migrationID string) error { 55 | gm := gormigrate.New(m.api.PostgreDatabase, gormigrate.DefaultOptions, migrations) 56 | if err := gm.RollbackTo(migrationID); err != nil { 57 | return err 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /migrations/template.tmpl: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "gopkg.in/gormigrate.v1" 6 | ) 7 | 8 | func init() { 9 | migrations = append(migrations, &gormigrate.Migration{ 10 | ID: "{{ $.MigrationID }}", 11 | Migrate: func(tx *gorm.DB) error { 12 | type {{ singularize $.ModelName }} struct { 13 | gorm.Model 14 | Name string `json:"name" gorm:"type:text"` 15 | } 16 | return tx.AutoMigrate(&{{ singularize $.ModelName }}{}).Error 17 | }, 18 | Rollback: func(tx *gorm.DB) error { 19 | return tx.DropTable("{{ toLower $.ModelName }}").Error 20 | }, 21 | }) 22 | } -------------------------------------------------------------------------------- /models/generic.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson" 4 | 5 | type QueryParams struct { 6 | Limit uint32 7 | Order int8 8 | StartTime uint32 9 | EndTime uint32 10 | } 11 | 12 | // All returns an empty filter (for semantic) 13 | func All() bson.M { return bson.M{} } 14 | 15 | // ByID returns a by ID filter 16 | func ByID(key string) bson.M { return bson.M{"_id": key} } 17 | 18 | // ByGroupID returns a group_id filter 19 | func ByGroupID(id string) bson.M { return bson.M{"group_id": id} } 20 | -------------------------------------------------------------------------------- /models/group.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "github.com/asaskevich/govalidator" 6 | mgobson "github.com/globalsign/mgo/bson" 7 | "github.com/go-lumen/lumen-api/helpers" 8 | "github.com/go-lumen/lumen-api/store" 9 | "github.com/sahilm/fuzzy" 10 | "github.com/sirupsen/logrus" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "net/http" 13 | ) 14 | 15 | // Group type holds all required informations 16 | type Group struct { 17 | //store.DefaultRoles `bson:"-,omitempty"` 18 | ID string `json:"id" bson:"_id,omitempty" valid:"-"` 19 | Name string `json:"name" bson:"name" valid:"-"` 20 | Role string `json:"role" bson:"role" valid:"-"` 21 | OrganizationID string `json:"organization_id" bson:"organization_id" valid:"-"` 22 | } 23 | 24 | // GetID returns ID 25 | func (group *Group) GetID() string { 26 | return group.ID 27 | } 28 | 29 | // GetRole returns role 30 | func (group *Group) GetRole() store.UserRole { 31 | return group.Role 32 | } 33 | 34 | // GetOrgID returns organization ID 35 | func (group *Group) GetOrgID() store.UserRole { 36 | return group.OrganizationID 37 | } 38 | 39 | // GetCollection returns mongodb collection 40 | func (group *Group) GetCollection() string { 41 | return GroupsCollection 42 | } 43 | 44 | // BeforeCreate validates object struct 45 | func (group *Group) BeforeCreate() error { 46 | group.ID = mgobson.NewObjectId().Hex() 47 | 48 | _, err := govalidator.ValidateStruct(group) 49 | if err != nil { 50 | return helpers.NewError(http.StatusBadRequest, "input_not_valid", err.Error(), err) 51 | } 52 | return nil 53 | } 54 | 55 | // FindGroup is used to find a group in a groups list (for performance purposes, only 1 db request) 56 | func FindGroup(dbGroups []*Group, groupID string) (ret *Group, err error) { 57 | for _, group := range dbGroups { 58 | if group.ID == groupID { 59 | return group, nil 60 | } 61 | } 62 | return nil, errors.New("group not found") 63 | } 64 | 65 | // FindGroupByFuzzyName is used to find a group in a groups list by fuzzy name matching (for performance purposes, only 1 db request) 66 | func FindGroupByFuzzyName(dbGroups []*Group, name string) (ret *Group, err error) { 67 | var groupNames []string 68 | for _, group := range dbGroups { 69 | groupNames = append(groupNames, group.Name) 70 | } 71 | matches := fuzzy.Find(name, groupNames) 72 | 73 | for _, group := range dbGroups { 74 | if matches.Len() > 0 && group.Name == matches[0].Str { 75 | return group, nil 76 | } 77 | } 78 | return nil, errors.New("group not found") 79 | } 80 | 81 | // GroupsCollection represents a specific MongoDB collection 82 | const GroupsCollection = "groups" 83 | 84 | // CreateGroup checks if group already exists, and if not, creates it 85 | func CreateGroup(c *store.Context, group *Group) error { 86 | 87 | err := group.BeforeCreate() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | var existingGroups []*Group 93 | err = c.Store.FindAll(c, bson.M{"name": group.Name}, &existingGroups) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | if len(existingGroups) > 0 { 99 | return helpers.NewError(http.StatusConflict, "group_already_exists", "Group already exists", err) 100 | } 101 | 102 | err = c.Store.Create(c, "groups", group) 103 | if err != nil { 104 | return helpers.NewError(http.StatusInternalServerError, "group_creation_failed", "Failed to insert the group in the database", err) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | // GetGroup allows to retrieve a group by its characteristics 111 | func GetGroup(c *store.Context, filter bson.M) (*Group, error) { 112 | var group Group 113 | err := c.Store.Find(c, filter, &group) 114 | if err != nil { 115 | return nil, helpers.NewError(http.StatusNotFound, "group_not_found", "Group not found", err) 116 | } 117 | 118 | return &group, err 119 | } 120 | 121 | // GetGroups allows to get all groups 122 | func GetGroups(c *store.Context, filter bson.M) ([]*Group, error) { 123 | var list []*Group 124 | err := c.Store.FindAll(c, filter, &list) 125 | if err != nil { 126 | logrus.Warnln("ErrorInternal on Finding all the documents", err) 127 | } 128 | 129 | return list, err 130 | } 131 | 132 | // ChangeGroupOrganization allows to change the organization of a group by its id 133 | func ChangeGroupOrganization(c *store.Context, groupID string, organizationID string) error { 134 | err := c.Store.Update(c, store.ID(groupID), &Group{OrganizationID: organizationID}, 135 | store.OnlyFields([]string{"organization_id"}), 136 | store.CreateIfNotExists(true)) 137 | 138 | if err != nil { 139 | return helpers.NewError(http.StatusInternalServerError, "group_group_change_failed", "Couldn't find the group to change group", err) 140 | } 141 | 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /models/notificationdata.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // EmailData holds informations required to send an email 4 | type EmailData struct { 5 | ReceiverMail string 6 | ReceiverName string 7 | User *User 8 | Subject string 9 | Body string 10 | ApiUrl string 11 | AppName string 12 | } 13 | 14 | // TextData holds informations required to send a text 15 | type TextData struct { 16 | PhoneNumber string 17 | Subject string 18 | Message string 19 | } 20 | -------------------------------------------------------------------------------- /models/organization.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "github.com/asaskevich/govalidator" 6 | mgobson "github.com/globalsign/mgo/bson" 7 | "github.com/go-lumen/lumen-api/helpers" 8 | "github.com/go-lumen/lumen-api/store" 9 | "github.com/go-lumen/lumen-api/utils" 10 | "github.com/sirupsen/logrus" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | "net/http" 14 | ) 15 | 16 | // Organization type holds all required information 17 | type Organization struct { 18 | store.DefaultRoles `bson:"-,omitempty"` 19 | ID string `json:"id" bson:"id,omitempty" valid:"-"` 20 | Name string `json:"name" bson:"name" valid:"-"` 21 | LogoURL string `json:"logo_url,omitempty" bson:"logo_url,omitempty" valid:"-"` 22 | Siret int64 `json:"siret,omitempty" bson:"siret,omitempty" valid:"-"` 23 | VATNumber string `json:"vat_number,omitempty" bson:"vat_number,omitempty" valid:"-"` 24 | Tokens int64 `json:"tokens,omitempty" bson:"tokens,omitempty" valid:"-"` 25 | Country string `json:"country" bson:"country" valid:"-"` 26 | AppKey string `json:"app_key,omitempty" bson:"app_key,omitempty" valid:"-"` 27 | Parent string `json:"parent_id,omitempty" bson:"parent_id,omitempty" valid:"-"` 28 | DefaultGroupID string `json:"default_group_id" bson:"default_group_id" valid:"-"` 29 | } 30 | 31 | // GetCollection returns mongodb collection 32 | func (organization *Organization) GetCollection() string { 33 | return "organizations" 34 | } 35 | 36 | // OrgByAppKey returns an org filter 37 | func OrgByAppKey(key string) bson.M { return bson.M{"app_key": key} } 38 | 39 | // SanitizedOrganization type holds only essential informations 40 | type SanitizedOrganization struct { 41 | ID string `json:"id" bson:"_id,omitempty" valid:"-"` 42 | Name string `json:"name" bson:"name" valid:"-"` 43 | LogoURL string `json:"logo_url" bson:"logo_url" valid:"-"` 44 | Parent string `json:"parent_id" bson:"parent_id" valid:"-"` 45 | } 46 | 47 | // SanitizedOrganizationWithParent type holds only essential informations with nested parent 48 | type SanitizedOrganizationWithParent struct { 49 | ID string `json:"id" bson:"_id,omitempty" valid:"-"` 50 | Name string `json:"name" bson:"name" valid:"-"` 51 | LogoURL string `json:"logo_url" bson:"logo_url" valid:"-"` 52 | Parent SanitizedOrganization `json:"parent_organization" bson:"parent_organization" valid:"-"` 53 | } 54 | 55 | // Sanitize allows to generate a SanitizedOrganization 56 | func (organization *Organization) Sanitize() SanitizedOrganization { 57 | return SanitizedOrganization{organization.ID, organization.Name, organization.LogoURL, organization.Parent} 58 | } 59 | 60 | // SanitizeWithParent allows to generate a SanitizedOrganizationWithParent 61 | func (organization *Organization) SanitizeWithParent(parentOrganization SanitizedOrganization) SanitizedOrganizationWithParent { 62 | return SanitizedOrganizationWithParent{organization.ID, organization.Name, organization.LogoURL, parentOrganization} 63 | } 64 | 65 | // FindOrganization is used to find an organization in a organizations list (for performance purposes, only 1 db request) 66 | func FindOrganization(dbOrganizations []*Organization, organizationID string) (ret *Organization, err error) { 67 | for _, organization := range dbOrganizations { 68 | if organization.ID == organizationID { 69 | return organization, nil 70 | } 71 | } 72 | return nil, errors.New("Organization not found") 73 | } 74 | 75 | // BeforeCreate validates object struct 76 | func (organization *Organization) BeforeCreate() error { 77 | organization.ID = mgobson.NewObjectId().Hex() 78 | organization.AppKey = helpers.RandomString(40) 79 | 80 | _, err := govalidator.ValidateStruct(organization) 81 | if err != nil { 82 | return helpers.NewError(http.StatusBadRequest, "input_not_valid", err.Error(), err) 83 | } 84 | return nil 85 | } 86 | 87 | // ApplyOptions set default find options 88 | func (organization *Organization) ApplyOptions(o *options.FindOptions) { 89 | o.SetSort(bson.D{{Key: "name", Value: 1}}) 90 | } 91 | 92 | // CreateOrganization checks if organization already exists, and if not, creates it 93 | func CreateOrganization(c *store.Context, organization *Organization) error { 94 | err := organization.BeforeCreate() 95 | if err != nil { 96 | return err 97 | } 98 | 99 | var existingOrgs []*Organization 100 | err = c.Store.FindAll(c, bson.M{"name": organization.Name}, &existingOrgs) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if len(existingOrgs) > 0 { 106 | return helpers.NewError(http.StatusConflict, "organization_already_exists", "Organization already exists", err) 107 | } 108 | 109 | err = c.Store.Create(c, "organizations", organization) 110 | if err != nil { 111 | utils.Log(nil, "warn", err) 112 | return helpers.NewError(http.StatusInternalServerError, "organization_creation_failed", "Failed to insert the organization in the database", err) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // GetOrganization allows to retrieve a organization by its characteristics 119 | func GetOrganization(c *store.Context, filter bson.M) (*Organization, error) { 120 | var organization Organization 121 | err := c.Store.Find(c, filter, &organization) 122 | if err != nil { 123 | return nil, helpers.NewError(http.StatusNotFound, "organization_not_found", "Organization not found", err) 124 | } 125 | 126 | return &organization, err 127 | } 128 | 129 | // IsOrganizationParent allows to know if an organization is a parent, and retrieve its parent if not 130 | func IsOrganizationParent(c *store.Context, organizationID string) (bool, string, error) { 131 | var organization Organization 132 | err := c.Store.Find(c, bson.M{"_id": organizationID}, &organization) 133 | if err != nil { 134 | return false, "", helpers.NewError(http.StatusNotFound, "organization_not_found", "Organization not found", err) 135 | } 136 | 137 | if len(organization.Parent) > 1 { 138 | return false, organization.Parent, nil 139 | } 140 | 141 | return true, "", err 142 | } 143 | 144 | // IsOrganizationChildren allows to know if an organization is a children 145 | func IsOrganizationChildren(c *store.Context, organizationID string, comparedOne string) (bool, string, error) { 146 | var organization Organization 147 | err := c.Store.Find(c, bson.M{"_id": organizationID}, &organization) 148 | if err != nil { 149 | return false, "", helpers.NewError(http.StatusNotFound, "organization_not_found", "Organization not found", err) 150 | } 151 | 152 | if organization.Parent == comparedOne { 153 | return true, organization.Parent, nil 154 | } 155 | 156 | return false, organization.Parent, err 157 | } 158 | 159 | // GetOrganizations allows to get all organizations 160 | func GetOrganizations(c *store.Context, filter bson.M) ([]*Organization, error) { 161 | var list []*Organization 162 | 163 | err := c.Store.FindAll(c, filter, &list) 164 | if err != nil { 165 | logrus.Warnln("ErrorInternal on Finding all the documents", err) 166 | } 167 | 168 | return list, err 169 | } 170 | 171 | // UpdateOrganization allows to update one or more organization characteristics 172 | func UpdateOrganization(c *store.Context, organizationID string, newOrganization *Organization) error { 173 | err := c.Store.Update(c, store.ID(organizationID), newOrganization, store.CreateIfNotExists(true)) 174 | if err != nil { 175 | return helpers.NewError(http.StatusInternalServerError, "organization_update_failed", "Failed to update the organization", err) 176 | } 177 | 178 | return nil 179 | } 180 | 181 | // ChangeParent allows to change an organization parent by their IDs 182 | func ChangeParent(c *store.Context, organizationID string, newParent string) error { 183 | err := c.Store.Update(c, store.ID(organizationID), &Organization{Parent: newParent}, 184 | store.CreateIfNotExists(true), 185 | store.OnlyFields([]string{"parent_id"})) 186 | if err != nil { 187 | return helpers.NewError(http.StatusInternalServerError, "organization_update_failed", "Failed to update the organization", err) 188 | } 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "github.com/asaskevich/govalidator" 6 | mgobson "github.com/globalsign/mgo/bson" 7 | "github.com/go-lumen/lumen-api/helpers" 8 | "github.com/go-lumen/lumen-api/store" 9 | "github.com/sirupsen/logrus" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "golang.org/x/crypto/bcrypt" 12 | "gorm.io/gorm" 13 | "net/http" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // UserAuth type holds required information for authentication 19 | type UserAuth struct { 20 | Email string `json:"email" bson:"email" valid:"email,required"` 21 | Password string `json:"password" bson:"password" valid:"required"` 22 | } 23 | 24 | // User type holds all required information 25 | type User struct { 26 | store.DefaultRoles `bson:"-,omitempty"` 27 | ID string `json:"id" bson:"id,omitempty" valid:"-"` 28 | FirstName string `json:"first_name" bson:"first_name" valid:"-"` 29 | LastName string `json:"last_name" bson:"last_name" valid:"-"` 30 | Password string `json:"password" bson:"password" valid:"required"` 31 | Email string `json:"email" bson:"email" valid:"email,required"` 32 | Address string `json:"address,omitempty" bson:"address,omitempty" valid:"-"` 33 | Status string `json:"status" bson:"status" valid:"-"` 34 | Phone string `json:"phone,omitempty" bson:"phone,omitempty" valid:"-"` 35 | Language string `json:"language,omitempty" bson:"language,omitempty" valid:"-"` 36 | Key string `json:"key,omitempty" bson:"key,omitempty" valid:"-"` 37 | LastLogin int64 `json:"last_login,omitempty" bson:"last_login,omitempty" valid:"-"` 38 | LastModification int64 `json:"last_modification,omitempty" bson:"last_modification,omitempty" valid:"-"` 39 | GroupID string `json:"group_id" bson:"group_id" valid:"-"` 40 | } 41 | 42 | // GetID returns ID 43 | func (user *User) GetID() string { 44 | return user.ID 45 | } 46 | 47 | // GetGroupID returns organization ID 48 | func (user *User) GetGroupID() string { 49 | return user.GroupID 50 | } 51 | 52 | // GetCollection returns mongodb collection 53 | func (user *User) GetCollection() string { 54 | return UsersCollection 55 | } 56 | 57 | // UserDetails type holds user with details 58 | type UserDetails struct { 59 | ID string `json:"id" bson:"_id,omitempty" valid:"-"` 60 | FirstName string `json:"first_name" bson:"first_name" valid:"-"` 61 | LastName string `json:"last_name" bson:"last_name" valid:"-"` 62 | Email string `json:"email" bson:"email" valid:"email,required"` 63 | Address string `json:"address" bson:"address" valid:"-"` 64 | Status string `json:"status" bson:"status" valid:"-"` 65 | Phone string `json:"phone" bson:"phone" valid:"-"` 66 | Language string `json:"language" bson:"language" valid:"-"` 67 | GroupID string `json:"group_id" bson:"group_id" valid:"-"` 68 | Role string `json:"role" bson:"role" valid:"-"` 69 | Organization string `json:"organization" bson:"organization" valid:"-"` 70 | } 71 | 72 | // SanitizedUser allows to expose only few characteristics 73 | type SanitizedUser struct { 74 | ID string `json:"id" bson:"_id,omitempty" valid:"-"` 75 | FirstName string `json:"first_name" bson:"first_name" valid:"-"` 76 | LastName string `json:"last_name" bson:"last_name" valid:"-"` 77 | Email string `json:"email" bson:"email" valid:"-"` 78 | Status string `json:"status" bson:"status" valid:"-"` 79 | GroupID string `json:"group_id" bson:"group_id" valid:"-"` 80 | Role string `json:"role" bson:"role" valid:"-"` 81 | OrganizationID string `json:"organization_id" bson:"organization_id" valid:"-"` 82 | OrganizationName string `json:"organization_name" bson:"organization_name" valid:"-"` 83 | } 84 | 85 | // Sanitize allows to create a lightweight user 86 | func (user *User) Sanitize(role string, organizationID string, organizationName string) SanitizedUser { 87 | return SanitizedUser{user.ID, user.FirstName, user.LastName, user.Email, user.Status, user.GroupID, role, organizationID, organizationName} 88 | } 89 | 90 | // Detail to detail a user 91 | func (user *User) Detail(role string, organization string) UserDetails { 92 | return UserDetails{user.ID, user.FirstName, user.LastName, user.Email, user.Address, user.Status, user.Phone, user.Language, user.GroupID, role, organization} 93 | } 94 | 95 | // FindUser is used to find a user in a users list (for performance purposes, only 1 db request) 96 | func FindUser(dbUsers []*User, userID string) (ret *User, err error) { 97 | for _, user := range dbUsers { 98 | if userID == user.ID { 99 | return user, nil 100 | } 101 | } 102 | return nil, errors.New("User not found") 103 | } 104 | 105 | // BeforeCreate validates object struct 106 | func (user *User) BeforeCreate(tx *gorm.DB, keepId, keepKey, keepPassword bool) error { 107 | if !keepId { 108 | user.ID = mgobson.NewObjectId().Hex() 109 | } 110 | if !keepKey { 111 | user.Key = helpers.RandomString(40) 112 | } 113 | user.Email = strings.ToLower(user.Email) 114 | user.LastModification = time.Now().Unix() 115 | if user.Status == "" { 116 | user.Status = "created" 117 | } 118 | 119 | if !keepPassword { 120 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) 121 | if err != nil { 122 | return helpers.NewError(http.StatusInternalServerError, "encryption_failed", "Failed to generate the crypted password", err) 123 | } 124 | user.Password = string(hashedPassword) //user.Password 125 | } 126 | 127 | _, err := govalidator.ValidateStruct(user) 128 | if err != nil { 129 | return helpers.NewError(http.StatusBadRequest, "input_not_valid", err.Error(), err) 130 | } 131 | return nil 132 | } 133 | 134 | // UsersCollection represents a specific MongoDB collection 135 | const UsersCollection = "users" 136 | 137 | // UsersTableName represents a SQL table 138 | const UsersTableName = "users_user" 139 | 140 | // CreateUser checks if user already exists, and if not, creates it 141 | func CreateUser(c *store.Context, user *User) error { 142 | var existingUsers []*User 143 | if err := c.Store.FindAll(c, bson.M{"email": user.Email}, &existingUsers); err != nil { 144 | return err 145 | } 146 | 147 | if len(existingUsers) > 0 { 148 | return helpers.NewError(http.StatusConflict, "user_already_exists", "User already exists", nil) 149 | } 150 | 151 | err := c.Store.Create(c, "users", user) 152 | if err != nil { 153 | return helpers.NewError(http.StatusInternalServerError, "user_creation_failed", "Failed to insert the user in the database", err) 154 | } 155 | 156 | return nil 157 | } 158 | 159 | // GetUser allows to retrieve a user by its characteristics 160 | func GetUser(c *store.Context, filter bson.M) (*User, error) { 161 | var user User 162 | err := c.Store.Find(c, filter, &user) 163 | if err != nil { 164 | return nil, helpers.NewError(http.StatusNotFound, "user_not_found", "User not found", err) 165 | } 166 | 167 | return &user, err 168 | } 169 | 170 | // UserExists allows checking if a user exists by its email 171 | func UserExists(c *store.Context, email string) (bool, *User, error) { 172 | var user User 173 | err := c.Store.Find(c, bson.M{"email": email}, &user) 174 | if err != nil { 175 | return false, nil, helpers.NewError(http.StatusNotFound, "user_not_found", "User not found", err) 176 | } 177 | 178 | return true, &user, err 179 | } 180 | 181 | // GetUsers allows to get all users 182 | func GetUsers(c *store.Context, filter bson.M) ([]*User, error) { 183 | var list []*User 184 | 185 | err := c.Store.FindAll(c, filter, &list, store.WithSort("last_name", store.SortAscending)) 186 | if err != nil { 187 | logrus.Warnln("ErrorInternal on Finding all the documents", err) 188 | } 189 | 190 | return list, err 191 | } 192 | 193 | // UpdateUser allows to update one or more user characteristics 194 | func UpdateUser(c *store.Context, userID string, newUser *User) error { 195 | err := c.Store.Update(c, store.ID(userID), newUser, store.CreateIfNotExists(true)) 196 | if err != nil { 197 | return helpers.NewError(http.StatusInternalServerError, "user_update_failed", "Failed to update the user", err) 198 | } 199 | 200 | return nil 201 | } 202 | 203 | // ActivateUser allows to activate a user by its id 204 | func ActivateUser(c *store.Context, activationKey string, id string) error { 205 | err := c.Store.Update(c, bson.M{"id": id, "key": activationKey}, &User{Status: "activated"}, store.OnlyFields([]string{"status"})) 206 | //mongo: err := c.Store.Update(c, bson.M{"$and": []bson.M{{"id": id}, {"key": activationKey}}}, &User{Status: "activated"}, store.OnlyFields([]string{"status"})) 207 | //psql arr: err := c.Store.Update(c, bson.A{bson.M{"id": id}, bson.M{"key": activationKey}}, &User{Status: "activated"}, store.OnlyFields([]string{"status"})) 208 | 209 | if err != nil { 210 | return helpers.NewError(http.StatusInternalServerError, "user_activation_failed", "Couldn't find the user to activate", err) 211 | } 212 | 213 | return nil 214 | } 215 | 216 | // DeleteUser allows to delete a user by its id 217 | func DeleteUser(c *store.Context, userID string) error { 218 | err := c.Store.Delete(c, userID, &User{}) 219 | if err != nil { 220 | return helpers.NewError(http.StatusInternalServerError, "user_delete_failed", "Failed to delete the user", err) 221 | } 222 | 223 | return nil 224 | } 225 | -------------------------------------------------------------------------------- /nginx/conf-https-step-1: -------------------------------------------------------------------------------- 1 | server { 2 | server_name api.yourdomain.com www.api.yourdomain.com; 3 | 4 | root /var/www/api-yourdomain; 5 | 6 | location ~ /\.well-known/acme-challenge { 7 | allow all; 8 | } 9 | # On interdit habituellement l'accès au dotfiles 10 | #location ~ /\. { deny all; access_log off; log_not_found off; } 11 | 12 | } -------------------------------------------------------------------------------- /nginx/conf-https-step-2: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | 5 | server_name api.yourdomain.com www.api.yourdomain.com; 6 | location '/.well-known/acme-challenge' { 7 | root /var/www/yourdomain-api; 8 | } 9 | 10 | location / { 11 | return 301 https://api.yourdomain.com$request_uri; 12 | } 13 | } 14 | 15 | server { 16 | listen 443 ssl http2; 17 | listen [::]:443 ssl http2; 18 | 19 | server_name api.yourdomain.com www.api.yourdomain.com; 20 | 21 | location / { 22 | proxy_pass http://localhost:4000/; 23 | proxy_set_header X-Forwarded-Host $host; 24 | proxy_set_header X-Forwarded-Server $host; 25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 26 | 27 | } 28 | 29 | #root /var/www/yourdomain-api; 30 | #index index.html index.htm; 31 | error_log /var/log/nginx/api.yourdomain.com.log notice; 32 | access_log off; 33 | 34 | #### Locations 35 | # On cache les fichiers statiques 36 | location ~* \.(html|css|js|png|jpg|jpeg|gif|ico|svg|eot|woff|ttf)$ { expires max; } 37 | # On interdit les dotfiles 38 | location ~ /\. { deny all; } 39 | 40 | 41 | #### SSL 42 | ssl on; 43 | ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem; 44 | ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem; 45 | 46 | ssl_stapling on; 47 | ssl_stapling_verify on; 48 | ssl_trusted_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem; 49 | # Google DNS, Open DNS, Dyn DNS 50 | resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 216.146.35.35 216.146.36.36 valid=300s; 51 | resolver_timeout 3s; 52 | 53 | 54 | #### Session Tickets 55 | # Session Cache doit avoir la même valeur sur tous les blocs "server". 56 | ssl_session_cache shared:SSL:100m; 57 | ssl_session_timeout 24h; 58 | ssl_session_tickets on; 59 | # [ATTENTION] il faudra générer le ticket de session. 60 | #ssl_session_ticket_key /etc/nginx/ssl/ticket.key; 61 | 62 | # [ATTENTION] Les paramètres Diffie-Helman doivent être générés 63 | #ssl_dhparam /etc/nginx/ssl/dhparam.pem; 64 | 65 | 66 | #### ECDH Curve 67 | ssl_ecdh_curve secp384r1; 68 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 69 | ssl_prefer_server_ciphers on; 70 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 71 | } -------------------------------------------------------------------------------- /prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | openssl genrsa -out key.rsa 2048 3 | openssl base64 -in key.rsa -out key64.rsa 4 | cat key64.rsa | tr -d '\n' 5 | docker run --name things-mongo -d mongo 6 | docker build -t api-things-img . 7 | docker run --name lumen-api -p 127.0.0.1:4000:4000 --link things-mongo:mongo -d api-things-img -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /server/api.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-lumen/lumen-api/services" 7 | "github.com/go-lumen/lumen-api/store" 8 | "github.com/spf13/viper" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | // API structure that holds various necessary services 14 | type API struct { 15 | Router *gin.Engine 16 | Config *viper.Viper 17 | Context *store.Context 18 | MongoDatabase *mongo.Database 19 | PostgreDatabase *gorm.DB 20 | MySQLDatabase *sql.DB 21 | EmailSender services.EmailSender 22 | TextSender services.TextSender 23 | } 24 | -------------------------------------------------------------------------------- /server/database.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | "go.mongodb.org/mongo-driver/mongo/options" 8 | 9 | "gorm.io/driver/postgres" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type dbLogger struct{} 14 | 15 | func (a *API) SetupMongoDatabase() (*mongo.Database, error) { 16 | uri := a.Config.GetString("mongo_db_prefix") 17 | 18 | if (a.Config.GetString("mongo_db_user") != "") && (a.Config.GetString("mongo_db_password") != "") { 19 | uri += a.Config.GetString("mongo_db_user") + ":" + a.Config.GetString("mongo_db_password") + "@" 20 | } 21 | 22 | uri += a.Config.GetString("mongo_db_host") 23 | 24 | //utils.Log(nil, "info", uri) 25 | 26 | clientOptions := options.Client().ApplyURI(uri) 27 | 28 | // Connect to MongoDB 29 | client, err := mongo.Connect(context.TODO(), clientOptions) 30 | if err != nil { 31 | return nil, fmt.Errorf("Mongo client couldn't connect with background context: %v", err) 32 | } 33 | database := client.Database(a.Config.GetString("mongo_db_name")) 34 | a.MongoDatabase = database 35 | 36 | return database, nil 37 | } 38 | 39 | // SetupPostgreDatabase establishes the connexion with the PostgreSQL database 40 | func (a *API) SetupPostgreDatabase() (*gorm.DB, error) { 41 | connectionURI := fmt.Sprintf( 42 | "dbname=%s host=%s port=%s user=%s password=%s sslmode=disable", 43 | a.Config.GetString("postgres_db_name"), 44 | a.Config.GetString("postgres_db_addr"), 45 | a.Config.GetString("postgres_db_port"), 46 | a.Config.GetString("postgres_db_user"), 47 | a.Config.GetString("postgres_db_password"), 48 | ) 49 | 50 | db, err := gorm.Open(postgres.Open(connectionURI), &gorm.Config{}) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | a.PostgreDatabase = db 56 | return db, nil 57 | } 58 | -------------------------------------------------------------------------------- /server/index.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-lumen/lumen-api/utils" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | "go.mongodb.org/mongo-driver/mongo/options" 11 | ) 12 | 13 | // SetupMongoIndexes allows to setup MongoDB index 14 | func (a *API) SetupMongoIndexes() error { 15 | /*database := a.MongoDatabase 16 | 17 | collection := database.C(models.DeviceMessagesCollection) 18 | err := collection.EnsureIndex(mgo.Index{ 19 | { 20 | Key: []string{"$2dsphere:location"}, 21 | Bits: 26, 22 | }, 23 | })*/ 24 | 25 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 26 | defer cancel() 27 | db := a.MongoDatabase 28 | 29 | indexOpts := options.CreateIndexes().SetMaxTime(time.Second * 10) 30 | // Index to location 2dsphere type. 31 | pointIndexModel := mongo.IndexModel{ 32 | Options: options.Index().SetBackground(true), 33 | Keys: bson.D{{Key: "location", Value: "2dsphere"}}, 34 | } 35 | poiIndexes := db.Collection("pois").Indexes() 36 | deviceMessagesNames, err := poiIndexes.CreateOne(ctx, pointIndexModel, indexOpts) 37 | if err != nil { 38 | return err 39 | } 40 | utils.Log(nil, "info", "Index successfully created for:", deviceMessagesNames) 41 | /* 42 | 43 | // Creates a list of indexes to ensure 44 | collectionIndexes := make(map[*mgo.Collection][]mgo.Index) 45 | 46 | // User indexes 47 | users := database.C(models.UsersCollection) 48 | collectionIndexes[users] = []mgo.Index{ 49 | { 50 | Key: []string{"email"}, 51 | Unique: true, 52 | }, 53 | } 54 | 55 | deviceMessages := database.C(models.DeviceMessagesCollection) 56 | collectionIndexes[deviceMessages] = []mgo.Index{ 57 | { 58 | Key: []string{"$2dsphere:location"}, 59 | Bits: 26, 60 | }, 61 | } 62 | 63 | for collection, indexes := range collectionIndexes { 64 | for _, index := range indexes { 65 | err := collection.EnsureIndex(index) 66 | 67 | utils.CheckErr(err) 68 | } 69 | }*/ 70 | /*var indexView *mongo.IndexView 71 | 72 | // Specify the MaxTime option to limit the amount of time the operation can run on the server 73 | opts := options.ListIndexes().SetMaxTime(2 * time.Second) 74 | cursor, err := indexView.List(context.TODO(), opts) 75 | if err != nil { 76 | utils.Log(nil, "error", err) 77 | } 78 | 79 | // Get a slice of all indexes returned and print them out. 80 | var results []bson.M 81 | if err = cursor.All(context.TODO(), &results); err != nil { 82 | log.Fatal(err) 83 | } 84 | utils.Log(nil, "info", "index results:", results)*/ 85 | 86 | /*var indexView *mongo.IndexView 87 | 88 | // Create two indexes: {name: 1, email: 1} and {name: 1, age: 1} 89 | // For the first index, specify no options. The name will be generated as "name_1_email_1" by the driver. 90 | // For the second index, specify the Name option to explicitly set the name to "nameAge". 91 | models := []mongo.IndexModel{ 92 | { 93 | Keys: bson.D{{"name", 1}, {"email", 1}}, 94 | }, 95 | { 96 | Keys: bson.D{{"name", 1}, {"age", 1}}, 97 | Options: options.Index().SetName("nameAge"), 98 | }, 99 | } 100 | 101 | // Specify the MaxTime option to limit the amount of time the operation can run on the server 102 | opts := options.CreateIndexes().SetMaxTime(2 * time.Second) 103 | names, err := indexView.CreateMany(context.TODO(), models, opts) 104 | if err != nil { 105 | log.Fatal(err) 106 | } 107 | 108 | fmt.Printf("created indexes %v\n", names)*/ 109 | return nil 110 | } 111 | 112 | /*func CreateValidator(collection *mgo.Collection, validator bson.M) { 113 | info := &mgo.CollectionInfo{ 114 | Validator: validator, 115 | ValidationLevel: "strict", 116 | } 117 | collection.Create(info) 118 | }*/ 119 | -------------------------------------------------------------------------------- /server/router.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/go-lumen/lumen-api/config" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/go-lumen/lumen-api/controllers" 9 | "github.com/go-lumen/lumen-api/middlewares" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | // Index is the default place 15 | func Index(c *gin.Context) { 16 | c.JSON(http.StatusOK, gin.H{"status": "success", "message": "You successfully reached the " + config.GetString(c, "mail_sender_name") + " API."}) 17 | } 18 | 19 | // SetupRouter is the main routing point 20 | func (a *API) SetupRouter() { 21 | router := a.Router 22 | 23 | router.Use(middlewares.ErrorMiddleware()) 24 | 25 | router.Use(middlewares.CorsMiddleware(middlewares.Config{ 26 | Origins: "*", 27 | Methods: "GET, PUT, POST, DELETE", 28 | RequestHeaders: "Origin, Authorization, Content-Type", 29 | ExposedHeaders: "", 30 | MaxAge: 50 * time.Second, 31 | Credentials: true, 32 | ValidateHeaders: false, 33 | })) 34 | 35 | router.Use(middlewares.ConfigMiddleware(a.Config)) 36 | 37 | dbType := a.Config.GetString("db_type") 38 | switch dbType { 39 | case "mongo": 40 | router.Use(middlewares.StoreMongoMiddleware(a.MongoDatabase)) 41 | case "postgresql": 42 | router.Use(middlewares.StorePostgreMiddleware(a.PostgreDatabase)) 43 | } 44 | 45 | router.Use(middlewares.EmailMiddleware(a.EmailSender)) 46 | router.Use(middlewares.TextMiddleware(a.TextSender)) 47 | 48 | authenticationMiddleware := middlewares.AuthenticationMiddleware() 49 | //authorizationMiddleware := middlewares.AuthorizationMiddleware() 50 | 51 | v1 := router.Group("/v1") 52 | { 53 | v1.GET("/", Index) 54 | 55 | authentication := v1.Group("/auth") 56 | { 57 | authController := controllers.NewAuthController() 58 | userController := controllers.NewUserController() 59 | authentication.POST("/", authController.TokensGeneration) 60 | authentication.POST("/renew", authController.TokenRenewal) 61 | authentication.Use(authenticationMiddleware) 62 | authentication.GET("/", userController.GetUserMe) 63 | authentication.GET("/details", userController.GetUserDetails) 64 | //https://skarlso.github.io/2016/06/12/google-signin-with-go/ 65 | //https://github.com/zalando/gin-oauth2/blob/master/google/google.go 66 | } 67 | 68 | users := v1.Group("/users") 69 | { 70 | userController := controllers.NewUserController() 71 | users.POST("/resetPassword/:email", userController.ResetPasswordRequest) 72 | users.POST("/update", userController.UpdateUser) 73 | users.Use(authenticationMiddleware) 74 | users.POST("/", userController.CreateUser) 75 | users.GET("/:id", userController.GetUser) 76 | users.POST("/changeGroup/:groupID", userController.ChangeUserGroup) 77 | users.DELETE("/:id", userController.DeleteUser) 78 | users.GET("/", userController.GetUsers) 79 | } 80 | 81 | organizations := v1.Group("/organizations") 82 | { 83 | organizationController := controllers.NewOrganizationController() 84 | organizations.GET("/token/:id", organizationController.GetOrganizationByAppKey) 85 | organizations.Use(authenticationMiddleware) 86 | organizations.POST("/", organizationController.CreateOrganization) 87 | organizations.GET("/", organizationController.GetOrganizations) 88 | organizations.GET("/me", organizationController.GetUserOrganization) 89 | organizations.GET("/:id", organizationController.GetOrganization) 90 | organizations.GET("/:id/groups", organizationController.GetOrganizationGroups) 91 | } 92 | 93 | groups := v1.Group("/groups") 94 | { 95 | groupController := controllers.NewGroupController() 96 | groups.Use(authenticationMiddleware) 97 | groups.POST("/", groupController.CreateGroup) 98 | groups.GET("/", groupController.GetGroups) 99 | groups.GET("/:id", groupController.GetGroup) 100 | groups.GET("/me", groupController.GetUserGroup) 101 | } 102 | } 103 | return 104 | } 105 | -------------------------------------------------------------------------------- /server/seeder.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-lumen/lumen-api/models" 6 | "github.com/go-lumen/lumen-api/store" 7 | "github.com/go-lumen/lumen-api/store/mongodb" 8 | "github.com/go-lumen/lumen-api/store/postgresql" 9 | "github.com/go-lumen/lumen-api/utils" 10 | "go.mongodb.org/mongo-driver/bson" 11 | ) 12 | 13 | // SetupMongoSeeds creates the first user 14 | func (a *API) SetupMongoSeeds() error { 15 | s := mongodb.New(nil, a.MongoDatabase, a.Config.GetString("mongo_db_name")) 16 | ctx := store.NewGodContext(s) 17 | 18 | //Mails: 0.10$/1000 Texts: 0.05-0.10$/1 WiFi: 5$/1000 19 | 20 | organization := &models.Organization{ 21 | Name: a.Config.GetString("project_name"), 22 | LogoURL: "", 23 | Siret: 0, 24 | VATNumber: "", 25 | Tokens: 100000000000, 26 | Parent: "", 27 | } 28 | dbOrga := &models.Organization{} 29 | if err := s.Find(ctx, bson.M{"name": organization.Name}, dbOrga); err == nil { 30 | utils.Log(nil, "warn", `Organization:`, organization.Name, `already exists`) 31 | } else if err := s.Create(ctx, "", organization); err != nil { 32 | utils.Log(nil, "err", `ErrorInternal when creating organization:`, err) 33 | } else { 34 | utils.Log(nil, "info", `Organization:`, organization.Name, `well created`) 35 | } 36 | 37 | group := &models.Group{ 38 | Name: a.Config.GetString("project_name") + " superadmin", 39 | Role: store.RoleGod, 40 | OrganizationID: organization.ID, 41 | } 42 | if err := s.Find(ctx, bson.M{"name": group.Name}, group); err == nil { 43 | utils.Log(nil, "warn", `Group:`, group.Name, `already exists`) 44 | } else if err := s.Create(ctx, "", group); err != nil { 45 | utils.Log(nil, "err", `ErrorInternal when creating group:`, group.Name, err) 46 | } else { 47 | utils.Log(nil, "info", "Group well created") 48 | } 49 | 50 | user := &models.User{ 51 | FirstName: a.Config.GetString("admin_firstname"), 52 | LastName: a.Config.GetString("admin_lastname"), 53 | Password: a.Config.GetString("admin_password"), 54 | Email: a.Config.GetString("admin_email"), 55 | Phone: a.Config.GetString("admin_phone"), 56 | GroupID: group.ID, 57 | } 58 | 59 | userExists, user, err := models.UserExists(ctx, user.Email) 60 | if userExists { 61 | utils.Log(nil, "warn", `Seed user already exists`) 62 | } else { 63 | utils.Log(nil, "info", "User doesn't exists already") 64 | } 65 | 66 | err = models.CreateUser(ctx, user) 67 | if err != nil { 68 | utils.Log(nil, "warn", `ErrorInternal when creating user:`, err) 69 | user, _ = models.GetUser(ctx, bson.M{"email": a.Config.GetString("admin_email")}) 70 | } else { 71 | utils.Log(nil, "info", "User well created") 72 | } 73 | 74 | err = models.ActivateUser(ctx, user.Key, user.ID) 75 | if err != nil { 76 | utils.Log(nil, "warn", `ErrorInternal when activating user`, err) 77 | } else { 78 | utils.Log(nil, "info", "User well activated") 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // SetupPostgreSeeds creates the first user 85 | func (a *API) SetupPostgreSeeds() error { 86 | utils.Log(nil, "info", "Setup postgre seeds") 87 | s := postgresql.New(&gin.Context{}, a.PostgreDatabase, a.Config.GetString("POSTGRES_DB_NAME")) 88 | //ctx := store.NewGodContext(s) 89 | 90 | organization := &models.Organization{ 91 | Name: a.Config.GetString("project_name"), 92 | } 93 | s.Create(a.Context, "", organization) 94 | 95 | adminGroup := &models.Group{ 96 | Name: a.Config.GetString("project_name") + " Admin", 97 | Role: "god", 98 | OrganizationID: organization.ID, 99 | } 100 | s.Create(a.Context, "", adminGroup) 101 | 102 | adminUser := &models.User{ 103 | FirstName: a.Config.GetString("admin_firstname"), 104 | LastName: a.Config.GetString("admin_lastname"), 105 | Password: a.Config.GetString("admin_password"), 106 | Email: a.Config.GetString("admin_email"), 107 | Phone: a.Config.GetString("admin_phone"), 108 | Status: "activated", 109 | GroupID: adminGroup.ID, 110 | } 111 | s.GetOrCreateUser(adminUser) 112 | /*err := models.ActivateUser(ctx, adminUser.Key, adminUser.ID) 113 | if err != nil { 114 | utils.Log(nil, "warn", `ErrorInternal when activating user`, err) 115 | } else { 116 | utils.Log(nil, "info", "User well activated") 117 | }*/ 118 | 119 | user1 := &models.User{ 120 | FirstName: a.Config.GetString("user1_firstname"), 121 | LastName: a.Config.GetString("user1_lastname"), 122 | Password: a.Config.GetString("user1_password"), 123 | Email: a.Config.GetString("user1_email"), 124 | GroupID: adminGroup.ID, 125 | } 126 | s.GetOrCreateUser(user1) 127 | user2 := &models.User{ 128 | FirstName: a.Config.GetString("user2_firstname"), 129 | LastName: a.Config.GetString("user2_lastname"), 130 | Password: a.Config.GetString("user2_password"), 131 | Email: a.Config.GetString("user2_email"), 132 | GroupID: adminGroup.ID, 133 | } 134 | s.GetOrCreateUser(user2) 135 | 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /server/viper.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "os" 7 | 8 | "github.com/joho/godotenv" 9 | ) 10 | 11 | // SetupViper according to env var that selects related conf file 12 | func (a *API) SetupViper() error { 13 | filename := ".env" 14 | logrus.Infoln("Using env:" + os.Getenv("SAM_ENV")) 15 | switch os.Getenv("SAM_ENV") { 16 | case "testing": 17 | filename = "../.env.testing" 18 | case "prod": 19 | filename = ".env.prod" 20 | } 21 | 22 | err := godotenv.Overload(filename) 23 | if err != nil { 24 | fmt.Println("godotenv error:", err) 25 | } else { 26 | logrus.Infoln("Godotenv OK") 27 | } 28 | 29 | a.Config.SetEnvPrefix("SAM") 30 | a.Config.AutomaticEnv() 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /services/email.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/go-lumen/lumen-api/models" 5 | "github.com/go-lumen/lumen-api/utils" 6 | "github.com/matcornic/hermes/v2" 7 | "github.com/sirupsen/logrus" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/awserr" 12 | "github.com/aws/aws-sdk-go/aws/credentials" 13 | "github.com/aws/aws-sdk-go/aws/session" 14 | "github.com/aws/aws-sdk-go/service/ses" 15 | "github.com/spf13/viper" 16 | "golang.org/x/net/context" 17 | ) 18 | 19 | const ( 20 | emailSenderKey = "emailSender" 21 | ) 22 | 23 | // GetEmailSender retrieves text sender 24 | func GetEmailSender(c context.Context) EmailSender { 25 | return c.Value(emailSenderKey).(EmailSender) 26 | } 27 | 28 | // EmailSender creates a text sender interface 29 | type EmailSender interface { 30 | SendEmail(content string, data *models.EmailData) error 31 | SendActivationEmail(user *models.User, apiURL string, appName string, frontURL string) error 32 | SendResetEmail(user *models.User, apiURL string, appName string, frontURL string) error 33 | } 34 | 35 | // FakeEmailSender structure 36 | type FakeEmailSender struct{} 37 | 38 | // EmailSenderParams with various text sender params 39 | type EmailSenderParams struct { 40 | senderEmail string 41 | senderName string 42 | apiID string 43 | apiKey string 44 | //apiURL string 45 | } 46 | 47 | // NewEmailSender instantiates of the sender 48 | func NewEmailSender(config *viper.Viper) EmailSender { 49 | return &EmailSenderParams{ 50 | config.GetString("mail_sender_address"), 51 | config.GetString("mail_sender_name"), 52 | config.GetString("aws_api_id"), 53 | config.GetString("aws_api_key"), 54 | //config.GetString("api_url"), 55 | } 56 | } 57 | 58 | // SendEmail is used for test purposes 59 | func (f *FakeEmailSender) SendEmail(content string, data *models.EmailData) error { 60 | return nil 61 | } 62 | 63 | // SendActivationEmail is used for test purposes 64 | func (f *FakeEmailSender) SendActivationEmail(user *models.User, apiURL string, appName string, frontURL string) error { 65 | return nil 66 | } 67 | 68 | // SendEmail sends an mail 69 | func (s *EmailSenderParams) SendEmail(content string, data *models.EmailData) error { 70 | sess, err := session.NewSession(&aws.Config{ 71 | Region: aws.String("eu-west-1")}, 72 | ) 73 | 74 | creds := credentials.NewStaticCredentials(s.apiID, s.apiKey, "") 75 | 76 | // Create an SES session. 77 | svc := ses.New(sess, &aws.Config{Credentials: creds}) 78 | 79 | // Assemble the email. 80 | input := &ses.SendEmailInput{ 81 | Destination: &ses.Destination{ 82 | CcAddresses: []*string{}, 83 | ToAddresses: []*string{ 84 | aws.String(data.ReceiverMail), 85 | }, 86 | }, 87 | Message: &ses.Message{ 88 | Body: &ses.Body{ 89 | Html: &ses.Content{ 90 | Charset: aws.String("UTF-8"), 91 | Data: aws.String(content), 92 | }, 93 | }, 94 | Subject: &ses.Content{ 95 | Charset: aws.String("UTF-8"), 96 | Data: aws.String(data.Subject), 97 | }, 98 | }, 99 | Source: aws.String(s.senderEmail), 100 | } 101 | 102 | // Attempt to send the email. 103 | _, err = svc.SendEmail(input) 104 | 105 | // Display error messages if they occur. 106 | if err != nil { 107 | if aerr, ok := err.(awserr.Error); ok { 108 | switch aerr.Code() { 109 | case ses.ErrCodeMessageRejected: 110 | logrus.Warnln(ses.ErrCodeMessageRejected, aerr.Error()) 111 | case ses.ErrCodeMailFromDomainNotVerifiedException: 112 | logrus.Warnln(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error()) 113 | case ses.ErrCodeConfigurationSetDoesNotExistException: 114 | logrus.Warnln(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error()) 115 | default: 116 | logrus.Warnln(aerr.Error()) 117 | } 118 | } else { 119 | logrus.Warnln(err.Error()) 120 | } 121 | 122 | logrus.Warnln(err) 123 | return err 124 | } 125 | 126 | logrus.Infoln("SES Email Sent to " + data.ReceiverName + " at address: " + data.ReceiverMail) 127 | 128 | return nil 129 | } 130 | 131 | // SendActivationEmail allows to send an email to user to activate his account 132 | func (s *EmailSenderParams) SendActivationEmail(user *models.User, apiURL string, appName string, frontURL string) error { 133 | currentYear := string(time.Now().Year()) 134 | 135 | h := hermes.Hermes{ 136 | Theme: new(hermes.Flat), 137 | Product: hermes.Product{ // Appears in header & footer of e-mails 138 | Name: appName, 139 | Link: frontURL, 140 | //Logo: ``, 141 | Copyright: `Copyright © ` + currentYear + ` ` + appName + `. All rights reserved.`, 142 | }, 143 | } 144 | 145 | email := hermes.Email{ 146 | Body: hermes.Body{ 147 | Name: user.FirstName + ` ` + user.LastName, 148 | Intros: []string{ 149 | `Welcome to ` + appName + `! We're very excited to have you on board.`, 150 | }, 151 | Dictionary: []hermes.Entry{ 152 | {Key: "Email", Value: user.Email}, 153 | {Key: "FirstName", Value: user.FirstName}, 154 | {Key: "LastName", Value: user.LastName}, 155 | {Key: "Phone", Value: user.Phone}, 156 | }, 157 | Actions: []hermes.Action{ 158 | { 159 | Instructions: `To get started with ` + appName + `, please click here:`, 160 | Button: hermes.Button{ 161 | Color: `#22BC66`, 162 | TextColor: `#FFFFFF`, 163 | Text: "Confirm your account", 164 | Link: apiURL, 165 | }, 166 | }, 167 | }, 168 | Outros: []string{ 169 | `If you received this mail and it was not intended to you, please ignore it.`, 170 | }, 171 | }, 172 | } 173 | 174 | emailBody, err := h.GenerateHTML(email) 175 | if err != nil { 176 | logrus.Warnln(err) 177 | panic(err) 178 | } 179 | 180 | data := models.EmailData{ReceiverMail: user.Email, ReceiverName: user.FirstName + " " + user.LastName, User: user, Subject: `Welcome to ` + appName + `! We're very excited to have you on board.`, AppName: appName} 181 | 182 | return s.SendEmail(emailBody, &data) 183 | } 184 | 185 | // SendResetEmail allows to send an email to user to reset his password 186 | func (s *EmailSenderParams) SendResetEmail(user *models.User, apiURL string, appName string, frontURL string) error { 187 | currentYear := string(time.Now().Year()) 188 | 189 | h := hermes.Hermes{ 190 | Theme: new(hermes.Flat), 191 | Product: hermes.Product{ // Appears in header & footer of e-mails 192 | Name: appName, 193 | Link: frontURL, 194 | //Logo: ``, 195 | Copyright: `Copyright © ` + currentYear + ` ` + appName + `. All rights reserved.`, 196 | }, 197 | } 198 | 199 | email := hermes.Email{ 200 | Body: hermes.Body{ 201 | Name: user.FirstName + ` ` + user.LastName, 202 | Intros: []string{ 203 | `We received a request to reset your` + appName + `password.`, 204 | `We have found an account with the following details`, 205 | }, 206 | Dictionary: []hermes.Entry{ 207 | {Key: "Email", Value: user.Email}, 208 | {Key: "FirstName", Value: user.FirstName}, 209 | {Key: "LastName", Value: user.LastName}, 210 | {Key: "Phone", Value: user.Phone}, 211 | }, 212 | Actions: []hermes.Action{ 213 | { 214 | Instructions: `If you want to reset your ` + appName + ` password, please click here:`, 215 | Button: hermes.Button{ 216 | Color: `#DC4D2F`, 217 | TextColor: `#FFFFFF`, 218 | Text: "Reset your password", 219 | Link: apiURL, 220 | }, 221 | }, 222 | }, 223 | Outros: []string{ 224 | `If you received this mail and it was not intended to you, please ignore it.`, 225 | }, 226 | }, 227 | } 228 | 229 | emailBody, err := h.GenerateHTML(email) 230 | utils.CheckErr(err) 231 | 232 | data := models.EmailData{ReceiverMail: user.Email, ReceiverName: user.FirstName + " " + user.LastName, User: user, Subject: appName + ` password reset request.`, AppName: appName} 233 | 234 | return s.SendEmail(emailBody, &data) 235 | } 236 | -------------------------------------------------------------------------------- /services/text.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/credentials" 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/aws/aws-sdk-go/service/sns" 8 | "github.com/gin-gonic/gin" 9 | "github.com/go-lumen/lumen-api/models" 10 | "github.com/sirupsen/logrus" 11 | "github.com/spf13/viper" 12 | "golang.org/x/net/context" 13 | ) 14 | 15 | const ( 16 | textSenderKey = "textSender" 17 | ) 18 | 19 | // GetTextSender retrieves text sender 20 | func GetTextSender(c context.Context) TextSender { 21 | return c.Value(textSenderKey).(TextSender) 22 | } 23 | 24 | // TextSender creates a text sender interface 25 | type TextSender interface { 26 | SendAlertText(c *gin.Context, user *models.User, message string, templateLink string) error 27 | SendText(ctx *gin.Context, data models.TextData) error 28 | } 29 | 30 | // FakeTextSender structure 31 | type FakeTextSender struct{} 32 | 33 | // TextSenderParams with various text sender params 34 | type TextSenderParams struct { 35 | senderEmail string 36 | senderName string 37 | apiID string 38 | apiKey string 39 | apiURL string 40 | } 41 | 42 | // NewTextSender instantiates of the sender 43 | func NewTextSender(config *viper.Viper) TextSender { 44 | return &TextSenderParams{ 45 | config.GetString("mail_sender_address"), 46 | config.GetString("mail_sender_name"), 47 | config.GetString("aws_api_id"), 48 | config.GetString("aws_api_key"), 49 | config.GetString("api_url"), 50 | } 51 | } 52 | 53 | // SendAlertText sends a simple alert 54 | func (s *TextSenderParams) SendAlertText(c *gin.Context, user *models.User, message string, templateLink string) error { 55 | data := models.TextData{PhoneNumber: user.Phone, Message: message} 56 | if s.SendText(c, data) != nil { 57 | logrus.Warnln(`Send text error`) 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // SendText sends any type of text 64 | func (s *TextSenderParams) SendText(ctx *gin.Context, data models.TextData) error { 65 | sess, err := session.NewSession(&aws.Config{ 66 | Region: aws.String("eu-west-1")}, 67 | ) 68 | 69 | // logrus.Infoln("Amazon Creds: " + s.apiID + s.apiKey) 70 | creds := credentials.NewStaticCredentials(s.apiID, s.apiKey, "") 71 | 72 | // Creates an SES session. 73 | svc := sns.New(sess, &aws.Config{Credentials: creds}) 74 | 75 | // Assembling the text and attempting to send the email. 76 | params := &sns.PublishInput{ 77 | Subject: aws.String(data.Subject), 78 | Message: aws.String(data.Message), 79 | PhoneNumber: aws.String(data.PhoneNumber), 80 | } 81 | _, err = svc.Publish(params) 82 | 83 | if err != nil { 84 | logrus.Warnln(err.Error()) 85 | return err 86 | } 87 | 88 | // logrus.Infoln("SNS Text Sent to " + data.PhoneNumber) 89 | // logrus.Infoln(resp) 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /store/authorization.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | // RoleChecker defines a standard interface for role checking 4 | type RoleChecker interface { 5 | // CanBeCreated allows to determine if a user can create a device 6 | CanBeCreated(user User, group Group) bool 7 | 8 | // CanBeRead allows to determine if a user can read a device 9 | CanBeRead(user User, group Group) bool 10 | 11 | // CanBeUpdated allows to determine if a user can update a device 12 | CanBeUpdated(user User, group Group) bool 13 | 14 | // CanBeDeleted allows to determine if a user can delete a device 15 | CanBeDeleted(User, Group) bool 16 | } 17 | 18 | // DefaultRoles implements default role for basic models 19 | type DefaultRoles struct{} 20 | 21 | // CanBeCreated allows to determine if a user can create a device 22 | func (dr *DefaultRoles) CanBeCreated(user User, group Group) bool { 23 | switch group.GetRole() { 24 | case RoleGod, RoleAdmin: 25 | return true 26 | default: 27 | return false 28 | } 29 | } 30 | 31 | // CanBeRead allows to determine if a user can read a device 32 | func (dr *DefaultRoles) CanBeRead(user User, group Group) bool { 33 | switch group.GetRole() { 34 | case RoleGod, RoleAdmin, RoleUser, RoleCustomer: 35 | return true 36 | default: 37 | return false 38 | } 39 | } 40 | 41 | // CanBeUpdated allows to determine if a user can update a device 42 | func (dr *DefaultRoles) CanBeUpdated(user User, group Group) bool { 43 | return dr.CanBeCreated(user, group) 44 | } 45 | 46 | // CanBeDeleted allows to determine if a user can delete a device 47 | func (dr *DefaultRoles) CanBeDeleted(user User, group Group) bool { 48 | return dr.CanBeCreated(user, group) 49 | } 50 | -------------------------------------------------------------------------------- /store/base.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/mongo/options" 5 | ) 6 | 7 | // Model represents a generic store model 8 | type Model interface { 9 | //RoleChecker 10 | } 11 | 12 | // GenericModel represents a generic mongo model 13 | type GenericModel interface { 14 | Model 15 | GetCollection() string 16 | } 17 | 18 | // EnsureGenericModel ensures that a model implement GenericModel interface 19 | func EnsureGenericModel(model Model) GenericModel { 20 | genericModel, ok := model.(GenericModel) 21 | if !ok { 22 | panic("when using driver, you should implement MongoModel interface on your model") 23 | } 24 | return genericModel 25 | } 26 | 27 | // MongoFindAllOptioner represents default mongo FindOptions 28 | type MongoFindAllOptioner interface { 29 | ApplyOptions(*options.FindOptions) 30 | } 31 | -------------------------------------------------------------------------------- /store/context.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "golang.org/x/net/context" 6 | ) 7 | 8 | const ( 9 | // CurrentUserKey for user 10 | CurrentUserKey = "currentUser" 11 | // CurrentUserGroupKey for user group 12 | CurrentUserGroupKey = "currentUserGroup" 13 | // StoreKey for storing 14 | StoreKey = "store" 15 | // AppKey for external access 16 | AppKey = "appKey" 17 | ) 18 | 19 | // Setter interface 20 | type Setter interface { 21 | Set(string, interface{}) 22 | } 23 | 24 | // Current allows to retrieve user from context 25 | func Current(c context.Context) *User { 26 | return c.Value(CurrentUserKey).(*User) 27 | } 28 | 29 | // ToContext allows to set a value in store 30 | func ToContext(c Setter, store Store) { 31 | c.Set(StoreKey, store) 32 | } 33 | 34 | // FromContext allows to get store from context 35 | func FromContext(c context.Context) Store { 36 | return c.Value(StoreKey).(Store) 37 | } 38 | 39 | // UserRole represents an user role 40 | type UserRole = string 41 | 42 | const ( 43 | // RoleGod have access to everything 44 | RoleGod UserRole = "god" 45 | // RoleAdmin have access to everything on its group 46 | RoleAdmin = "admin" 47 | // RoleUser have access to everything that was created by himself 48 | RoleUser = "user" 49 | // RoleCustomer have access 50 | RoleCustomer = "customer" 51 | ) 52 | 53 | // User is a generic store user 54 | type User interface { 55 | GetID() string 56 | GetGroupID() string 57 | } 58 | 59 | // Group is a generic store group 60 | type Group interface { 61 | GetID() string 62 | GetRole() UserRole 63 | GetOrgID() UserRole 64 | } 65 | 66 | // Context is an advanced context for authenticated users 67 | type Context struct { 68 | C *gin.Context 69 | Store Store 70 | IsLogged bool 71 | User User 72 | Role UserRole 73 | Group Group 74 | } 75 | 76 | // AuthContext retrieves the authenticated context for current store user 77 | func AuthContext(c *gin.Context) *Context { 78 | storedUser, userExists := c.Get(CurrentUserKey) 79 | storedUserGroup, userGroupExists := c.Get(CurrentUserGroupKey) 80 | 81 | var user User 82 | if userExists { 83 | user = storedUser.(User) 84 | } 85 | 86 | var group Group 87 | role := "" 88 | if userGroupExists { 89 | group = storedUserGroup.(Group) 90 | role = group.GetRole() 91 | } 92 | 93 | return &Context{ 94 | C: c, 95 | Store: FromContext(c), 96 | IsLogged: userExists && userGroupExists, 97 | User: user, 98 | Role: role, 99 | Group: group, 100 | } 101 | } 102 | 103 | // GetCache returns the cached value and true or nil and false if not found (request scoped cache) 104 | func (c *Context) GetCache(key string) (interface{}, bool) { 105 | if cachedValue, found := c.C.Get("store:" + key); found { 106 | return cachedValue, true 107 | } 108 | return nil, false 109 | } 110 | 111 | // SetCache sets a cache value (request scoped cache) 112 | func (c *Context) SetCache(key string, value interface{}) { 113 | c.C.Set("store:"+key, value) 114 | } 115 | 116 | type user struct{} 117 | 118 | func (u user) GetID() string { return "" } 119 | func (u user) GetGroupID() string { return "fline" } 120 | 121 | type group struct{} 122 | 123 | func (g group) GetID() string { return "" } 124 | func (g group) GetRole() UserRole { return RoleGod } 125 | func (g group) GetOrgID() UserRole { return "fline" } 126 | 127 | // NewGodContext creates a god in memory context 128 | func NewGodContext(s Store) *Context { 129 | return &Context{ 130 | C: new(gin.Context), 131 | Store: s, 132 | IsLogged: true, 133 | User: user{}, 134 | Role: RoleGod, 135 | Group: group{}, 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /store/mongodb/generic.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/chidiwilliams/flatbson" 7 | "github.com/go-lumen/lumen-api/store" 8 | "github.com/go-lumen/lumen-api/utils" 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/bson/primitive" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | "go.mongodb.org/mongo-driver/mongo/options" 15 | "reflect" 16 | ) 17 | 18 | const idName = "ID" 19 | 20 | func setID(model store.Model, id string) { 21 | v := reflect.ValueOf(model).Elem().FieldByName(idName) 22 | if v.IsValid() && v.CanSet() { 23 | v.SetString(id) 24 | } 25 | } 26 | 27 | func orderToMongoOrder(order store.SortOrder) int { 28 | if order == store.SortAscending { 29 | return 1 30 | } 31 | return -1 32 | } 33 | 34 | // getCacheKey returns an unique cache key 35 | func getCacheKey(filter bson.M, input interface{}) (string, error) { 36 | data, err := json.Marshal(filter) 37 | if err != nil { 38 | return "", errors.Wrap(err, "cannot marshall filters") 39 | } 40 | cacheKey := reflect.TypeOf(input).String() + ":" + string(data) 41 | return cacheKey, nil 42 | } 43 | 44 | // GetCollection returns mongo collection 45 | func (db *Mngo) GetCollection(c *store.Context, model store.Model) *mongo.Collection { 46 | utils.EnsurePointer(model) 47 | mongoModel := store.EnsureGenericModel(model) 48 | return db.database.Collection(mongoModel.GetCollection()) 49 | } 50 | 51 | // Create a generic model 52 | func (db *Mngo) Create(c *store.Context, collectionName string, model store.Model) error { 53 | utils.EnsurePointer(model) 54 | mongoModel := store.EnsureGenericModel(model) 55 | collection := db.database.Collection(mongoModel.GetCollection()) 56 | 57 | if creator, ok := model.(store.BeforeCreator); ok { 58 | if err := creator.BeforeCreate(); err != nil { 59 | return errors.Wrap(err, "error in BeforeCreate") 60 | } 61 | } 62 | if creator, ok := model.(store.BeforeCreatorWithContext); ok { 63 | if err := creator.BeforeCreate(c); err != nil { 64 | return errors.Wrap(err, "error in BeforeCreatorWithContext") 65 | } 66 | } 67 | 68 | res, err := collection.InsertOne(db.context, model) 69 | if err != nil { 70 | logrus.WithError(err).Errorln("cannot insert model") 71 | return errors.Wrap(err, "cannot insert model") 72 | } 73 | 74 | // update with inserted id 75 | if id, ok := res.InsertedID.(primitive.ObjectID); ok { 76 | setID(model, id.Hex()) 77 | } 78 | if id, ok := res.InsertedID.(string); ok { 79 | setID(model, id) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Find return a generic model 86 | func (db *Mngo) Find(c *store.Context, filter bson.M, model store.Model, opts ...store.FindOption) error { 87 | optValues := store.GetFindOptions(opts...) 88 | utils.EnsurePointer(model) 89 | mongoModel := store.EnsureGenericModel(model) 90 | collection := db.database.Collection(mongoModel.GetCollection()) 91 | 92 | cacheKey, err := getCacheKey(filter, model) 93 | if err != nil { 94 | return errors.Wrap(err, "cannot compute cache key") 95 | } 96 | 97 | if cached, found := c.GetCache(cacheKey); found && optValues.Cache { 98 | model = cached.(store.Model) 99 | return nil 100 | } 101 | 102 | var findOptions options.FindOneOptions 103 | // apply sort 104 | if len(optValues.SortedFields) > 0 { 105 | sortBson := bson.D{} 106 | for _, sortedField := range optValues.SortedFields { 107 | sortBson = append(sortBson, bson.E{Key: sortedField.Field, Value: orderToMongoOrder(sortedField.Order)}) 108 | } 109 | findOptions.SetSort(sortBson) 110 | } 111 | 112 | err = collection.FindOne(db.context, filter, &findOptions).Decode(model) 113 | if err != nil { 114 | return errors.Wrap(err, "cannot find model") 115 | } 116 | 117 | c.SetCache(cacheKey, model) 118 | return err 119 | } 120 | 121 | // FindAll return several generic models 122 | func (db *Mngo) FindAll(c *store.Context, filter bson.M, results interface{}, opts ...store.FindOption) error { 123 | utils.EnsurePointer(results) 124 | optValues := store.GetFindOptions(opts...) 125 | slice := reflect.ValueOf(results).Elem() 126 | modelType := reflect.TypeOf(results).Elem().Elem().Elem() 127 | newModel := reflect.New(modelType) 128 | sliceItem := newModel.Interface().(store.Model) 129 | mongoModel := store.EnsureGenericModel(sliceItem) 130 | collection := db.database.Collection(mongoModel.GetCollection()) 131 | 132 | cacheKey, err := getCacheKey(filter, results) 133 | if err != nil { 134 | return errors.Wrap(err, "cannot compute cache key") 135 | } 136 | 137 | if cached, found := c.GetCache(cacheKey); found && optValues.Cache { 138 | results = cached 139 | return nil 140 | } 141 | 142 | // apply model default options 143 | var findOptions options.FindOptions 144 | if optioner, ok := mongoModel.(store.MongoFindAllOptioner); ok { 145 | optioner.ApplyOptions(&findOptions) 146 | } 147 | // apply limit 148 | if optValues.HasLimit { 149 | findOptions.SetLimit(optValues.Limit) 150 | } 151 | // apply sort 152 | if len(optValues.SortedFields) > 0 { 153 | sortBson := bson.D{} 154 | for _, sortedField := range optValues.SortedFields { 155 | sortBson = append(sortBson, bson.E{Key: sortedField.Field, Value: orderToMongoOrder(sortedField.Order)}) 156 | } 157 | findOptions.SetSort(sortBson) 158 | } 159 | 160 | cur, err := collection.Find(context.TODO(), filter, &findOptions) 161 | if err != nil { 162 | logrus.WithError(err).Errorln("cannot find models") 163 | return errors.Wrap(err, "cannot find models") 164 | } 165 | 166 | for cur.Next(context.TODO()) { 167 | record := reflect.New(modelType) 168 | err = cur.Decode(record.Interface()) 169 | if err != nil { 170 | logrus.Warnln("ErrorInternal on Decoding the document", err) 171 | } 172 | reflect.ValueOf(results).Elem().Set(reflect.Append(slice, record)) 173 | } 174 | 175 | c.SetCache(cacheKey, results) 176 | return err 177 | } 178 | 179 | // Update a generic model 180 | func (db *Mngo) Update(c *store.Context, filter bson.M, model store.Model, opts ...store.UpdateOption) error { 181 | optValues := store.GetUpdateOptions(opts...) 182 | utils.EnsurePointer(model) 183 | mongoModel := store.EnsureGenericModel(model) 184 | collection := db.database.Collection(mongoModel.GetCollection()) 185 | 186 | // flatten field to update for mongodb driver 187 | fields, err := flatbson.Flatten(model) 188 | // filter out fields if OnlyFields is set 189 | if len(optValues.OnlyFields) > 0 { 190 | for key := range fields { 191 | if !utils.FindStringInSlice(key, optValues.OnlyFields) { 192 | delete(fields, key) 193 | } 194 | } 195 | } 196 | 197 | if err != nil { 198 | logrus.WithError(err).Errorln("cannot flatten model") 199 | return errors.Wrap(err, "cannot flatten model") 200 | } 201 | 202 | result, err := collection.UpdateOne(context.TODO(), filter, 203 | bson.M{"$set": fields}, options.Update().SetUpsert(optValues.CreateIfNotExists)) 204 | 205 | if err != nil { 206 | logrus.WithError(err).Errorln("cannot update model") 207 | return errors.Wrap(err, "cannot update model") 208 | } 209 | 210 | if result.MatchedCount != 0 { 211 | logrus.Debugln("matched and replaced an existing document") 212 | return nil 213 | } 214 | if result.UpsertedCount != 0 { 215 | logrus.WithField("id", result.UpsertedID).Debugln("inserted a new document") 216 | return nil 217 | } 218 | 219 | return nil 220 | } 221 | 222 | // Delete a generic model 223 | func (db *Mngo) Delete(c *store.Context, id string, model store.Model) error { 224 | utils.EnsurePointer(model) 225 | mongoModel := store.EnsureGenericModel(model) 226 | collection := db.database.Collection(mongoModel.GetCollection()) 227 | 228 | _, err := collection.DeleteOne(db.context, bson.M{"_id": id}) 229 | if err != nil { 230 | logrus.WithError(err).Errorln("cannot delete model") 231 | return errors.Wrap(err, "cannot delete model") 232 | } 233 | 234 | return nil 235 | } 236 | 237 | // DeleteAll a generic model 238 | func (db *Mngo) DeleteAll(c *store.Context, filter bson.M, model store.Model) (int64, error) { 239 | utils.EnsurePointer(model) 240 | mongoModel := store.EnsureGenericModel(model) 241 | collection := db.database.Collection(mongoModel.GetCollection()) 242 | 243 | res, err := collection.DeleteOne(db.context, filter) 244 | if err != nil { 245 | logrus.WithError(err).Errorln("cannot delete model") 246 | return 0, errors.Wrap(err, "cannot delete model") 247 | } 248 | 249 | return res.DeletedCount, nil 250 | } 251 | -------------------------------------------------------------------------------- /store/mongodb/mongo.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | "go.mongodb.org/mongo-driver/mongo" 6 | ) 7 | 8 | // Mngo struct holds informations about MongoDB database 9 | type Mngo struct { 10 | database *mongo.Database 11 | dbName string 12 | context context.Context 13 | } 14 | 15 | // New creates a database connexion 16 | func New(context context.Context, database *mongo.Database, dbName string) *Mngo { 17 | return &Mngo{database, dbName, context} 18 | } 19 | -------------------------------------------------------------------------------- /store/postgresql/generic.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-lumen/lumen-api/store" 6 | "github.com/go-lumen/lumen-api/utils" 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | ) 12 | 13 | // Create a generic model 14 | func (db *PSQL) Create(c *store.Context, tableName string, model store.Model) error { 15 | utils.EnsurePointer(model) 16 | //store.EnsureGenericModel(model) 17 | 18 | if creator, ok := model.(store.BeforeCreator); ok { 19 | if err := creator.BeforeCreate(); err != nil { 20 | return errors.Wrap(err, "error in BeforeCreate") 21 | } 22 | } 23 | if creator, ok := model.(store.BeforeCreatorWithContext); ok { 24 | if err := creator.BeforeCreate(c); err != nil { 25 | return errors.Wrap(err, "error in BeforeCreatorWithContext") 26 | } 27 | } 28 | 29 | res := db.database.Table(tableName).Create(model) 30 | if res.Error != nil { 31 | logrus.WithError(res.Error).Errorln("cannot insert model") 32 | return errors.Wrap(res.Error, "cannot insert model") 33 | } 34 | 35 | return nil 36 | } 37 | 38 | // Find return a generic model 39 | func (db *PSQL) Find(c *store.Context, filters bson.M, model store.Model, opts ...store.FindOption) error { 40 | utils.EnsurePointer(model) 41 | //store.EnsureGenericModel(model) 42 | 43 | /*var sortQuery, sortValues string 44 | // apply sort 45 | if len(optValues.SortedFields) > 0 { 46 | sortBson := bson.D{} 47 | for i, sortedField := range optValues.SortedFields { 48 | sortedField.Field 49 | } 50 | }*/ 51 | 52 | var filtersQuery string 53 | var filtersValue0 /*, filtersValue1*/ string 54 | var i int 55 | for key, value := range filters { 56 | //filtersValues = append(filtersValues, fmt.Sprint(value)) 57 | if i == 0 { 58 | filtersQuery += key + " = ?" 59 | filtersValue0 = fmt.Sprint(value) 60 | } else { 61 | filtersQuery += " AND " + key + " = ?" 62 | //filtersValue1 = fmt.Sprint(value) 63 | } 64 | i++ 65 | } 66 | db.database.Where(filtersQuery, filtersValue0).First(model) // find product with code D42) 67 | 68 | return nil 69 | } 70 | 71 | // FindAll return several generic models 72 | func (db *PSQL) FindAll(c *store.Context, filters bson.M, results interface{}, opts ...store.FindOption) error { 73 | /*var sortQuery, sortValues string 74 | // apply sort 75 | if len(optValues.SortedFields) > 0 { 76 | sortBson := bson.D{} 77 | for i, sortedField := range optValues.SortedFields { 78 | sortedField.Field 79 | } 80 | }*/ 81 | 82 | var filtersQuery string 83 | var filtersValues []string 84 | var i int 85 | for key, value := range filters { 86 | filtersValues = append(filtersValues, fmt.Sprint(value)) 87 | if i == 0 { 88 | filtersQuery += key + " = ?" 89 | } else { 90 | filtersQuery += " AND " + key + " = ?" 91 | } 92 | i++ 93 | } 94 | if len(filtersQuery) <= 2 { 95 | db.database.Find(results) 96 | } else { 97 | db.database.Where(filtersQuery, filtersValues).Find(results) 98 | } 99 | 100 | return nil 101 | } 102 | 103 | // Update a generic model 104 | func (db *PSQL) Update(c *store.Context, filters bson.M, model store.Model, opts ...store.UpdateOption) error { 105 | utils.EnsurePointer(model) 106 | //store.EnsureGenericModel(model) 107 | 108 | var filtersQuery string 109 | var filtersValues []string 110 | var i int 111 | for key, value := range filters { 112 | filtersValues = append(filtersValues, fmt.Sprint(value)) 113 | if i == 0 { 114 | filtersQuery += key + " = ?" 115 | } else { 116 | filtersQuery += " AND " + key + " = ?" 117 | } 118 | i++ 119 | } 120 | if len(filtersQuery) <= 2 { 121 | return errors.New("Missing filter to update") 122 | } else { 123 | db.database.Model(&model).Where(filtersQuery, filtersValues).Updates(model) 124 | } 125 | 126 | return nil 127 | } 128 | 129 | // Delete a generic model 130 | func (db *PSQL) Delete(c *store.Context, id string, model store.Model) error { 131 | utils.EnsurePointer(model) 132 | //store.EnsureGenericModel(model) 133 | 134 | db.database.Delete(&model) 135 | 136 | return nil 137 | } 138 | 139 | // DeleteAll a generic model 140 | func (db *PSQL) DeleteAll(c *store.Context, filter bson.M, model store.Model) (int64, error) { 141 | utils.EnsurePointer(model) 142 | //store.EnsureGenericModel(model) 143 | 144 | db.database.Delete(&model) 145 | 146 | return 0, nil //res.DeletedCount, nil 147 | } 148 | 149 | func (db *PSQL) GetCollection(c *store.Context, model store.Model) *mongo.Collection { 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /store/postgresql/group.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "github.com/go-lumen/lumen-api/helpers/params" 5 | "github.com/go-lumen/lumen-api/models" 6 | ) 7 | 8 | // CreateGroup checks if group already exists, and if not, creates it 9 | func (db *PSQL) CreateGroup(group *models.Group) error { 10 | return nil 11 | } 12 | 13 | // GetGroupByID allows to retrieve a group by its id 14 | func (db *PSQL) GetGroupByID(id string) (*models.Group, error) { 15 | return nil, nil 16 | } 17 | 18 | // GetGroup allows to retrieve a group by its characteristics 19 | func (db *PSQL) GetGroup(params params.M) (*models.Group, error) { 20 | return nil, nil 21 | } 22 | 23 | // UpdateGroup allows to update one or more group characteristics 24 | func (db *PSQL) UpdateGroup(groupID string, params params.M) error { 25 | return nil 26 | } 27 | 28 | // DeleteGroup allows to delete a group by its id 29 | func (db *PSQL) DeleteGroup(groupID string) error { 30 | return nil 31 | } 32 | 33 | // ChangeGroupOrganization allows to change the organization of a group by its id 34 | func (db *PSQL) ChangeGroupOrganization(groupID string, organizationID string) error { 35 | return nil 36 | } 37 | 38 | // GetGroups allows to get all groups 39 | func (db *PSQL) GetGroups() ([]*models.Group, error) { 40 | return nil, nil 41 | } 42 | 43 | // CountGroups allows to count all groups 44 | func (db *PSQL) CountGroups() (int, error) { 45 | return 0, nil 46 | } 47 | 48 | // GroupExists allows to know if a group exists through his mail 49 | func (db *PSQL) GroupExists(groupEmail string) (bool, error) { 50 | return false, nil 51 | } 52 | -------------------------------------------------------------------------------- /store/postgresql/organization.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "github.com/go-lumen/lumen-api/helpers/params" 5 | "github.com/go-lumen/lumen-api/models" 6 | ) 7 | 8 | // CreateOrganization checks if organization already exists, and if not, creates it 9 | func (db *PSQL) CreateOrganization(organization *models.Organization) error { 10 | return nil 11 | } 12 | 13 | // GetOrganizationByID allows to retrieve a organization by its id 14 | func (db *PSQL) GetOrganizationByID(id string) (*models.Organization, error) { 15 | return nil, nil 16 | } 17 | 18 | // GetOrganization allows to retrieve a organization by its characteristics 19 | func (db *PSQL) GetOrganization(params params.M) (*models.Organization, error) { 20 | return nil, nil 21 | } 22 | 23 | // UpdateOrganization allows to update one or more organization characteristics 24 | func (db *PSQL) UpdateOrganization(organizationID string, params params.M) error { 25 | return nil 26 | } 27 | 28 | // DeleteOrganization allows to delete a organization by its id 29 | func (db *PSQL) DeleteOrganization(organizationID string) error { 30 | return nil 31 | } 32 | 33 | // IsOrganizationParent allows to know if an organization is a parent, and retrieve its parent if not 34 | func (db *PSQL) IsOrganizationParent(organizationID string) (bool, string, error) { 35 | return false, "", nil 36 | } 37 | 38 | // ChangeParent allows to change an organization parent by its id 39 | func (db *PSQL) ChangeParent(organizationID string, parentID string) error { 40 | return nil 41 | } 42 | 43 | // GetOrganizations allows to get all organizations 44 | func (db *PSQL) GetOrganizations() ([]*models.Organization, error) { 45 | return nil, nil 46 | } 47 | 48 | // CountOrganizations allows to count all organizations 49 | func (db *PSQL) CountOrganizations() (int, error) { 50 | return 0, nil 51 | } 52 | 53 | // OrganizationExists allows to know if a organization exists through his mail 54 | func (db *PSQL) OrganizationExists(organizationEmail string) (bool, error) { 55 | return true, nil 56 | } 57 | -------------------------------------------------------------------------------- /store/postgresql/postgre.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "context" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type PSQL struct { 9 | database *gorm.DB 10 | dbName string 11 | context context.Context 12 | } 13 | 14 | // New creates a database connexion 15 | func New(context context.Context, database *gorm.DB, dbName string) *PSQL { 16 | return &PSQL{database, dbName, context} 17 | } 18 | -------------------------------------------------------------------------------- /store/postgresql/user.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-lumen/lumen-api/helpers" 6 | "github.com/go-lumen/lumen-api/helpers/params" 7 | "github.com/go-lumen/lumen-api/models" 8 | "github.com/go-lumen/lumen-api/utils" 9 | "net/http" 10 | ) 11 | 12 | // CreateUser checks if user already exists, and if not, creates it 13 | func (db *PSQL) CreateUser(user *models.User) error { 14 | var count int64 15 | if err := db.database.Model(user).Where("email = ?", user.Email).Count(&count).Error; err != nil || count > 0 { 16 | return helpers.NewError(http.StatusBadRequest, "user_exists", "the user already exists", err) 17 | } 18 | 19 | if err := db.database.Create(user).Error; err != nil { 20 | return helpers.NewError(http.StatusInternalServerError, "user_creation_failed", "could not create the user", err) 21 | } 22 | 23 | return nil 24 | } 25 | 26 | // GetUserByID allows to retrieve a user by its id 27 | func (db *PSQL) GetUserByID(id string) (*models.User, error) { 28 | var user models.User 29 | if err := db.database.Where("id = ?", id).First(&user).Error; err != nil { 30 | return nil, helpers.NewError(http.StatusNotFound, "user_not_found", "could not find the user", err) 31 | } 32 | return &user, nil 33 | } 34 | 35 | // GetUser allows to retrieve a user by its characteristics 36 | func (db *PSQL) GetUser(params params.M) (*models.User, error) { 37 | session := db.database 38 | 39 | var user models.User 40 | for key, value := range params { 41 | session = session.Where(key+" = ?", value) 42 | } 43 | 44 | if err := session.First(&user).Error; err != nil { 45 | return nil, helpers.NewError(http.StatusNotFound, "user_not_found", "could not find the user", err) 46 | } 47 | 48 | return &user, nil 49 | } 50 | 51 | func (db *PSQL) GetOrCreateUser(user *models.User) (*models.User, error) { 52 | if err := db.database.Where("email = ?", user.Email).First(&user).Error; err != nil { 53 | utils.Log(nil, "warn", `User already exists`, err) 54 | dbUser, err := db.GetUser(params.M{"email": user.Email}) 55 | if err != nil { 56 | utils.Log(nil, "warn", err) 57 | } else { 58 | dbUser.FirstName = user.FirstName 59 | dbUser.LastName = user.LastName 60 | dbUser.Password = user.Password 61 | dbUser.Email = user.Email 62 | dbUser.Phone = user.Phone 63 | if err := db.ActivateUser(dbUser.Key /*strconv.Itoa(dbUser.ID)*/, dbUser.Email); err != nil { 64 | utils.Log(nil, "warn", `Error when activating user`, err) 65 | } 66 | dbUser.BeforeCreate(db.database, true, true, true) 67 | db.UpdateUser(dbUser.ID, dbUser) 68 | fmt.Println("Found user", dbUser.ID, ":", dbUser) 69 | } 70 | } 71 | user.BeforeCreate(db.database, false, false, false) 72 | return user, db.CreateUser(user) 73 | } 74 | 75 | // DeleteUser allows to delete a user by its id 76 | func (db *PSQL) DeleteUser(userID string) error { 77 | return nil 78 | } 79 | 80 | // ActivateUser allows to activate a user by its id 81 | func (db *PSQL) ActivateUser(activationKey string, email string) error { 82 | var user models.User 83 | fmt.Println("Trying to find user:", email, "with activationKey:", activationKey) 84 | if err := db.database.Where("email = ?", email).First(&user).Error; err != nil { 85 | return helpers.NewError(http.StatusNotFound, "user_not_found", "could not find the user", err) 86 | } 87 | 88 | if user.Key != activationKey { 89 | return helpers.NewError(http.StatusBadRequest, "invalid_validation_code", "the provided activation code is invalid", nil) 90 | } 91 | 92 | if err := db.database.Model(&user).Update("status", "activated").Error; err != nil { 93 | return helpers.NewError(http.StatusInternalServerError, "update_user_failed", "could not update the user", err) 94 | } 95 | fmt.Println("Final user:", user) 96 | 97 | return nil 98 | } 99 | 100 | // ChangeLanguage allows to change a user language by its id 101 | func (db *PSQL) ChangeLanguage(id string, language string) error { 102 | return nil 103 | } 104 | 105 | // UpdateUser allows to update one or more user characteristics 106 | func (db *PSQL) UpdateUser(userID string, user *models.User) error { 107 | if err := db.database.Model(user).Updates(&user).Error; err != nil { 108 | return helpers.NewError(http.StatusInternalServerError, "update_user_failed", "could not update the user", err) 109 | } 110 | return nil 111 | } 112 | 113 | // GetUsers allows to get all users 114 | func (db *PSQL) GetUsers() ([]*models.User, error) { 115 | var users []*models.User 116 | db.database.Find(&users) 117 | 118 | return users, nil 119 | } 120 | 121 | // CountUsers allows to count all users 122 | func (db *PSQL) CountUsers() (int, error) { 123 | return 0, nil 124 | } 125 | 126 | // UserExists allows to know if a user exists through his mail 127 | func (db *PSQL) UserExists(userEmail string) (bool, error) { 128 | var user models.User 129 | if err := db.database.Where("email = ?", userEmail).First(&user).Error; err == nil { 130 | return true, nil 131 | } 132 | return false, nil 133 | } 134 | -------------------------------------------------------------------------------- /store/signals.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | // BeforeCreator add a BeforeCreate callback 4 | type BeforeCreator interface { 5 | BeforeCreate() error 6 | } 7 | 8 | // BeforeCreatorWithContext add a BeforeCreate callback with context 9 | type BeforeCreatorWithContext interface { 10 | BeforeCreate(ctx *Context) error 11 | } 12 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson" 5 | "go.mongodb.org/mongo-driver/mongo" 6 | ) 7 | 8 | // SortOrder represents a sort direction 9 | type SortOrder string 10 | 11 | const ( 12 | // SortAscending for sorting in ascending order 13 | SortAscending SortOrder = "ascending" 14 | // SortDescending for sorting in ascending order 15 | SortDescending SortOrder = "descending" 16 | ) 17 | 18 | // SortOption represents a sorted field 19 | type SortOption struct { 20 | Field string 21 | Order SortOrder 22 | } 23 | 24 | // FindOptions represents store common optional parameters 25 | type FindOptions struct { 26 | Cache bool 27 | Limit int64 28 | HasLimit bool 29 | SortedFields []SortOption 30 | } 31 | 32 | // FindOption is an updater function used to update FindOptions 33 | type FindOption func(uo *FindOptions) 34 | 35 | // WithCache is used to set the Cache flag 36 | func WithCache(value bool) FindOption { 37 | return func(uo *FindOptions) { 38 | uo.Cache = value 39 | } 40 | } 41 | 42 | // WithLimit is used to set the Limit flag 43 | func WithLimit(value int64) FindOption { 44 | return func(uo *FindOptions) { 45 | uo.Limit = value 46 | uo.HasLimit = true 47 | } 48 | } 49 | 50 | // WithSort is used to add a SortedFields 51 | func WithSort(field string, order SortOrder) FindOption { 52 | return func(uo *FindOptions) { 53 | uo.SortedFields = append(uo.SortedFields, SortOption{ 54 | Field: field, 55 | Order: order, 56 | }) 57 | } 58 | } 59 | 60 | // GetFindOptions retrieves common options from varargs 61 | func GetFindOptions(opts ...FindOption) FindOptions { 62 | defaults := FindOptions{ 63 | Cache: false, 64 | } 65 | for _, opt := range opts { 66 | opt(&defaults) 67 | } 68 | return defaults 69 | } 70 | 71 | // UpdateOptions represents store update optional parameters 72 | type UpdateOptions struct { 73 | CreateIfNotExists bool 74 | OnlyFields []string // update only these fields, default all field except omitempty ones 75 | } 76 | 77 | // UpdateOption is an updater function used to update UpdateOptions 78 | type UpdateOption func(uo *UpdateOptions) 79 | 80 | // CreateIfNotExists is used to set the CreateIfNotExists flag 81 | func CreateIfNotExists(value bool) UpdateOption { 82 | return func(uo *UpdateOptions) { 83 | uo.CreateIfNotExists = value 84 | } 85 | } 86 | 87 | // OnlyFields is used to set the OnlyFields flag 88 | func OnlyFields(value []string) UpdateOption { 89 | return func(uo *UpdateOptions) { 90 | uo.OnlyFields = value 91 | } 92 | } 93 | 94 | // GetUpdateOptions retrieves options from varargs 95 | func GetUpdateOptions(opts ...UpdateOption) UpdateOptions { 96 | defaults := UpdateOptions{ 97 | CreateIfNotExists: false, 98 | OnlyFields: []string{}, 99 | } 100 | for _, opt := range opts { 101 | opt(&defaults) 102 | } 103 | return defaults 104 | } 105 | 106 | // ID is a shortcut for creating an id filter 107 | func ID(id string) bson.M { 108 | return bson.M{"id": id} 109 | } 110 | 111 | // Store interface 112 | type Store interface { 113 | Create(*Context, string, Model) error 114 | Find(*Context, bson.M, Model, ...FindOption) error 115 | FindAll(*Context, bson.M, interface{}, ...FindOption) error 116 | Update(*Context, bson.M, Model, ...UpdateOption) error 117 | Delete(*Context, string, Model) error 118 | DeleteAll(*Context, bson.M, Model) (int64, error) 119 | GetCollection(*Context, Model) *mongo.Collection 120 | } 121 | -------------------------------------------------------------------------------- /templates/html/page_account_activated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{.Subject}} 9 | 10 | 17 | 18 | 19 | 20 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 129 | 130 | 174 | 175 | 176 | 177 |
178 | 179 |
180 | {{.AppName}} - Account mail validated 181 |
182 | 183 | 184 | 260 |
261 | 262 | -------------------------------------------------------------------------------- /templates/html/page_define_password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Define password 6 | 7 | 8 | 9 | 10 | 137 | 138 | 139 |
140 |
141 |
142 | 158 |
159 |
160 |
161 | 162 | -------------------------------------------------------------------------------- /tests/auth_controller_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAuth(t *testing.T) { 11 | parameters := []byte(` 12 | { 13 | "email":"adrien@plugblocks.com", 14 | "password":"adminpwd" 15 | }`) 16 | 17 | resp := SendRequest(parameters, "POST", "/v1/auth/") 18 | assert.Equal(t, http.StatusOK, resp.Code) 19 | } 20 | 21 | func TestLogOut(t *testing.T) { 22 | defer ResetDatabase() 23 | 24 | resp := SendRequestWithToken(nil, "GET", "/v1/auth/logout", authToken) 25 | assert.Equal(t, http.StatusOK, resp.Code) 26 | 27 | resp = SendRequestWithToken(nil, "GET", "/v1/users/"+user.ID, authToken) 28 | assert.Equal(t, http.StatusUnauthorized, resp.Code) 29 | } 30 | -------------------------------------------------------------------------------- /tests/helpers_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | "net/http/httptest" 8 | 9 | "github.com/go-lumen/lumen-api/models" 10 | "github.com/go-lumen/lumen-api/server" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | func SendRequest(parameters []byte, method string, url string) *httptest.ResponseRecorder { 15 | req, _ := http.NewRequest(method, url, bytes.NewBuffer(parameters)) 16 | req.Header.Add("Content-Type", "application/json") 17 | resp := httptest.NewRecorder() 18 | api.Router.ServeHTTP(resp, req) 19 | return resp 20 | } 21 | 22 | func SendRequestWithToken(parameters []byte, method string, url string, authToken string) *httptest.ResponseRecorder { 23 | req, _ := http.NewRequest(method, url, bytes.NewBuffer(parameters)) 24 | req.Header.Add("Content-Type", "application/json") 25 | req.Header.Add("Authorization", "Bearer "+authToken) 26 | resp := httptest.NewRecorder() 27 | api.Router.ServeHTTP(resp, req) 28 | return resp 29 | } 30 | 31 | func CreateUserAndGenerateToken() (*models.User, string) { 32 | /*users := api.SetupMongoDatabase.C(models.UsersCollection) 33 | 34 | user := models.User{ 35 | ID: bson.NewObjectId().Hex(), 36 | Email: "adrien@plugblocks.com", 37 | FirstName: "Adrien", 38 | LastName: "Chapelet", 39 | Password: "adminpwd", 40 | Active: true, 41 | Admin: true, 42 | } 43 | 44 | hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) 45 | user.Password = string(hashedPassword) 46 | 47 | users.Insert(user) 48 | 49 | privateKeyFile, _ := ioutil.ReadFile(api.Config.GetString("rsa_private")) 50 | privateKey, _ := jwt.ParseRSAPrivateKeyFromPEM(privateKeyFile) 51 | 52 | token := jwt.New(jwt.GetSigningMethod(jwt.SigningMethodRS256.Alg())) 53 | 54 | claims := make(jwt.MapClaims) 55 | // TODO: ADD EXPIRATION 56 | //claims["exp"] = time.Now().Add(time.Hour * time.Duration(settings.Get().JWTExpirationDelta)).Unix() 57 | claims["iat"] = time.Now().Unix() 58 | claims["id"] = user.ID 59 | 60 | token.Claims = claims 61 | 62 | tokenString, _ := token.SignedString(privateKey) 63 | 64 | return &user, tokenString*/ 65 | return nil, "" 66 | } 67 | 68 | func ResetDatabase() { 69 | //api.MongoDatabase.DropDatabase() 70 | user, authToken = CreateUserAndGenerateToken() 71 | } 72 | 73 | func SetupApi() *server.API { 74 | api := &server.API{Router: gin.Default(), Config: viper.New()} 75 | 76 | err := api.SetupViper() 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | _, err = api.SetupMongoDatabase() 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | /*api.MongoDatabase.DropDatabase() 87 | 88 | err = api.SetupIndexes() 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | api.EmailSender = &services.FakeEmailSender{}*/ 94 | api.SetupRouter() 95 | 96 | return api 97 | } 98 | -------------------------------------------------------------------------------- /tests/main_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/go-lumen/lumen-api/models" 8 | "github.com/go-lumen/lumen-api/server" 9 | ) 10 | 11 | var api *server.API 12 | var user *models.User 13 | var authToken string 14 | 15 | func TestMain(m *testing.M) { 16 | api = SetupApi() 17 | user, authToken = CreateUserAndGenerateToken() 18 | retCode := m.Run() 19 | //api.Database.Session.Close() 20 | os.Exit(retCode) 21 | } 22 | -------------------------------------------------------------------------------- /tests/router_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestHomePage(t *testing.T) { 13 | req, err := http.NewRequest("GET", "/v1/", nil) 14 | if err != nil { 15 | fmt.Println(err) 16 | } 17 | 18 | resp := httptest.NewRecorder() 19 | api.Router.ServeHTTP(resp, req) 20 | assert.Equal(t, resp.Code, 200) 21 | } 22 | -------------------------------------------------------------------------------- /tests/user_controller_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/globalsign/mgo/bson" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCreateAccount(t *testing.T) { 12 | //Missing field 13 | parameters := []byte(` 14 | { 15 | "password":"adminpwd", 16 | "first_name":"Adrien", 17 | "last_name": "Chapelet" 18 | }`) 19 | resp := SendRequest(parameters, "POST", "/v1/users/") 20 | assert.Equal(t, http.StatusBadRequest, resp.Code) 21 | 22 | //Everything is fine 23 | parameters = []byte(` 24 | { 25 | "email":"adrien@plugblocks.com", 26 | "password":"adminpwd", 27 | "first_name":"Adrien", 28 | "last_name": "Chapelet" 29 | }`) 30 | resp = SendRequest(parameters, "POST", "/v1/users/") 31 | assert.Equal(t, http.StatusCreated, resp.Code) 32 | 33 | // User already exists 34 | resp = SendRequest(parameters, "POST", "/v1/users/") 35 | assert.Equal(t, http.StatusConflict, resp.Code) 36 | 37 | // Duplicate email 38 | parameters = []byte(` 39 | { 40 | "email":"aDrIeN@plugblocks.com", 41 | "password":"adminpwd", 42 | "first_name":"Adrien", 43 | "last_name": "Chapelet" 44 | }`) 45 | resp = SendRequest(parameters, "POST", "/v1/users/") 46 | assert.Equal(t, http.StatusConflict, resp.Code) 47 | 48 | // Test activation 49 | /*user := models.User{} 50 | err := api.MongoDatabase.C(models.UsersCollection).Find(bson.M{"email": "adrien@plugblocks.com"}).One(&user) 51 | if err != nil { 52 | t.Fail() 53 | return 54 | } 55 | 56 | assert.Equal(t, user.Active, false) 57 | resp = SendRequest(nil, "GET", "/v1/users/"+user.ID+"/activate/"+user.ActivationKey) 58 | 59 | //Update user information 60 | err = api.MongoDatabase.C(models.UsersCollection).Find(bson.M{"email": "adrien@plugblocks.com"}).One(&user) 61 | if err != nil { 62 | t.Fail() 63 | return 64 | } 65 | 66 | assert.Equal(t, http.StatusOK, resp.Code) 67 | assert.Equal(t, user.Active, true)*/ 68 | 69 | //Activation key isn't right 70 | resp = SendRequest(nil, "GET", "/v1/users/"+user.ID+"/activate/fakeKey") 71 | assert.Equal(t, http.StatusInternalServerError, resp.Code) 72 | 73 | //Unknown user 74 | resp = SendRequest(nil, "GET", "/v1/users/"+bson.NewObjectId().Hex()+"/activate/fakeKey") 75 | assert.Equal(t, http.StatusInternalServerError, resp.Code) 76 | } 77 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker stop lumen-api 3 | docker rm lumen-api 4 | docker volume prune 5 | docker rmi api-things-img:latest 6 | 7 | docker build -t api-things-img . 8 | docker run --name lumen-api -p 127.0.0.1:4000:4000 --link things-mongo:mongo -d api-things-img -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "github.com/go-lumen/lumen-api/config" 6 | "github.com/sirupsen/logrus" 7 | "github.com/snwfdhmp/errlog" 8 | "reflect" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // CheckErr checks error and print it if it exists 14 | func CheckErr(e error) bool { 15 | if e != nil { 16 | errlog.Debug(e) 17 | return true 18 | } 19 | return false 20 | } 21 | 22 | // Log logs if debug env var is set at true 23 | func Log(ctxt context.Context, level string, msg ...interface{}) { 24 | if ctxt == nil || config.GetBool(ctxt, "debug") { 25 | switch level { 26 | case "info": 27 | logrus.Infoln(msg...) 28 | case "warn": 29 | logrus.Warnln(msg...) 30 | case "error": 31 | logrus.Errorln(msg...) 32 | default: 33 | logrus.Infoln(msg...) 34 | } 35 | } 36 | } 37 | 38 | // RemoveStringFromSlice allows to remove a string in a slice 39 | func RemoveStringFromSlice(s []string, r string) []string { 40 | for i, v := range s { 41 | if v == r { 42 | return append(s[:i], s[i+1:]...) 43 | } 44 | } 45 | return s 46 | } 47 | 48 | // FindStringInSlice allows to find a string in a slice 49 | func FindStringInSlice(val string, slice []string) (isFound bool) { 50 | for _, item := range slice { 51 | if item == val { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | // FindIntInSlice allows to find an int in a slice 59 | func FindIntInSlice(val int64, slice []int64) (isFound bool) { 60 | for _, item := range slice { 61 | if item == val { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | // MongoIDToTimestamp allows extracting timestamp from Mongo Object ID 69 | func MongoIDToTimestamp(mongoID string) (ret time.Time) { 70 | ts, _ := strconv.ParseInt(mongoID[0:8], 16, 32) 71 | return time.Unix(ts, 0) 72 | } 73 | 74 | // FormatDate allows to format a date 75 | func FormatDate(timestamp int64, format string) (ret string) { 76 | return time.Unix(timestamp, 0).Format(format) 77 | } 78 | 79 | // GenerateTimestampArray allows to generate a Timestamp array 80 | func GenerateTimestampArray(startTS, endTS int64) (tsArray []int64) { 81 | daysNbr := (endTS - startTS) / (24 * 3600) 82 | var i int64 83 | for i = 0; i <= daysNbr; i++ { 84 | tsArray = append(tsArray, startTS+(i*24*3600)) 85 | } 86 | return tsArray 87 | } 88 | 89 | // EnsurePointer ensures that an interface{} is passed as a pointer. Panic if it is not a pointer. 90 | func EnsurePointer(obj interface{}) { 91 | if reflect.ValueOf(obj).Kind() != reflect.Ptr { 92 | panic("the argument must be passed as a pointer") 93 | } 94 | } 95 | 96 | // EnforcePointer takes an object and enforce a pointer. Returning a pointer if it is not the case. 97 | func EnforcePointer(obj interface{}) interface{} { 98 | if reflect.ValueOf(obj).Kind() != reflect.Ptr { 99 | ptr := reflect.New(reflect.TypeOf(obj)) 100 | ptr.Elem().Set(reflect.ValueOf(obj)) 101 | return ptr.Interface() 102 | } 103 | return obj 104 | } 105 | 106 | // Typ returns object type. 107 | func Typ(any interface{}) reflect.Type { 108 | return reflect.TypeOf(EnforcePointer(any)).Elem() 109 | } 110 | 111 | // ValidateInputField allows returning correct value from an input 112 | func ValidateInputField(oldValue, newValue string) string { 113 | if newValue == "undefined" || newValue == "" || newValue == " " { 114 | return oldValue 115 | } 116 | return newValue 117 | } 118 | 119 | // CalcBusinessDays allows computing number of business days between 2 dates 120 | func CalcBusinessDays(from, to time.Time) int { 121 | totalDays := float32(to.Sub(from) / (24 * time.Hour)) 122 | weekDays := float32(from.Weekday()) - float32(to.Weekday()) 123 | businessDays := int(1 + (totalDays*5-weekDays*2)/7) 124 | if from.Weekday() == time.Saturday || from.Weekday() == time.Sunday || to.Weekday() == time.Saturday || to.Weekday() == time.Sunday { 125 | businessDays-- 126 | } 127 | 128 | return businessDays 129 | } 130 | --------------------------------------------------------------------------------