You've already forked golang-base-project
Adds godoc comments
This commit is contained in:
@ -1,5 +1,7 @@
|
|||||||
# Golang Base Project
|
# Golang Base Project
|
||||||
|
|
||||||
|
[](https://godoc.org/github.com/uberswe/golang-base-project)
|
||||||
|
|
||||||
A minimal Golang project with user authentication ready out of the box. All frontend assets should be less than 100 kB on every page load.
|
A minimal Golang project with user authentication ready out of the box. All frontend assets should be less than 100 kB on every page load.
|
||||||
|
|
||||||
See a live example at: [https://www.golangbase.com](https://www.golangbase.com)
|
See a live example at: [https://www.golangbase.com](https://www.golangbase.com)
|
||||||
@ -162,6 +164,10 @@ There is a workflow to deploy to my personal server whenever there is a merge to
|
|||||||
|
|
||||||
I use [supervisor](http://supervisord.org/) with [docker-compose](https://docs.docker.com/compose/production/) to run my containers. [Caddy](https://caddyserver.com/) handles the SSL configuration and routing. I use [Ansible](https://docs.ansible.com/ansible/latest/user_guide/playbooks.html) to manage my configurations.
|
I use [supervisor](http://supervisord.org/) with [docker-compose](https://docs.docker.com/compose/production/) to run my containers. [Caddy](https://caddyserver.com/) handles the SSL configuration and routing. I use [Ansible](https://docs.ansible.com/ansible/latest/user_guide/playbooks.html) to manage my configurations.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See [GoDoc](https://godoc.org/github.com/uberswe/golang-base-project) for further documentation.
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Contributions are welcome and greatly appreciated. Please note that I am not looking to add any more features to this project but I am happy to take care of bugfixes, updates and other suggestions. If you have a question or suggestion please feel free to [open an issue](https://github.com/uberswe/golang-base-project/issues/new). To contribute code, please fork this repository, make your changes on a separate branch and then [open a pull request](https://github.com/uberswe/golang-base-project/compare).
|
Contributions are welcome and greatly appreciated. Please note that I am not looking to add any more features to this project but I am happy to take care of bugfixes, updates and other suggestions. If you have a question or suggestion please feel free to [open an issue](https://github.com/uberswe/golang-base-project/issues/new). To contribute code, please fork this repository, make your changes on a separate branch and then [open a pull request](https://github.com/uberswe/golang-base-project/compare).
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Package config defines the env configuration variables
|
// Package config defines the env configuration variables
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
// Config defines all the configuration variables for the golang-base-project
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port string
|
Port string
|
||||||
CookieSecret string
|
CookieSecret string
|
||||||
|
@ -5,23 +5,26 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/uberswe/golang-base-project/config"
|
"github.com/uberswe/golang-base-project/config"
|
||||||
"github.com/uberswe/golang-base-project/util"
|
"github.com/uberswe/golang-base-project/text"
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service holds a golang-base-project config.Config and provides functions to send emails
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Config config.Config
|
Config config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New takes a golang-base-project config.Config and returns an instance of Service
|
||||||
func New(c config.Config) Service {
|
func New(c config.Config) Service {
|
||||||
return Service{
|
return Service{
|
||||||
Config: c,
|
Config: c,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send sends an email with the provided subject and message to the provided email.
|
||||||
func (s Service) Send(to string, subject string, message string) {
|
func (s Service) Send(to string, subject string, message string) {
|
||||||
// Authentication.
|
// Authentication.
|
||||||
auth := smtp.PlainAuth("", s.Config.SMTPUsername, s.Config.SMTPPassword, s.Config.SMTPHost)
|
auth := smtp.PlainAuth("", s.Config.SMTPUsername, s.Config.SMTPPassword, s.Config.SMTPHost)
|
||||||
@ -34,8 +37,8 @@ func (s Service) Send(to string, subject string, message string) {
|
|||||||
_, _ = fmt.Fprintf(&b, "Content-Type: multipart/alternative; charset=\"UTF-8\"; boundary=\"%s\"\r\n", writer.Boundary())
|
_, _ = fmt.Fprintf(&b, "Content-Type: multipart/alternative; charset=\"UTF-8\"; boundary=\"%s\"\r\n", writer.Boundary())
|
||||||
_, _ = fmt.Fprintf(&b, "\r\n\r\n--%s\r\nContent-Type: %s; charset=UTF-8;\nContent-Transfer-Encoding: 8bit\r\n\r\n", writer.Boundary(), "text/plain")
|
_, _ = fmt.Fprintf(&b, "\r\n\r\n--%s\r\nContent-Type: %s; charset=UTF-8;\nContent-Transfer-Encoding: 8bit\r\n\r\n", writer.Boundary(), "text/plain")
|
||||||
b.Write([]byte(message))
|
b.Write([]byte(message))
|
||||||
htmlMessage := util.StringLinkToHTMLLink(message)
|
htmlMessage := text.LinkToHTMLLink(message)
|
||||||
htmlMessage = util.NL2BR(htmlMessage)
|
htmlMessage = text.Nl2Br(htmlMessage)
|
||||||
_, _ = fmt.Fprintf(&b, "\r\n\r\n--%s\r\nContent-Type: %s; charset=UTF-8;\nContent-Transfer-Encoding: 8bit\r\n\r\n", writer.Boundary(), "text/html")
|
_, _ = fmt.Fprintf(&b, "\r\n\r\n--%s\r\nContent-Type: %s; charset=UTF-8;\nContent-Transfer-Encoding: 8bit\r\n\r\n", writer.Boundary(), "text/html")
|
||||||
b.Write([]byte(htmlMessage))
|
b.Write([]byte(htmlMessage))
|
||||||
|
|
||||||
@ -43,7 +46,7 @@ func (s Service) Send(to string, subject string, message string) {
|
|||||||
|
|
||||||
sender := s.Config.SMTPSender
|
sender := s.Config.SMTPSender
|
||||||
if strings.Contains(sender, "<") {
|
if strings.Contains(sender, "<") {
|
||||||
sender = util.GetStringBetweenStrings(sender, "<", ">")
|
sender = text.BetweenStrings(sender, "<", ">")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sending email.
|
// Sending email.
|
||||||
|
33
main.go
33
main.go
@ -15,58 +15,85 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// staticFS is an embedded file system
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
var staticFS embed.FS
|
var staticFS embed.FS
|
||||||
|
|
||||||
|
// Run is the main function that runs the entire package and starts the webserver, this is called by /cmd/base/main.go
|
||||||
func Run() {
|
func Run() {
|
||||||
// When generating random strings we need to provide a seed otherwise we always get the same strings the next time our application starts
|
// When generating random strings we need to provide a seed otherwise we always get the same strings the next time our application starts
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
var t *template.Template
|
// We load environment variables, these are only read when the application launches
|
||||||
conf := loadEnvVariables()
|
conf := loadEnvVariables()
|
||||||
|
|
||||||
|
// We connect to the database using the configuration generated from the environment variables.
|
||||||
db, err := connectToDatabase(conf)
|
db, err := connectToDatabase(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Once a database connection is established we run any needed migrations
|
||||||
err = migrateDatabase(db)
|
err = migrateDatabase(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
// t will hold all our html templates used to render pages
|
||||||
|
var t *template.Template
|
||||||
|
|
||||||
|
// We parse and load the html files into our t variable
|
||||||
t, err = loadTemplates()
|
t, err = loadTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A gin Engine instance with the default configuration
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
// We create a new cookie store with a key used to secure cookies with HMAC
|
||||||
store := cookie.NewStore([]byte(conf.CookieSecret))
|
store := cookie.NewStore([]byte(conf.CookieSecret))
|
||||||
|
|
||||||
|
// We define our session middleware to be used globally on all routes
|
||||||
r.Use(sessions.Sessions("golang_base_project_session", store))
|
r.Use(sessions.Sessions("golang_base_project_session", store))
|
||||||
|
|
||||||
|
// We pase our template variable t to the gin engine so it can be used to render html pages
|
||||||
r.SetHTMLTemplate(t)
|
r.SetHTMLTemplate(t)
|
||||||
|
|
||||||
|
// our assets are only located in a section of our file system. so we create a sub file system.
|
||||||
subFS, err := fs.Sub(staticFS, "dist/assets")
|
subFS, err := fs.Sub(staticFS, "dist/assets")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All static assets are under the /assets path so we make this its own group called assets
|
||||||
assets := r.Group("/assets")
|
assets := r.Group("/assets")
|
||||||
|
|
||||||
|
// This middleware sets the Cache-Control header and is applied to the assets group only
|
||||||
assets.Use(middleware.Cache(conf.CacheMaxAge))
|
assets.Use(middleware.Cache(conf.CacheMaxAge))
|
||||||
|
|
||||||
|
// All requests to /assets will use the sub fil system which contains all our static assets
|
||||||
assets.StaticFS("/", http.FS(subFS))
|
assets.StaticFS("/", http.FS(subFS))
|
||||||
|
|
||||||
|
// Session middleware is applied to all groups after this point.
|
||||||
r.Use(middleware.Session(db))
|
r.Use(middleware.Session(db))
|
||||||
|
|
||||||
|
// A General middleware is defined to add default headers to improve site security
|
||||||
r.Use(middleware.General())
|
r.Use(middleware.General())
|
||||||
|
|
||||||
|
// A new instance of the routes controller is created
|
||||||
controller := routes.New(db, conf)
|
controller := routes.New(db, conf)
|
||||||
|
|
||||||
|
// Any request to / will call controller.Index
|
||||||
r.GET("/", controller.Index)
|
r.GET("/", controller.Index)
|
||||||
|
|
||||||
|
// We want to handle both POST and GET requests on the /search route. We define both but use the same function to handle the requests.
|
||||||
r.GET("/search", controller.Search)
|
r.GET("/search", controller.Search)
|
||||||
r.POST("/search", controller.Search)
|
r.POST("/search", controller.Search)
|
||||||
|
|
||||||
|
// We define our 404 handler for when a page can not be found
|
||||||
r.NoRoute(controller.NoRoute)
|
r.NoRoute(controller.NoRoute)
|
||||||
|
|
||||||
|
// noAuth is a group for routes which should only be accessed if the user is not authenticated
|
||||||
noAuth := r.Group("/")
|
noAuth := r.Group("/")
|
||||||
noAuth.Use(middleware.NoAuth())
|
noAuth.Use(middleware.NoAuth())
|
||||||
|
|
||||||
@ -77,6 +104,7 @@ func Run() {
|
|||||||
noAuth.GET("/user/password/forgot", controller.ForgotPassword)
|
noAuth.GET("/user/password/forgot", controller.ForgotPassword)
|
||||||
noAuth.GET("/user/password/reset/:token", controller.ResetPassword)
|
noAuth.GET("/user/password/reset/:token", controller.ResetPassword)
|
||||||
|
|
||||||
|
// We make a separate group for our post requests on the same endpoints so that we can define our throttling middleware on POST requests only.
|
||||||
noAuthPost := noAuth.Group("/")
|
noAuthPost := noAuth.Group("/")
|
||||||
noAuthPost.Use(middleware.Throttle(conf.RequestsPerMinute))
|
noAuthPost.Use(middleware.Throttle(conf.RequestsPerMinute))
|
||||||
|
|
||||||
@ -86,6 +114,7 @@ func Run() {
|
|||||||
noAuthPost.POST("/user/password/forgot", controller.ForgotPasswordPost)
|
noAuthPost.POST("/user/password/forgot", controller.ForgotPasswordPost)
|
||||||
noAuthPost.POST("/user/password/reset/:token", controller.ResetPasswordPost)
|
noAuthPost.POST("/user/password/reset/:token", controller.ResetPasswordPost)
|
||||||
|
|
||||||
|
// the admin group handles routes that should only be accessible to authenticated users
|
||||||
admin := r.Group("/")
|
admin := r.Group("/")
|
||||||
admin.Use(middleware.Auth())
|
admin.Use(middleware.Auth())
|
||||||
admin.Use(middleware.Sensitive())
|
admin.Use(middleware.Sensitive())
|
||||||
@ -95,6 +124,8 @@ func Run() {
|
|||||||
admin.POST("/admin", controller.Admin)
|
admin.POST("/admin", controller.Admin)
|
||||||
admin.GET("/logout", controller.Logout)
|
admin.GET("/logout", controller.Logout)
|
||||||
|
|
||||||
|
// This starts our webserver, our application will not stop running or go past this point unless
|
||||||
|
// an error occurs or the web server is stopped for some reason. It is designed to run forever.
|
||||||
err = r.Run(conf.Port)
|
err = r.Run(conf.Port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
|
@ -6,8 +6,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UserIDKey is the key used to set and get the user id in the context of the current request
|
||||||
const UserIDKey = "UserID"
|
const UserIDKey = "UserID"
|
||||||
|
|
||||||
|
// Auth middleware redirects to /login and aborts the current request if there is no authenticated user
|
||||||
func Auth() gin.HandlerFunc {
|
func Auth() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
_, exists := c.Get(UserIDKey)
|
_, exists := c.Get(UserIDKey)
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cache middleware sets the Cache-Control header
|
||||||
func Cache(maxAge int) gin.HandlerFunc {
|
func Cache(maxAge int) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||||
|
@ -5,8 +5,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var SessionIdentifierKey = "SESSION_IDENTIFIER"
|
|
||||||
|
|
||||||
// NoAuth is for routes that can only be accessed when the user is unauthenticated
|
// NoAuth is for routes that can only be accessed when the user is unauthenticated
|
||||||
func NoAuth() gin.HandlerFunc {
|
func NoAuth() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
@ -8,10 +8,14 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SessionIDKey is the key used to set and get the session id in the context of the current request
|
||||||
|
const SessionIDKey = "SessionID"
|
||||||
|
|
||||||
|
// Session middleware checks for an active session and sets the UserIDKey to the context of the current request if found
|
||||||
func Session(db *gorm.DB) gin.HandlerFunc {
|
func Session(db *gorm.DB) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
sessionIdentifierInterface := session.Get(SessionIdentifierKey)
|
sessionIdentifierInterface := session.Get(SessionIDKey)
|
||||||
|
|
||||||
if sessionIdentifier, ok := sessionIdentifierInterface.(string); ok {
|
if sessionIdentifier, ok := sessionIdentifierInterface.(string); ok {
|
||||||
ses := models.Session{
|
ses := models.Session{
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Throttle middleware takes a limit per minute and blocks any additional requests that go over this limit
|
||||||
func Throttle(limit int) gin.HandlerFunc {
|
func Throttle(limit int) gin.HandlerFunc {
|
||||||
store := memory.NewStore()
|
store := memory.NewStore()
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Session holds information about user sessions and when they expire
|
||||||
type Session struct {
|
type Session struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Identifier string
|
Identifier string
|
||||||
@ -12,6 +13,7 @@ type Session struct {
|
|||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasExpired is a helper function that checks if the current time is after the session expire datetime
|
||||||
func (s Session) HasExpired() bool {
|
func (s Session) HasExpired() bool {
|
||||||
return s.ExpiresAt.Before(time.Now())
|
return s.ExpiresAt.Before(time.Now())
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Token holds tokens typically used for user activation and password resets
|
||||||
type Token struct {
|
type Token struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Value string
|
Value string
|
||||||
@ -14,11 +15,14 @@ type Token struct {
|
|||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasExpired is a helper function that checks if the current time is after the token expiration time
|
||||||
func (t Token) HasExpired() bool {
|
func (t Token) HasExpired() bool {
|
||||||
return t.ExpiresAt.Before(time.Now())
|
return t.ExpiresAt.Before(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// TokenUserActivation is a constant used to identify tokens used for user activation
|
||||||
TokenUserActivation string = "user_activation"
|
TokenUserActivation string = "user_activation"
|
||||||
TokenPasswordReset string = "password_reset"
|
// TokenPasswordReset is a constant used to identify tokens used for password resets
|
||||||
|
TokenPasswordReset string = "password_reset"
|
||||||
)
|
)
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// User holds information relating to users that use the application
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Email string
|
Email string
|
||||||
|
@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import "gorm.io/gorm"
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
// Website holds information about different websites
|
||||||
type Website struct {
|
type Website struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Title string
|
Title string
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Activate handles requests used to activate a users account
|
||||||
func (controller Controller) Activate(c *gin.Context) {
|
func (controller Controller) Activate(c *gin.Context) {
|
||||||
activationError := "Please provide a valid activation token"
|
activationError := "Please provide a valid activation token"
|
||||||
activationSuccess := "Account activated. You may now proceed to login to your account."
|
activationSuccess := "Account activated. You may now proceed to login to your account."
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Admin renders the admin dashboard
|
||||||
func (controller Controller) Admin(c *gin.Context) {
|
func (controller Controller) Admin(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "Admin",
|
Title: "Admin",
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
email2 "github.com/uberswe/golang-base-project/email"
|
email2 "github.com/uberswe/golang-base-project/email"
|
||||||
"github.com/uberswe/golang-base-project/models"
|
"github.com/uberswe/golang-base-project/models"
|
||||||
"github.com/uberswe/golang-base-project/util"
|
"github.com/uberswe/golang-base-project/ulid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ForgotPassword renders the HTML page where a password request can be initiated
|
||||||
func (controller Controller) ForgotPassword(c *gin.Context) {
|
func (controller Controller) ForgotPassword(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "Forgot Password",
|
Title: "Forgot Password",
|
||||||
@ -23,6 +24,7 @@ func (controller Controller) ForgotPassword(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "forgotpassword.html", pd)
|
c.HTML(http.StatusOK, "forgotpassword.html", pd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForgotPasswordPost handles the POST request which requests a password reset and then renders the HTML page with the appropriate message
|
||||||
func (controller Controller) ForgotPasswordPost(c *gin.Context) {
|
func (controller Controller) ForgotPasswordPost(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "Forgot Password",
|
Title: "Forgot Password",
|
||||||
@ -47,7 +49,7 @@ func (controller Controller) ForgotPasswordPost(c *gin.Context) {
|
|||||||
|
|
||||||
func (controller Controller) forgotPasswordEmailHandler(userID uint, email string) {
|
func (controller Controller) forgotPasswordEmailHandler(userID uint, email string) {
|
||||||
forgotPasswordToken := models.Token{
|
forgotPasswordToken := models.Token{
|
||||||
Value: util.GenerateULID(),
|
Value: ulid.Generate(),
|
||||||
Type: models.TokenPasswordReset,
|
Type: models.TokenPasswordReset,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Index renders the HTML of the index page
|
||||||
func (controller Controller) Index(c *gin.Context) {
|
func (controller Controller) Index(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "Home",
|
Title: "Home",
|
||||||
|
@ -5,13 +5,14 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/uberswe/golang-base-project/middleware"
|
"github.com/uberswe/golang-base-project/middleware"
|
||||||
"github.com/uberswe/golang-base-project/models"
|
"github.com/uberswe/golang-base-project/models"
|
||||||
"github.com/uberswe/golang-base-project/util"
|
"github.com/uberswe/golang-base-project/ulid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Login renders the HTML of the login page
|
||||||
func (controller Controller) Login(c *gin.Context) {
|
func (controller Controller) Login(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "Login",
|
Title: "Login",
|
||||||
@ -21,6 +22,7 @@ func (controller Controller) Login(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "login.html", pd)
|
c.HTML(http.StatusOK, "login.html", pd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginPost handles login requests and returns the appropriate HTML and messages
|
||||||
func (controller Controller) LoginPost(c *gin.Context) {
|
func (controller Controller) LoginPost(c *gin.Context) {
|
||||||
loginError := "Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below."
|
loginError := "Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below."
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
@ -72,7 +74,7 @@ func (controller Controller) LoginPost(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a ulid for the current session
|
// Generate a ulid for the current session
|
||||||
sessionIdentifier := util.GenerateULID()
|
sessionIdentifier := ulid.Generate()
|
||||||
|
|
||||||
ses := models.Session{
|
ses := models.Session{
|
||||||
Identifier: sessionIdentifier,
|
Identifier: sessionIdentifier,
|
||||||
@ -94,7 +96,7 @@ func (controller Controller) LoginPost(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
session.Set(middleware.SessionIdentifierKey, sessionIdentifier)
|
session.Set(middleware.SessionIDKey, sessionIdentifier)
|
||||||
|
|
||||||
err = session.Save()
|
err = session.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -8,9 +8,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Logout deletes the current user session and redirects the user to the index page
|
||||||
func (controller Controller) Logout(c *gin.Context) {
|
func (controller Controller) Logout(c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
session.Delete(middleware.SessionIdentifierKey)
|
session.Delete(middleware.SessionIDKey)
|
||||||
err := session.Save()
|
err := session.Save()
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
|
||||||
|
@ -8,11 +8,13 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Controller holds all the variables needed for routes to perform their logic
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
config config.Config
|
config config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new instance of the routes.Controller
|
||||||
func New(db *gorm.DB, c config.Config) Controller {
|
func New(db *gorm.DB, c config.Config) Controller {
|
||||||
return Controller{
|
return Controller{
|
||||||
db: db,
|
db: db,
|
||||||
@ -20,6 +22,7 @@ func New(db *gorm.DB, c config.Config) Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageData holds the default data needed for HTML pages to render
|
||||||
type PageData struct {
|
type PageData struct {
|
||||||
Title string
|
Title string
|
||||||
Messages []Message
|
Messages []Message
|
||||||
@ -27,11 +30,13 @@ type PageData struct {
|
|||||||
CacheParameter string
|
CacheParameter string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Message holds a message which can be rendered as responses on HTML pages
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Type string // success, warning, error, etc.
|
Type string // success, warning, error, etc.
|
||||||
Content string
|
Content string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAuthenticated checks if the current user is authenticated or not
|
||||||
func isAuthenticated(c *gin.Context) bool {
|
func isAuthenticated(c *gin.Context) bool {
|
||||||
_, exists := c.Get(middleware.UserIDKey)
|
_, exists := c.Get(middleware.UserIDKey)
|
||||||
return exists
|
return exists
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NoRoute handles rendering of the 404 page
|
||||||
func (controller Controller) NoRoute(c *gin.Context) {
|
func (controller Controller) NoRoute(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "404 Not Found",
|
Title: "404 Not Found",
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
email2 "github.com/uberswe/golang-base-project/email"
|
email2 "github.com/uberswe/golang-base-project/email"
|
||||||
"github.com/uberswe/golang-base-project/models"
|
"github.com/uberswe/golang-base-project/models"
|
||||||
"github.com/uberswe/golang-base-project/util"
|
"github.com/uberswe/golang-base-project/ulid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"log"
|
"log"
|
||||||
@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Register renders the HTML content of the register page
|
||||||
func (controller Controller) Register(c *gin.Context) {
|
func (controller Controller) Register(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "Register",
|
Title: "Register",
|
||||||
@ -25,6 +26,7 @@ func (controller Controller) Register(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "register.html", pd)
|
c.HTML(http.StatusOK, "register.html", pd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterPost handles requests to register users and returns appropriate messages as HTML content
|
||||||
func (controller Controller) RegisterPost(c *gin.Context) {
|
func (controller Controller) RegisterPost(c *gin.Context) {
|
||||||
passwordError := "Your password must be 8 characters in length or longer"
|
passwordError := "Your password must be 8 characters in length or longer"
|
||||||
registerError := "Could not register, please make sure the details you have provided are correct and that you do not already have an existing account."
|
registerError := "Could not register, please make sure the details you have provided are correct and that you do not already have an existing account."
|
||||||
@ -121,7 +123,7 @@ func (controller Controller) RegisterPost(c *gin.Context) {
|
|||||||
|
|
||||||
func (controller Controller) activationEmailHandler(userID uint, email string) {
|
func (controller Controller) activationEmailHandler(userID uint, email string) {
|
||||||
activationToken := models.Token{
|
activationToken := models.Token{
|
||||||
Value: util.GenerateULID(),
|
Value: ulid.Generate(),
|
||||||
Type: models.TokenUserActivation,
|
Type: models.TokenUserActivation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResendActivation renders the HTML page used to request a new activation email
|
||||||
func (controller Controller) ResendActivation(c *gin.Context) {
|
func (controller Controller) ResendActivation(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "Resend Activation Email",
|
Title: "Resend Activation Email",
|
||||||
@ -16,6 +17,7 @@ func (controller Controller) ResendActivation(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "resendactivation.html", pd)
|
c.HTML(http.StatusOK, "resendactivation.html", pd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResendActivationPost handles the post request for requesting a new activation email
|
||||||
func (controller Controller) ResendActivationPost(c *gin.Context) {
|
func (controller Controller) ResendActivationPost(c *gin.Context) {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Title: "Resend Activation Email",
|
Title: "Resend Activation Email",
|
||||||
|
@ -8,11 +8,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResetPasswordPageData defines additional data needed to render the reset password page
|
||||||
type ResetPasswordPageData struct {
|
type ResetPasswordPageData struct {
|
||||||
PageData
|
PageData
|
||||||
Token string
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetPassword renders the HTML page for resetting the users password
|
||||||
func (controller Controller) ResetPassword(c *gin.Context) {
|
func (controller Controller) ResetPassword(c *gin.Context) {
|
||||||
token := c.Param("token")
|
token := c.Param("token")
|
||||||
pd := ResetPasswordPageData{
|
pd := ResetPasswordPageData{
|
||||||
@ -26,6 +28,7 @@ func (controller Controller) ResetPassword(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "resetpassword.html", pd)
|
c.HTML(http.StatusOK, "resetpassword.html", pd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetPasswordPost handles post request used to reset users passwords
|
||||||
func (controller Controller) ResetPasswordPost(c *gin.Context) {
|
func (controller Controller) ResetPasswordPost(c *gin.Context) {
|
||||||
passwordError := "Your password must be 8 characters in length or longer"
|
passwordError := "Your password must be 8 characters in length or longer"
|
||||||
resetError := "Could not reset password, please try again"
|
resetError := "Could not reset password, please try again"
|
||||||
|
@ -8,11 +8,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SearchData holds additional data needed to render the search HTML page
|
||||||
type SearchData struct {
|
type SearchData struct {
|
||||||
PageData
|
PageData
|
||||||
Results []models.Website
|
Results []models.Website
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search renders the search HTML page and any search results
|
||||||
func (controller Controller) Search(c *gin.Context) {
|
func (controller Controller) Search(c *gin.Context) {
|
||||||
pd := SearchData{
|
pd := SearchData{
|
||||||
PageData: PageData{
|
PageData: PageData{
|
||||||
|
24
text/html.go
Normal file
24
text/html.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nl2Br converts \n to HTML <br> tags
|
||||||
|
func Nl2Br(s string) string {
|
||||||
|
return strings.Replace(s, "\n", "<br>", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkToHTMLLink converts regular links to HTML links
|
||||||
|
func LinkToHTMLLink(s string) string {
|
||||||
|
s = strings.Replace(s, "\n", " \n ", -1)
|
||||||
|
for _, p := range strings.Split(s, " ") {
|
||||||
|
u, err := url.ParseRequestURI(p)
|
||||||
|
if err == nil && u.Scheme != "" {
|
||||||
|
s = strings.Replace(s, p, fmt.Sprintf("<a href=\"%s\">%s</a>", p, p), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Replace(s, " \n ", "\n", -1)
|
||||||
|
}
|
15
text/random.go
Normal file
15
text/random.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import "math/rand"
|
||||||
|
|
||||||
|
// RandomString generates a random string of n length. Based on https://stackoverflow.com/a/22892986/1260548
|
||||||
|
func RandomString(n int) string {
|
||||||
|
// remove vowels to make it less likely to generate something offensive
|
||||||
|
var letters = []rune("bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ")
|
||||||
|
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[rand.Intn(len(letters))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
17
text/segment.go
Normal file
17
text/segment.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// BetweenStrings returns a string between the starting and ending string or an empty string if none could be found
|
||||||
|
func BetweenStrings(str string, start string, end string) (result string) {
|
||||||
|
s := strings.Index(str, start)
|
||||||
|
if s == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s += len(start)
|
||||||
|
e := strings.Index(str[s:], end)
|
||||||
|
if e == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return str[s : s+e]
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package util
|
package ulid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
@ -6,7 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateULID() string {
|
// Generate a new ULID string
|
||||||
|
func Generate() string {
|
||||||
entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0)
|
entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0)
|
||||||
res := ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
|
res := ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
|
||||||
return res.String()
|
return res.String()
|
49
util/text.go
49
util/text.go
@ -1,49 +0,0 @@
|
|||||||
// Package util provides utility functions for various things such as strings
|
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NL2BR(s string) string {
|
|
||||||
return strings.Replace(s, "\n", "<br>", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func StringLinkToHTMLLink(s string) string {
|
|
||||||
s = strings.Replace(s, "\n", " \n ", -1)
|
|
||||||
for _, p := range strings.Split(s, " ") {
|
|
||||||
u, err := url.ParseRequestURI(p)
|
|
||||||
if err == nil && u.Scheme != "" {
|
|
||||||
s = strings.Replace(s, p, fmt.Sprintf("<a href=\"%s\">%s</a>", p, p), 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Replace(s, " \n ", "\n", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetStringBetweenStrings(str string, start string, end string) (result string) {
|
|
||||||
s := strings.Index(str, start)
|
|
||||||
if s == -1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s += len(start)
|
|
||||||
e := strings.Index(str[s:], end)
|
|
||||||
if e == -1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return str[s : s+e]
|
|
||||||
}
|
|
||||||
|
|
||||||
// RandomString generates a random string of n length. Based on https://stackoverflow.com/a/22892986/1260548
|
|
||||||
func RandomString(n int) string {
|
|
||||||
// remove vowels to make it less likely to generate something offensive
|
|
||||||
var letters = []rune("bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ")
|
|
||||||
|
|
||||||
b := make([]rune, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = letters[rand.Intn(len(letters))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
Reference in New Issue
Block a user