Compare commits

..

2 commits

Author SHA1 Message Date
5d67da6b1c first commit 2024-09-15 13:36:50 -05:00
968575e907 add gitignore 2024-09-15 13:36:17 -05:00
33 changed files with 1326 additions and 0 deletions

80
.env-example Normal file
View file

@ -0,0 +1,80 @@
#######################################
###### App ######
#######################################
APP_NAME=GoCondor
APP_ENV=local # local | testing | production
APP_DEBUG_MODE=true
App_HTTP_HOST=localhost
App_HTTP_PORT=80
App_USE_HTTPS=false
App_USE_LETSENCRYPT=false
APP_LETSENCRYPT_EMAIL=mail@example.com
App_HTTPS_HOSTS=example.com, www.example.com
App_REDIRECT_HTTP_TO_HTTPS=false
App_CERT_FILE_PATH=tls/server.crt
App_KEY_FILE_PATH=tls/server.key
#######################################
###### JWT ######
#######################################
JWT_SECRET=dkfTgonmgaAdlgkw
JWT_LIFESPAN_MINUTES=10080 # expires after 7 days
#######################################
###### DATABASE ######
#######################################
DB_DRIVER=mysql # mysql | postgres | sqlite
#_____ MYSQL _____#
MYSQL_HOST=db-host-here
MYSQL_DB_NAME=db-name-here
MYSQL_PORT=3306
MYSQL_USERNAME=db-user-here
MYSQL_PASSWORD=db-password-here
MYSQL_CHARSET=utf8mb4
#_____ postgres _____#
POSTGRES_HOST=localhost
POSTGRES_USER=user
POSTGRES_PASSWORD=secret
POSTGRES_DB_NAME=db_test
POSTGRES_PORT=5432
POSTGRES_SSL_MODE=disable
POSTGRES_TIMEZONE=Asia/Dubai
#_____ SQLITE _____#
SQLITE_DB_PATH=storage/sqlite/db.sqlite
#######################################
###### CACHE ######
#######################################
CACHE_DRIVER=redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
#######################################
###### Emails ######
#######################################
EMAILS_DRIVER=smtp # smtp | sparkpost | sendgrid | mailgun
#_____ SMTP _____#
SMTP_HOST=
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_SKIP_VERIFY_HOST=true # (set true for development only!)
#_____ sparkpost _____#
SPARKPOST_BASE_URL=https://api.sparkpost.com
SPARKPOST_API_VERSION=1
SPARKPOST_API_KEY=sparkpost-api-key-here # the api key
#_____ sendgrid _____#
SENDGRID_HOST=https://api.sendgrid.com
SENDGRID_ENDPOINT=/v3/mail/send
SENDGRID_API_KEY=sendgrid-api-key-here # the api key
#_____ mailgun _____#
MAILGUN_DOMAIN=your-domain.com # your domain
MAILGUN_API_KEY=mailgun-api-key-here # the api key
MAILGUN_TLS_SKIP_VERIFY_HOST=true # (set true for development only!)

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.air.toml
tmp/*
logs/*
!logs/.gitkeep
tls/*
!tls/.gitkeep
.DS_Store

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021 Harran Ali <harran.m@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,20 @@
## Example Todo App using Goffee framework
This repository contains the code for an example todo app using [Goffee framework](https://git.smarteching.com/goffee)
### How to run locally?
1- Clone the repository
2- Next add your database credentials (mysql) to the `.env` file
3- `cd` into the project dir and run `go mod tidy` to install any missing dependency
4- Run the app using Goffee's cli tool [Goffee](https://git.smarteching.com/goffee/goffee)
```bash
goffee run:dev
```
if [Goffee](https://git.smarteching.com/goffee/goffee) is not installed you can install it by executing the following command
```bash
go install git.smarteching.com/goffee/goffee@latest
```
All routers are defined in the file `routes.go`
All request controllers are defined in the directory `controllers/`

20
config/cache.go Normal file
View file

@ -0,0 +1,20 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package config
import "git.smarteching.com/goffee/core"
// Retrieve the main config for the cache
func GetCacheConfig() core.CacheConfig {
//#####################################
//# Main configuration for cache #####
//#####################################
return core.CacheConfig{
// For enabling and disabling the cache
// set to true to enable it, set to false to disable
EnableCache: true,
}
}

22
config/dotenvfile.go Normal file
View file

@ -0,0 +1,22 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package config
import "git.smarteching.com/goffee/core"
// Retrieve the main config for controlling the .env file
func GetEnvFileConfig() core.EnvFileConfig {
//#########################################################
//# Main configuration for controlling the .env file #####
//#########################################################
return core.EnvFileConfig{
// Set to true to read the environment variables from the .env file and then
// inject them into the os environment, please keep in mind this will override any
// variables previously set in the os envrionment
// set to false to ignore the .env file
UseDotEnvFile: true,
}
}

20
config/gorm.go Normal file
View file

@ -0,0 +1,20 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package config
import "git.smarteching.com/goffee/core"
// Retrieve the main config for the GORM
func GetGormConfig() core.GormConfig {
//#####################################
//# Main configuration for GORM #####
//#####################################
return core.GormConfig{
// For enabling and disabling the GORM
// set to true to enable it, set to false to disable
EnableGorm: true,
}
}

19
config/request.go Normal file
View file

@ -0,0 +1,19 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package config
import "git.smarteching.com/goffee/core"
// Retrieve the main config for the HTTP request
func GetRequestConfig() core.RequestConfig {
//#####################################
//# Main configuration for gorm #####
//#####################################
return core.RequestConfig{
// Set the max file upload size
MaxUploadFileSize: 20000000, // ~20MB
}
}

0
events/.gitkeep Normal file
View file

6
events/event-names.go Normal file
View file

@ -0,0 +1,6 @@
package events
// event names
const USER_REGISTERED = "user-registered"
const USER_PASSWORD_RESET_REQUESTED = "user-password-reset-requested"
const PASSWORD_CHANGED = "password-changed"

View file

View file

@ -0,0 +1,31 @@
package eventjobs
import (
"fmt"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/models"
)
var SendPasswordChangedEmail core.EventJob = func(event *core.Event, c *core.Context) {
go func() {
mailer := c.GetMailer()
logger := c.GetLogger()
user, ok := event.Payload["user"].(models.User)
if !ok {
logger.Error("[SendPasswordChangedEmail job] invalid user")
return
}
mailer.SetFrom(core.EmailAddress{Name: "GoCondor", Address: "mail@example.com"})
mailer.SetTo([]core.EmailAddress{
{
Name: user.Name, Address: user.Email,
},
})
mailer.SetSubject("Password Changed")
body := fmt.Sprintf("Hi %v, \nYour password have been changed. \nThanks.", user.Name)
mailer.SetPlainTextBody(body)
mailer.Send()
}()
}

View file

@ -0,0 +1,38 @@
package eventjobs
import (
"fmt"
"os"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/models"
)
var SendResetPasswordEmail core.EventJob = func(event *core.Event, c *core.Context) {
go func() {
mailer := c.GetMailer()
logger := c.GetLogger()
user, ok := event.Payload["user"].(models.User)
if !ok {
logger.Error("[SendResetPasswordEmail job] invalid user")
return
}
mailer.SetFrom(core.EmailAddress{Name: "Goffee", Address: "mail@example.com"})
mailer.SetTo([]core.EmailAddress{
{
Name: user.Name, Address: user.Email,
},
})
mailer.SetSubject("Reset Password Link")
hostname, err := os.Hostname()
if err != nil {
c.GetLogger().Error(err.Error())
}
resetPasswordLink := fmt.Sprintf("%v/reset-password/code/%v", hostname, c.CastToString(event.Payload["code"]))
body := fmt.Sprintf("Hi %v, <br>Click the link below to reset your password <br><a href=\"%v\">Reset Password</a>. <br>Thanks.", user.Name, resetPasswordLink)
mailer.SetHTMLBody(body)
mailer.Send()
}()
}

View file

@ -0,0 +1,31 @@
package eventjobs
import (
"fmt"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/models"
)
var SendWelcomeEmail core.EventJob = func(event *core.Event, c *core.Context) {
go func() {
mailer := c.GetMailer()
logger := c.GetLogger()
user, ok := event.Payload["user"].(models.User)
if !ok {
logger.Error("[SenEmail job] invalid user")
return
}
mailer.SetFrom(core.EmailAddress{Name: "GoCondor", Address: "mail@example.com"})
mailer.SetTo([]core.EmailAddress{
{
Name: user.Name, Address: user.Email,
},
})
mailer.SetSubject("Welcome To GoCondor")
body := fmt.Sprintf("Hi %v, \nWelcome to GoCondor \nYour account have been created successfully. \nThanks.", user.Name)
mailer.SetPlainTextBody(body)
mailer.Send()
}()
}

View file

@ -0,0 +1,9 @@
package eventjobs
import (
"git.smarteching.com/goffee/core"
)
var TestEvent core.EventJob = func(event *core.Event, c *core.Context) {
c.GetLogger().Info("hello from event test job")
}

43
go.mod Normal file
View file

@ -0,0 +1,43 @@
module git.smarteching.com/goffee/todoapp
go 1.23.1
require (
git.smarteching.com/goffee/core v1.7.2
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/julienschmidt/httprouter v1.3.0
gorm.io/gorm v1.25.12
)
require (
github.com/SparkPost/gosparkpost v0.2.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/harranali/mailing v1.2.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/mailgun/mailgun-go/v4 v4.10.0 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.0.5 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.12.0+incompatible // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/text v0.14.0 // indirect
gorm.io/driver/mysql v1.5.1 // indirect
gorm.io/driver/postgres v1.5.2 // indirect
gorm.io/driver/sqlite v1.5.2 // indirect
)

115
go.sum Normal file
View file

@ -0,0 +1,115 @@
git.smarteching.com/goffee/core v1.7.2 h1:3rha+OSi1UFqHkEZwyBKuFy0pOYt60HiHpEPQFEkJlk=
git.smarteching.com/goffee/core v1.7.2/go.mod h1:QQNIHVN6qjJBtq42WCQMrLYN9oFE3wm26SLU8ZxNTec=
github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s=
github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/brianvoe/gofakeit/v6 v6.21.0 h1:tNkm9yxEbpuPK8Bx39tT4sSc5i9SUGiciLdNix+VDQY=
github.com/brianvoe/gofakeit/v6 v6.21.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/harranali/mailing v1.2.0 h1:ihIyJwB8hyRVcdk+v465wk1PHMrSrgJqo/kMd+gZClY=
github.com/harranali/mailing v1.2.0/go.mod h1:4a5N3yG98pZKluMpmcYlTtll7bisvOfGQEMIng3VQk4=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailgun/mailgun-go/v4 v4.10.0 h1:e5LVsxpqjOYRyaOWifrJORoLQZTYDP+g4ljfmf9G2zE=
github.com/mailgun/mailgun-go/v4 v4.10.0/go.mod h1:L9s941Lgk7iB3TgywTPz074pK2Ekkg4kgbnAaAyJ2z8=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.12.0+incompatible h1:/N2vx18Fg1KmQOh6zESc5FJB8pYwt5QFBDflYPh1KVg=
github.com/sendgrid/sendgrid-go v3.12.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

390
handlers/authentication.go Normal file
View file

@ -0,0 +1,390 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package handlers
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/events"
"git.smarteching.com/goffee/todoapp/models"
"git.smarteching.com/goffee/todoapp/utils"
"github.com/google/uuid"
"gorm.io/gorm"
)
func Signup(c *core.Context) *core.Response {
name := c.GetRequestParam("name")
email := c.GetRequestParam("email")
password := c.GetRequestParam("password")
// check if email exists
var user models.User
res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user)
if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) {
c.GetLogger().Error(res.Error.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal error",
}))
}
if res.Error == nil {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "email already exists in the database",
}))
}
// validation data
data := map[string]interface{}{
"name": name,
"email": email,
"password": password,
}
// validation rules
rules := map[string]interface{}{
"name": "required|alphaNumeric",
"email": "required|email",
"password": "required|length:6,10",
}
// validate
v := c.GetValidator().Validate(data, rules)
if v.Failed() {
c.GetLogger().Error(v.GetErrorMessagesJson())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson())
}
//hash the password
passwordHashed, err := c.GetHashing().HashPassword(c.CastToString(password))
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]interface{}{
"message": err.Error(),
}))
}
// store the record in db
user = models.User{
Name: c.CastToString(name),
Email: c.CastToString(email),
Password: passwordHashed,
}
res = c.GetGorm().Create(&user)
if res.Error != nil {
c.GetLogger().Error(res.Error.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": res.Error.Error(),
}))
}
token, err := c.GetJWT().GenerateToken(map[string]interface{}{
"userID": user.ID,
})
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
// cache the token
userAgent := c.GetUserAgent()
hashedCacheKey := utils.CreateAuthTokenHashedCacheKey(user.ID, userAgent)
err = c.GetCache().Set(hashedCacheKey, token)
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
"message": "internal server error",
}))
}
// fire user registered event
err = c.GetEventsManager().Fire(&core.Event{Name: events.USER_REGISTERED, Payload: map[string]interface{}{
"user": user,
}})
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
return c.Response.Json(c.MapToJson(map[string]string{
"token": token,
}))
}
func Signin(c *core.Context) *core.Response {
email := c.GetRequestParam("email")
password := c.GetRequestParam("password")
data := map[string]interface{}{
"email": email,
"password": password,
}
rules := map[string]interface{}{
"email": "required|email",
"password": "required",
}
v := c.GetValidator().Validate(data, rules)
if v.Failed() {
c.GetLogger().Error(v.GetErrorMessagesJson())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson())
}
var user models.User
res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user)
if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) {
c.GetLogger().Error(res.Error.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid email or password",
}))
}
ok, err := c.GetHashing().CheckPasswordHash(user.Password, c.CastToString(password))
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": err.Error(),
}))
}
if !ok {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid email or password",
}))
}
token, err := c.GetJWT().GenerateToken(map[string]interface{}{
"userID": user.ID,
})
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
// cache the token
userAgent := c.GetUserAgent()
hashedCacheKey := utils.CreateAuthTokenHashedCacheKey(user.ID, userAgent)
err = c.GetCache().Set(hashedCacheKey, token)
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
"message": "internal server error",
}))
}
return c.Response.Json(c.MapToJson(map[string]string{
"token": token,
}))
}
func ResetPasswordRequest(c *core.Context) *core.Response {
email := c.GetRequestParam("email")
// validation data
data := map[string]interface{}{
"email": email,
}
// validation rules
rules := map[string]interface{}{
"email": "required|email",
}
// validate
v := c.GetValidator().Validate(data, rules)
if v.Failed() {
c.GetLogger().Error(v.GetErrorMessagesJson())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson())
}
// check email in the database
var user models.User
res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user)
if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) {
c.GetLogger().Error(res.Error.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "email not found in our database",
}))
}
// generate the link
expiresAt := time.Now().Add(time.Hour * 3).Unix()
linkCodeData := map[string]string{
"userID": c.CastToString(user.ID),
"expiresAt": c.CastToString(expiresAt),
}
code := uuid.NewString()
c.GetCache().SetWithExpiration(code, c.MapToJson(linkCodeData), time.Hour*3)
err := c.GetEventsManager().Fire(&core.Event{Name: events.USER_PASSWORD_RESET_REQUESTED, Payload: map[string]interface{}{
"user": user,
"code": code,
}})
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
return c.Response.Json(c.MapToJson(map[string]string{
"message": "reset password email sent successfully",
}))
}
func SetNewPassword(c *core.Context) *core.Response {
urlCode := c.CastToString(c.GetPathParam("code"))
linkCodeDataStr, err := c.GetCache().Get(urlCode)
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid link",
}))
}
var linkCode map[string]interface{}
json.Unmarshal([]byte(linkCodeDataStr), &linkCode)
expiresAtUnix, err := strconv.ParseInt(c.CastToString(linkCode["expiresAt"]), 10, 64)
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid link",
}))
}
expiresAt := time.Unix(expiresAtUnix, 0)
if time.Now().After(expiresAt) {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid link",
}))
}
userID, err := strconv.ParseUint(c.CastToString(linkCode["userID"]), 10, 64)
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid link",
}))
}
oldPassword := c.CastToString(c.GetRequestParam("old_password"))
newPassword := c.CastToString(c.GetRequestParam("new_password"))
newPasswordConfirm := c.CastToString(c.GetRequestParam("new_password_confirm"))
// validation data
data := map[string]interface{}{
"old_password": oldPassword,
"new_password": newPassword,
"new_password_confirm": newPasswordConfirm,
}
// validation rules
rules := map[string]interface{}{
"old_password": "required|length:6,10",
"new_password": "required|length:6,10",
"new_password_confirm": "required|length:6,10",
}
// validate
v := c.GetValidator().Validate(data, rules)
if v.Failed() {
c.GetLogger().Error(v.GetErrorMessagesJson())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson())
}
var user models.User
res := c.GetGorm().Where("id = ?", userID).First(&user)
if res.Error != nil {
c.GetLogger().Error(res.Error.Error())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid link",
}))
}
SamePassword, err := c.GetHashing().CheckPasswordHash(user.Password, oldPassword)
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid link",
}))
}
if !SamePassword {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "the old password is incorrect",
}))
}
if newPassword != newPasswordConfirm {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "new password does not match new password confirmation",
}))
}
hashedNewPassword, err := c.GetHashing().HashPassword(newPassword)
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid password",
}))
}
user.Password = hashedNewPassword
c.GetGorm().Save(&user)
err = c.GetEventsManager().Fire(&core.Event{Name: events.PASSWORD_CHANGED, Payload: map[string]interface{}{
"user": user,
}})
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
return c.Response.Json(c.MapToJson(map[string]string{
"message": "password changed successfully",
}))
}
func Signout(c *core.Context) *core.Response {
tokenRaw := c.GetHeader("Authorization")
token := strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1))
if token == "" {
return c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
}))
}
payload, err := c.GetJWT().DecodeToken(token)
if err != nil {
return c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
}))
}
userAgent := c.GetUserAgent()
hashedCacheKey := utils.CreateAuthTokenHashedCacheKey(uint(c.CastToInt(payload["userID"])), userAgent)
err = c.GetCache().Delete(hashedCacheKey)
if err != nil {
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
"message": "internal error",
}))
}
return c.Response.SetStatusCode(http.StatusOK).Json(c.MapToJson(map[string]interface{}{
"message": "signed out successfully",
}))
}

21
handlers/home.go Normal file
View file

@ -0,0 +1,21 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package handlers
import (
"git.smarteching.com/goffee/core"
)
// Show home page
func WelcomeHome(c *core.Context) *core.Response {
message := "{\"message\": \"Welcome to GoCondor\"}"
return c.Response.Json(message)
}
// Show dashboard
func WelcomeToDashboard(c *core.Context) *core.Response {
message := "{\"message\": \"Welcome to Dashboard\"}"
return c.Response.Json(message)
}

131
handlers/todos.go Normal file
View file

@ -0,0 +1,131 @@
package handlers
import (
"encoding/json"
"strconv"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/models"
)
func ListTodos(c *core.Context) *core.Response {
var todos []models.Todo
result := c.GetGorm().Find(&todos)
if result.Error != nil {
return c.Response.SetStatusCode(500).Json(c.MapToJson(map[string]string{"message": result.Error.Error()}))
}
todosJson, err := json.Marshal(todos)
if err != nil {
return c.Response.Json(c.MapToJson(map[string]string{"message": err.Error()}))
}
return c.Response.Json(string(todosJson))
}
func CreateTodos(c *core.Context) *core.Response {
title := c.CastToString(c.GetRequestParam("title"))
body := c.CastToString(c.GetRequestParam("body"))
isDone := c.CastToString(c.GetRequestParam("isDone"))
v := c.GetValidator().Validate(map[string]interface{}{
"title": title,
"body": body,
"isDone": isDone,
}, map[string]interface{}{
"title": "required",
"body": "required",
})
if v.Failed() {
return c.Response.Json(v.GetErrorMessagesJson())
}
result := c.GetGorm().Create(&models.Todo{
Title: title,
Body: body,
IsDone: false,
})
if result.Error != nil {
return c.Response.SetStatusCode(500).Json(c.MapToJson(map[string]string{
"message": result.Error.Error(),
}))
}
return c.Response.Json(c.MapToJson(map[string]string{
"message": "created successfully",
}))
}
func ShowTodo(c *core.Context) *core.Response {
todoID := c.CastToString(c.GetPathParam("id"))
var todo models.Todo
result := c.GetGorm().First(&todo, todoID)
if result.Error != nil {
return c.Response.SetStatusCode(500).Json(c.MapToJson(map[string]string{"message": result.Error.Error()}))
}
todoJson, err := json.Marshal(todo)
if err != nil {
return c.Response.Json(c.MapToJson(map[string]string{"message": err.Error()}))
}
return c.Response.Json(string(todoJson))
}
func DeleteTodo(c *core.Context) *core.Response {
todoID := c.CastToString(c.GetPathParam("id"))
var todo models.Todo
result := c.GetGorm().Delete(&todo, todoID)
if result.Error != nil {
return c.Response.SetStatusCode(500).Json(c.MapToJson(map[string]string{"message": result.Error.Error()}))
}
return c.Response.Json(c.MapToJson(map[string]string{"message": "record deleted successfully"}))
}
func UpdateTodo(c *core.Context) *core.Response {
var title string = ""
var body string = ""
var data map[string]interface{} = map[string]interface{}{}
var rules map[string]interface{} = map[string]interface{}{}
todoID := c.GetPathParam("id")
var todo models.Todo
result := c.GetGorm().First(&todo, todoID)
if result.Error != nil {
return c.Response.Json(c.MapToJson(map[string]string{"message": result.Error.Error()}))
}
if c.RequestParamExists("title") {
title = c.CastToString(c.GetRequestParam("title"))
data["title"] = title
}
if c.RequestParamExists("body") {
body = c.CastToString(c.GetRequestParam("body"))
data["body"] = body
}
if c.RequestParamExists("isDone") {
isDoneStr := c.CastToString(c.GetRequestParam("isDone"))
data["isDone"] = isDoneStr
rules["isDone"] = "in:true,false"
}
v := c.GetValidator().Validate(data, rules)
if v.Failed() {
return c.Response.Json(v.GetErrorMessagesJson())
}
if c.RequestParamExists("title") {
todo.Title = title
}
if c.RequestParamExists("body") {
todo.Body = body
}
if c.RequestParamExists("isDone") {
isDoneStr := c.CastToString(c.GetRequestParam("isDone"))
isDone, err := strconv.ParseBool(isDoneStr)
if err != nil {
return c.Response.Json(c.MapToJson(map[string]string{"message": err.Error()}))
}
todo.IsDone = isDone
}
c.GetGorm().Save(&todo)
todoJson, err := json.Marshal(todo)
if err != nil {
return c.Response.Json(c.MapToJson(map[string]string{"message": err.Error()}))
}
return c.Response.Json(string(todoJson))
}

52
main.go Normal file
View file

@ -0,0 +1,52 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package main
import (
"log"
"os"
"path"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/core/env"
"git.smarteching.com/goffee/core/logger"
"git.smarteching.com/goffee/todoapp/config"
"github.com/joho/godotenv"
"github.com/julienschmidt/httprouter"
)
// The main function
func main() {
app := core.New()
basePath, err := os.Getwd()
if err != nil {
log.Fatal("error getting current working dir")
}
app.SetBasePath(basePath)
app.MakeDirs("logs", "storage", "storage/sqlite", "tls")
// Handle the reading of the .env file
if config.GetEnvFileConfig().UseDotEnvFile {
envVars, err := godotenv.Read(".env")
if err != nil {
log.Fatal("Error loading .env file")
}
env.SetEnvVars(envVars)
}
// Handle the logs
app.SetLogsDriver(&logger.LogFileDriver{
FilePath: path.Join(basePath, "logs/app.log"),
})
app.SetRequestConfig(config.GetRequestConfig())
app.SetGormConfig(config.GetGormConfig())
app.SetCacheConfig(config.GetCacheConfig())
app.Bootstrap()
registerGlobalMiddlewares()
registerRoutes()
registerEvents()
if config.GetGormConfig().EnableGorm == true {
RunAutoMigrations()
}
app.Run(httprouter.New())
}

View file

@ -0,0 +1,17 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package middlewares
import (
"fmt"
"git.smarteching.com/goffee/core"
)
// Another example middleware
var AnotherExampleMiddleware core.Middleware = func(c *core.Context) {
fmt.Println("another example middleware!")
c.Next()
}

69
middlewares/auth-check.go Normal file
View file

@ -0,0 +1,69 @@
package middlewares
import (
"errors"
"net/http"
"strings"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/models"
"git.smarteching.com/goffee/todoapp/utils"
"gorm.io/gorm"
)
var AuthCheck core.Middleware = func(c *core.Context) {
tokenRaw := c.GetHeader("Authorization")
token := strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1))
if token == "" {
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
return
}
payload, err := c.GetJWT().DecodeToken(token)
if err != nil {
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
return
}
userAgent := c.GetUserAgent()
hashedCacheKey := utils.CreateAuthTokenHashedCacheKey(uint(c.CastToInt(payload["userID"])), userAgent)
cachedToken, err := c.GetCache().Get(hashedCacheKey)
if err != nil {
// user signed out
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
return
}
if cachedToken != token {
// using old token replaced with new one after recent signin
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
return
}
var user models.User
res := c.GetGorm().Where("id = ?", payload["userID"]).First(&user)
if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) {
// error with the database
c.GetLogger().Error(res.Error.Error())
c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
"message": "internal error",
})).ForceSendResponse()
return
}
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
// user record is not found (deleted)
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
return
}
c.Next()
}

View file

@ -0,0 +1,17 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package middlewares
import (
"fmt"
"git.smarteching.com/goffee/core"
)
// Example middleware
var ExampleMiddleware core.Middleware = func(c *core.Context) {
fmt.Println("example middleware!")
c.Next()
}

16
models/todo.go Normal file
View file

@ -0,0 +1,16 @@
package models
import "gorm.io/gorm"
type Todo struct {
gorm.Model
Title string
Body string
IsDone bool
// add your field here...
}
// Override the table name
func (Todo) TableName() string {
return "todos"
}

19
models/user.go Normal file
View file

@ -0,0 +1,19 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package models
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string
Email string
Password string
}
// Override the table name
func (User) TableName() string {
return "users"
}

25
register-events.go Normal file
View file

@ -0,0 +1,25 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package main
import (
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/events"
"git.smarteching.com/goffee/todoapp/events/eventjobs"
)
// Register events
func registerEvents() {
eventsManager := core.ResolveEventsManager()
//########################################
//# events registration #####
//########################################
// register your event here ...
eventsManager.Register(events.USER_REGISTERED, eventjobs.SendWelcomeEmail)
eventsManager.Register(events.USER_REGISTERED, eventjobs.TestEvent)
eventsManager.Register(events.USER_PASSWORD_RESET_REQUESTED, eventjobs.SendResetPasswordEmail)
eventsManager.Register(events.PASSWORD_CHANGED, eventjobs.SendPasswordChangedEmail)
}

View file

@ -0,0 +1,15 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package main
// Register middlewares globally
func registerGlobalMiddlewares() {
//########################################
//# Global middlewares registration #####
//########################################
// Register global middlewares here ...
// core.UseMiddleware(middlewares.AnotherExampleMiddleware)
}

33
routes.go Normal file
View file

@ -0,0 +1,33 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package main
import (
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/handlers"
)
// Register the app routes
func registerRoutes() {
router := core.ResolveRouter()
//#############################
//# App Routes #####
//#############################
// Define your routes here...
router.Get("/", handlers.WelcomeHome)
router.Get("/todos", handlers.ListTodos)
router.Post("/todos", handlers.CreateTodos)
router.Get("/todos/:id", handlers.ShowTodo)
router.Delete("/todos/:id", handlers.DeleteTodo)
router.Put("/todos/:id", handlers.UpdateTodo)
// Uncomment the lines below to enable authentication
// router.Post("/signup", handlers.Signup)
// router.Post("/signin", handlers.Signin)
// router.Post("/signout", handlers.Signout)
// router.Post("/reset-password", handlers.ResetPasswordRequest)
// router.Post("/reset-password/code/:code", handlers.SetNewPassword)
// router.Get("/dashboard", handlers.WelcomeToDashboard, middlewares.AuthCheck)
}

21
run-auto-migrations.go Normal file
View file

@ -0,0 +1,21 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package main
import (
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/todoapp/models"
)
func RunAutoMigrations() {
db := core.ResolveGorm()
//##############################
//# Models auto migration #####
//##############################
// Add auto migrations for your models here...
db.AutoMigrate(&models.User{})
db.AutoMigrate(&models.Todo{})
}

0
storage/.gitignore vendored Normal file
View file

BIN
storage/sqlite.db Normal file

Binary file not shown.

18
utils/helpers.go Normal file
View file

@ -0,0 +1,18 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package utils
import (
"crypto/md5"
"fmt"
)
// generate a hashed string to be used as key for caching auth jwt token
func CreateAuthTokenHashedCacheKey(userID uint, userAgent string) string {
cacheKey := fmt.Sprintf("userid:_%v_useragent:_%v_jwt_token", userID, userAgent)
hashedCacheKey := fmt.Sprintf("%v", fmt.Sprintf("%x", md5.Sum([]byte(cacheKey))))
return hashedCacheKey
}