You've already forked golang-base-project
Improves documentation (#13)
* Documents all env variables and adds an example project * Adds godoc comments * Fixed package naming issue
This commit is contained in:
104
README.md
104
README.md
@ -1,9 +1,15 @@
|
||||
# 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.
|
||||
|
||||
See a live example at: [https://www.golangbase.com](https://www.golangbase.com)
|
||||
|
||||
Projects that used this template as a starting point:
|
||||
|
||||
- [tournify/web](https://github.com/tournify/web) - A website for creating tournaments
|
||||
|
||||
Functionality includes:
|
||||
|
||||
- Login
|
||||
@ -22,7 +28,93 @@ The frontend is based off of examples from [https://getbootstrap.com/docs/5.0/ex
|
||||
|
||||
## Getting started
|
||||
|
||||
Simply run `go run cmd/base/main.go` and the entire project should run using an sqlite in-memory database.
|
||||
You can run this with go by typing `go run cmd/base/main.go` and the entire project should run using an sqlite in-memory database.
|
||||
|
||||
You can also use Docker.
|
||||
|
||||
### Docker
|
||||
|
||||
A dockerfile and docker compose file is provided to make running this project easy. Simply run `docker-compose up`.
|
||||
|
||||
You will need to change the env variables for sending email, when testing locally I recommend [Mailtrap.io](https://mailtrap.io/).
|
||||
|
||||
If you want to change the docker-compose file I recommend making a copy and overriding the base file with your own file like so `docker-compose -f docker-compose.yml -f docker-compose.local.yml up --build -d`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
This project uses environment variables and there are several ways to set them. If you are using docker see the article [Environment variables in Compose](https://docs.docker.com/compose/environment-variables/). Twilio has a more general guide on [how to set environment variables for Windows, Mac OS and Linux](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html).
|
||||
|
||||
The following variables can currently be set:
|
||||
|
||||
#### PORT
|
||||
|
||||
Port sets the port that the application should listen on for HTTP requests. A common port is 8080 and if you run the application locally you should see the application at `http://localhost:8080`.
|
||||
|
||||
#### BASE_URL
|
||||
|
||||
This url is mainly used for emails since it is considered unsafe to fetch the current url from headers. This should be set to url of the domain you are hosting the project on.
|
||||
|
||||
#### COOKIE_SECRET
|
||||
|
||||
This is the key used to authenticate the cookie value using HMAC. It is recommended to use a key with 32 or 64 bytes. This will default to a random 64 byte key if no value is set. Please read more about keys on [gorilla/securecookie](https://github.com/gorilla/securecookie).
|
||||
|
||||
If you don't set this to a value you might get an error like `ERROR! securecookie: the value is not valid` this is because a new key is generated every time you start the application and you have old cookies in your browser with an invalid HMAC.
|
||||
|
||||
#### DATABASE
|
||||
|
||||
The database you would like to use such as `mysql` or `sqlite`. See the [GORM documentation for more supported databases](https://gorm.io/docs/connecting_to_the_database.html).
|
||||
|
||||
#### DATABASE_HOST
|
||||
|
||||
The database host is usually localhost if running on the same machine or the container name, `db` in our case, if running with docker. If you have a remote database host you would set this to the ip or domain of that host.
|
||||
|
||||
#### DATABASE_PORT
|
||||
|
||||
The port of the database host.
|
||||
|
||||
#### DATABASE_USERNAME
|
||||
|
||||
Username used to authenticate to the database.
|
||||
|
||||
#### DATABASE_PASSWORD
|
||||
|
||||
Password used to authenticate to the database.
|
||||
|
||||
#### SMTP_USERNAME
|
||||
|
||||
Username used for authentication when sending emails over SMTP. For local development you can try using a free service like [Mailtrap.io](https://mailtrap.io/).
|
||||
|
||||
#### SMTP_PASSWORD
|
||||
|
||||
Password used for authentication when sending emails over SMTP.
|
||||
|
||||
#### SMTP_HOST
|
||||
|
||||
Host used for sending emails over SMTP.
|
||||
|
||||
#### SMTP_PORT
|
||||
|
||||
The port for the host used for sending emails over SMTP.
|
||||
|
||||
#### SMTP_SENDER
|
||||
|
||||
This will be the email shown in the `From:` field in emails.
|
||||
|
||||
#### STRICT_TRANSPORT_SECURITY
|
||||
|
||||
This will enable or disable strict transport security which sets a header that forces SSL. [Read more about HSTS here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security).
|
||||
|
||||
#### REQUESTS_PER_MINUTE
|
||||
|
||||
Used for throttling requests on authentication related endpoints. This value is how many times requests can be made per minute. Set to 5 by default.
|
||||
|
||||
#### CACHE_PARAMETER
|
||||
|
||||
This parameter is added to the end of static assets like so `/assets/js/main.js?c=rLWjPDCQTh`. A random one is set by default every time the application starts but you can set the `CACHE_PARAMETER` variable if you would like to control this in some other way.
|
||||
|
||||
#### CACHE_MAX_AGE
|
||||
|
||||
Sets the max-age time in seconds for the `Cache-Control` header. By default this header is set to 1 year.
|
||||
|
||||
## Project structure
|
||||
|
||||
@ -40,12 +132,6 @@ The `/routes` package contains all the route functions and logic. Typically, I t
|
||||
|
||||
All in all I have tried to keep the project simple and easy to understand. I want this project to serve as a template for myself and perhaps others when you want to create a new website.
|
||||
|
||||
## Docker
|
||||
|
||||
A dockerfile and docker compose file is provided to make running this project easy. Simply run `docker-compose up`.
|
||||
|
||||
You will need to change the env variables for sending email, when testing locally I recommend [Mailtrap.io](https://mailtrap.io/).
|
||||
|
||||
## Dependencies
|
||||
|
||||
I have tried to keep the dependencies low, there is always a balance here in my opinion and I have included the golang vendor folder and compiled assets so that there is no need to download anything to use this project other than the project itself.
|
||||
@ -78,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.
|
||||
|
||||
## Documentation
|
||||
|
||||
See [GoDoc](https://godoc.org/github.com/uberswe/golang-base-project) for further documentation.
|
||||
|
||||
## 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).
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Package config defines the env configuration variables
|
||||
package config
|
||||
|
||||
// Config defines all the configuration variables for the golang-base-project
|
||||
type Config struct {
|
||||
Port string
|
||||
CookieSecret string
|
||||
|
@ -5,23 +5,26 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/uberswe/golang-base-project/config"
|
||||
"github.com/uberswe/golang-base-project/util"
|
||||
"github.com/uberswe/golang-base-project/text"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Service holds a golang-base-project config.Config and provides functions to send emails
|
||||
type Service struct {
|
||||
Config config.Config
|
||||
}
|
||||
|
||||
// New takes a golang-base-project config.Config and returns an instance of Service
|
||||
func New(c config.Config) Service {
|
||||
return Service{
|
||||
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) {
|
||||
// Authentication.
|
||||
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, "\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))
|
||||
htmlMessage := util.StringLinkToHTMLLink(message)
|
||||
htmlMessage = util.NL2BR(htmlMessage)
|
||||
htmlMessage := text.LinkToHTMLLink(message)
|
||||
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")
|
||||
b.Write([]byte(htmlMessage))
|
||||
|
||||
@ -43,7 +46,7 @@ func (s Service) Send(to string, subject string, message string) {
|
||||
|
||||
sender := s.Config.SMTPSender
|
||||
if strings.Contains(sender, "<") {
|
||||
sender = util.GetStringBetweenStrings(sender, "<", ">")
|
||||
sender = text.BetweenStrings(sender, "<", ">")
|
||||
}
|
||||
|
||||
// Sending email.
|
||||
|
6
env.go
6
env.go
@ -3,7 +3,7 @@ package baseproject
|
||||
import (
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/uberswe/golang-base-project/config"
|
||||
"github.com/uberswe/golang-base-project/util"
|
||||
"github.com/uberswe/golang-base-project/text"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
@ -73,12 +73,12 @@ func loadEnvVariables() (c config.Config) {
|
||||
}
|
||||
|
||||
// CacheParameter is added to the end of static file urls to prevent caching old versions
|
||||
c.CacheParameter = util.RandomString(10)
|
||||
c.CacheParameter = text.RandomString(10)
|
||||
if os.Getenv("CACHE_PARAMETER") != "" {
|
||||
c.CacheParameter = os.Getenv("CACHE_PARAMETER")
|
||||
}
|
||||
|
||||
// CacheMaxAge is how many seconds to cache static assets
|
||||
// CacheMaxAge is how many seconds to cache static assets, 1 year by default
|
||||
c.CacheMaxAge = 31536000
|
||||
if os.Getenv("CACHE_MAX_AGE") != "" {
|
||||
i, err := strconv.Atoi(os.Getenv("CACHE_MAX_AGE"))
|
||||
|
33
main.go
33
main.go
@ -15,58 +15,85 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// staticFS is an embedded file system
|
||||
//go:embed dist/*
|
||||
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() {
|
||||
// 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())
|
||||
|
||||
var t *template.Template
|
||||
// We load environment variables, these are only read when the application launches
|
||||
conf := loadEnvVariables()
|
||||
|
||||
// We connect to the database using the configuration generated from the environment variables.
|
||||
db, err := connectToDatabase(conf)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Once a database connection is established we run any needed migrations
|
||||
err = migrateDatabase(db)
|
||||
if err != nil {
|
||||
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()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// A gin Engine instance with the default configuration
|
||||
r := gin.Default()
|
||||
|
||||
// We create a new cookie store with a key used to secure cookies with HMAC
|
||||
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))
|
||||
|
||||
// We pase our template variable t to the gin engine so it can be used to render html pages
|
||||
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")
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// All static assets are under the /assets path so we make this its own group called 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))
|
||||
|
||||
// All requests to /assets will use the sub fil system which contains all our static assets
|
||||
assets.StaticFS("/", http.FS(subFS))
|
||||
|
||||
// Session middleware is applied to all groups after this point.
|
||||
r.Use(middleware.Session(db))
|
||||
|
||||
// A General middleware is defined to add default headers to improve site security
|
||||
r.Use(middleware.General())
|
||||
|
||||
// A new instance of the routes controller is created
|
||||
controller := routes.New(db, conf)
|
||||
|
||||
// Any request to / will call 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.POST("/search", controller.Search)
|
||||
|
||||
// We define our 404 handler for when a page can not be found
|
||||
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.Use(middleware.NoAuth())
|
||||
|
||||
@ -77,6 +104,7 @@ func Run() {
|
||||
noAuth.GET("/user/password/forgot", controller.ForgotPassword)
|
||||
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.Use(middleware.Throttle(conf.RequestsPerMinute))
|
||||
|
||||
@ -86,6 +114,7 @@ func Run() {
|
||||
noAuthPost.POST("/user/password/forgot", controller.ForgotPasswordPost)
|
||||
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.Use(middleware.Auth())
|
||||
admin.Use(middleware.Sensitive())
|
||||
@ -95,6 +124,8 @@ func Run() {
|
||||
admin.POST("/admin", controller.Admin)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
|
@ -6,8 +6,10 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserIDKey is the key used to set and get the user id in the context of the current request
|
||||
const UserIDKey = "UserID"
|
||||
|
||||
// Auth middleware redirects to /login and aborts the current request if there is no authenticated user
|
||||
func Auth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
_, exists := c.Get(UserIDKey)
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Cache middleware sets the Cache-Control header
|
||||
func Cache(maxAge int) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
|
@ -5,8 +5,6 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var SessionIdentifierKey = "SESSION_IDENTIFIER"
|
||||
|
||||
// NoAuth is for routes that can only be accessed when the user is unauthenticated
|
||||
func NoAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
@ -8,10 +8,14 @@ import (
|
||||
"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 {
|
||||
return func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
sessionIdentifierInterface := session.Get(SessionIdentifierKey)
|
||||
sessionIdentifierInterface := session.Get(SessionIDKey)
|
||||
|
||||
if sessionIdentifier, ok := sessionIdentifierInterface.(string); ok {
|
||||
ses := models.Session{
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Throttle middleware takes a limit per minute and blocks any additional requests that go over this limit
|
||||
func Throttle(limit int) gin.HandlerFunc {
|
||||
store := memory.NewStore()
|
||||
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session holds information about user sessions and when they expire
|
||||
type Session struct {
|
||||
gorm.Model
|
||||
Identifier string
|
||||
@ -12,6 +13,7 @@ type Session struct {
|
||||
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 {
|
||||
return s.ExpiresAt.Before(time.Now())
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Token holds tokens typically used for user activation and password resets
|
||||
type Token struct {
|
||||
gorm.Model
|
||||
Value string
|
||||
@ -14,11 +15,14 @@ type Token struct {
|
||||
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 {
|
||||
return t.ExpiresAt.Before(time.Now())
|
||||
}
|
||||
|
||||
const (
|
||||
// TokenUserActivation is a constant used to identify tokens used for user activation
|
||||
TokenUserActivation string = "user_activation"
|
||||
// TokenPasswordReset is a constant used to identify tokens used for password resets
|
||||
TokenPasswordReset string = "password_reset"
|
||||
)
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// User holds information relating to users that use the application
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Email string
|
||||
|
@ -2,6 +2,7 @@ package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
// Website holds information about different websites
|
||||
type Website struct {
|
||||
gorm.Model
|
||||
Title string
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Activate handles requests used to activate a users account
|
||||
func (controller Controller) Activate(c *gin.Context) {
|
||||
activationError := "Please provide a valid activation token"
|
||||
activationSuccess := "Account activated. You may now proceed to login to your account."
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Admin renders the admin dashboard
|
||||
func (controller Controller) Admin(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Admin",
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
email2 "github.com/uberswe/golang-base-project/email"
|
||||
"github.com/uberswe/golang-base-project/models"
|
||||
"github.com/uberswe/golang-base-project/util"
|
||||
"github.com/uberswe/golang-base-project/ulid"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ForgotPassword renders the HTML page where a password request can be initiated
|
||||
func (controller Controller) ForgotPassword(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Forgot Password",
|
||||
@ -23,6 +24,7 @@ func (controller Controller) ForgotPassword(c *gin.Context) {
|
||||
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) {
|
||||
pd := PageData{
|
||||
Title: "Forgot Password",
|
||||
@ -47,7 +49,7 @@ func (controller Controller) ForgotPasswordPost(c *gin.Context) {
|
||||
|
||||
func (controller Controller) forgotPasswordEmailHandler(userID uint, email string) {
|
||||
forgotPasswordToken := models.Token{
|
||||
Value: util.GenerateULID(),
|
||||
Value: ulid.Generate(),
|
||||
Type: models.TokenPasswordReset,
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Index renders the HTML of the index page
|
||||
func (controller Controller) Index(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Home",
|
||||
|
@ -5,13 +5,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/uberswe/golang-base-project/middleware"
|
||||
"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"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Login renders the HTML of the login page
|
||||
func (controller Controller) Login(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Login",
|
||||
@ -21,6 +22,7 @@ func (controller Controller) Login(c *gin.Context) {
|
||||
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) {
|
||||
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{
|
||||
@ -72,7 +74,7 @@ func (controller Controller) LoginPost(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Generate a ulid for the current session
|
||||
sessionIdentifier := util.GenerateULID()
|
||||
sessionIdentifier := ulid.Generate()
|
||||
|
||||
ses := models.Session{
|
||||
Identifier: sessionIdentifier,
|
||||
@ -94,7 +96,7 @@ func (controller Controller) LoginPost(c *gin.Context) {
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Set(middleware.SessionIdentifierKey, sessionIdentifier)
|
||||
session.Set(middleware.SessionIDKey, sessionIdentifier)
|
||||
|
||||
err = session.Save()
|
||||
if err != nil {
|
||||
|
@ -8,9 +8,10 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Logout deletes the current user session and redirects the user to the index page
|
||||
func (controller Controller) Logout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Delete(middleware.SessionIdentifierKey)
|
||||
session.Delete(middleware.SessionIDKey)
|
||||
err := session.Save()
|
||||
log.Println(err)
|
||||
|
||||
|
@ -8,11 +8,13 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Controller holds all the variables needed for routes to perform their logic
|
||||
type Controller struct {
|
||||
db *gorm.DB
|
||||
config config.Config
|
||||
}
|
||||
|
||||
// New creates a new instance of the routes.Controller
|
||||
func New(db *gorm.DB, c config.Config) Controller {
|
||||
return Controller{
|
||||
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 {
|
||||
Title string
|
||||
Messages []Message
|
||||
@ -27,11 +30,13 @@ type PageData struct {
|
||||
CacheParameter string
|
||||
}
|
||||
|
||||
// Message holds a message which can be rendered as responses on HTML pages
|
||||
type Message struct {
|
||||
Type string // success, warning, error, etc.
|
||||
Content string
|
||||
}
|
||||
|
||||
// isAuthenticated checks if the current user is authenticated or not
|
||||
func isAuthenticated(c *gin.Context) bool {
|
||||
_, exists := c.Get(middleware.UserIDKey)
|
||||
return exists
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NoRoute handles rendering of the 404 page
|
||||
func (controller Controller) NoRoute(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "404 Not Found",
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
email2 "github.com/uberswe/golang-base-project/email"
|
||||
"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"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Register renders the HTML content of the register page
|
||||
func (controller Controller) Register(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Register",
|
||||
@ -25,6 +26,7 @@ func (controller Controller) Register(c *gin.Context) {
|
||||
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) {
|
||||
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."
|
||||
@ -121,7 +123,7 @@ func (controller Controller) RegisterPost(c *gin.Context) {
|
||||
|
||||
func (controller Controller) activationEmailHandler(userID uint, email string) {
|
||||
activationToken := models.Token{
|
||||
Value: util.GenerateULID(),
|
||||
Value: ulid.Generate(),
|
||||
Type: models.TokenUserActivation,
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ResendActivation renders the HTML page used to request a new activation email
|
||||
func (controller Controller) ResendActivation(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Resend Activation Email",
|
||||
@ -16,6 +17,7 @@ func (controller Controller) ResendActivation(c *gin.Context) {
|
||||
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) {
|
||||
pd := PageData{
|
||||
Title: "Resend Activation Email",
|
||||
|
@ -8,11 +8,13 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ResetPasswordPageData defines additional data needed to render the reset password page
|
||||
type ResetPasswordPageData struct {
|
||||
PageData
|
||||
Token string
|
||||
}
|
||||
|
||||
// ResetPassword renders the HTML page for resetting the users password
|
||||
func (controller Controller) ResetPassword(c *gin.Context) {
|
||||
token := c.Param("token")
|
||||
pd := ResetPasswordPageData{
|
||||
@ -26,6 +28,7 @@ func (controller Controller) ResetPassword(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "resetpassword.html", pd)
|
||||
}
|
||||
|
||||
// ResetPasswordPost handles post request used to reset users passwords
|
||||
func (controller Controller) ResetPasswordPost(c *gin.Context) {
|
||||
passwordError := "Your password must be 8 characters in length or longer"
|
||||
resetError := "Could not reset password, please try again"
|
||||
|
@ -8,11 +8,13 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SearchData holds additional data needed to render the search HTML page
|
||||
type SearchData struct {
|
||||
PageData
|
||||
Results []models.Website
|
||||
}
|
||||
|
||||
// Search renders the search HTML page and any search results
|
||||
func (controller Controller) Search(c *gin.Context) {
|
||||
pd := SearchData{
|
||||
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 (
|
||||
"github.com/oklog/ulid/v2"
|
||||
@ -6,7 +6,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenerateULID() string {
|
||||
// Generate a new ULID string
|
||||
func Generate() string {
|
||||
entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0)
|
||||
res := ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
|
||||
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