From e586933a6b80b81119217eeeb83668697ccc205d Mon Sep 17 00:00:00 2001 From: Markus Tenghamn Date: Sun, 9 Jan 2022 14:42:03 +0100 Subject: [PATCH] Improves documentation (#13) * Documents all env variables and adds an example project * Adds godoc comments * Fixed package naming issue --- README.md | 104 ++++++++++++++++++++++++++++--- config/main.go | 1 + email/main.go | 11 ++-- env.go | 6 +- main.go | 33 +++++++++- middleware/auth.go | 2 + middleware/cache.go | 1 + middleware/noauth.go | 2 - middleware/session.go | 6 +- middleware/throttle.go | 1 + models/session.go | 2 + models/token.go | 6 +- models/user.go | 1 + models/website.go | 1 + routes/activate.go | 1 + routes/admin.go | 1 + routes/forgotpassword.go | 6 +- routes/index.go | 1 + routes/login.go | 8 ++- routes/logout.go | 3 +- routes/main.go | 5 ++ routes/noroute.go | 1 + routes/register.go | 6 +- routes/resendactivation.go | 2 + routes/resetpassword.go | 3 + routes/search.go | 2 + text/html.go | 24 +++++++ text/random.go | 15 +++++ text/segment.go | 17 +++++ util/ulid.go => ulid/generate.go | 5 +- util/text.go | 49 --------------- 31 files changed, 248 insertions(+), 78 deletions(-) create mode 100644 text/html.go create mode 100644 text/random.go create mode 100644 text/segment.go rename util/ulid.go => ulid/generate.go (76%) delete mode 100644 util/text.go diff --git a/README.md b/README.md index fdb1284..eb380de 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # Golang Base Project +[![GoDoc](https://godoc.org/github.com/uberswe/golang-base-project?status.svg)](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). diff --git a/config/main.go b/config/main.go index 7019647..6aca277 100644 --- a/config/main.go +++ b/config/main.go @@ -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 diff --git a/email/main.go b/email/main.go index f21b7c0..6a7964f 100644 --- a/email/main.go +++ b/email/main.go @@ -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. diff --git a/env.go b/env.go index 2518ee1..51a4135 100644 --- a/env.go +++ b/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")) diff --git a/main.go b/main.go index 2940d60..5033fed 100644 --- a/main.go +++ b/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) diff --git a/middleware/auth.go b/middleware/auth.go index b7c991b..aad38d5 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -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) diff --git a/middleware/cache.go b/middleware/cache.go index 306e5fc..1f7d22f 100644 --- a/middleware/cache.go +++ b/middleware/cache.go @@ -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)) diff --git a/middleware/noauth.go b/middleware/noauth.go index 36f10e7..5fdee1e 100644 --- a/middleware/noauth.go +++ b/middleware/noauth.go @@ -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) { diff --git a/middleware/session.go b/middleware/session.go index a791477..e91af0c 100644 --- a/middleware/session.go +++ b/middleware/session.go @@ -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{ diff --git a/middleware/throttle.go b/middleware/throttle.go index 86d8b3f..501a9ef 100644 --- a/middleware/throttle.go +++ b/middleware/throttle.go @@ -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() diff --git a/models/session.go b/models/session.go index 0c631c8..726a3aa 100644 --- a/models/session.go +++ b/models/session.go @@ -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()) } diff --git a/models/token.go b/models/token.go index 40e589b..24f4158 100644 --- a/models/token.go +++ b/models/token.go @@ -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 string = "password_reset" + // TokenPasswordReset is a constant used to identify tokens used for password resets + TokenPasswordReset string = "password_reset" ) diff --git a/models/user.go b/models/user.go index 6e7f944..cada53a 100644 --- a/models/user.go +++ b/models/user.go @@ -6,6 +6,7 @@ import ( "time" ) +// User holds information relating to users that use the application type User struct { gorm.Model Email string diff --git a/models/website.go b/models/website.go index e3f2e45..97b5f02 100644 --- a/models/website.go +++ b/models/website.go @@ -2,6 +2,7 @@ package models import "gorm.io/gorm" +// Website holds information about different websites type Website struct { gorm.Model Title string diff --git a/routes/activate.go b/routes/activate.go index b8ff4e6..ed88169 100644 --- a/routes/activate.go +++ b/routes/activate.go @@ -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." diff --git a/routes/admin.go b/routes/admin.go index e436eaf..726492f 100644 --- a/routes/admin.go +++ b/routes/admin.go @@ -5,6 +5,7 @@ import ( "net/http" ) +// Admin renders the admin dashboard func (controller Controller) Admin(c *gin.Context) { pd := PageData{ Title: "Admin", diff --git a/routes/forgotpassword.go b/routes/forgotpassword.go index 6e7be55..775bfa3 100644 --- a/routes/forgotpassword.go +++ b/routes/forgotpassword.go @@ -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, } diff --git a/routes/index.go b/routes/index.go index 4cc8dac..e92081d 100644 --- a/routes/index.go +++ b/routes/index.go @@ -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", diff --git a/routes/login.go b/routes/login.go index 7aeb0b8..a65d42c 100644 --- a/routes/login.go +++ b/routes/login.go @@ -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 { diff --git a/routes/logout.go b/routes/logout.go index 8dd22b2..7346fcc 100644 --- a/routes/logout.go +++ b/routes/logout.go @@ -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) diff --git a/routes/main.go b/routes/main.go index 19b94ab..77f27ce 100644 --- a/routes/main.go +++ b/routes/main.go @@ -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 diff --git a/routes/noroute.go b/routes/noroute.go index ddd89d9..66d16c5 100644 --- a/routes/noroute.go +++ b/routes/noroute.go @@ -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", diff --git a/routes/register.go b/routes/register.go index 9b1a880..5bf58ce 100644 --- a/routes/register.go +++ b/routes/register.go @@ -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, } diff --git a/routes/resendactivation.go b/routes/resendactivation.go index 2d710d1..0103d1f 100644 --- a/routes/resendactivation.go +++ b/routes/resendactivation.go @@ -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", diff --git a/routes/resetpassword.go b/routes/resetpassword.go index 4892400..41f5a24 100644 --- a/routes/resetpassword.go +++ b/routes/resetpassword.go @@ -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" diff --git a/routes/search.go b/routes/search.go index 2bf771d..e079ff4 100644 --- a/routes/search.go +++ b/routes/search.go @@ -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{ diff --git a/text/html.go b/text/html.go new file mode 100644 index 0000000..d305bc6 --- /dev/null +++ b/text/html.go @@ -0,0 +1,24 @@ +package text + +import ( + "fmt" + "net/url" + "strings" +) + +// Nl2Br converts \n to HTML
tags +func Nl2Br(s string) string { + return strings.Replace(s, "\n", "
", -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("%s", p, p), 1) + } + } + return strings.Replace(s, " \n ", "\n", -1) +} diff --git a/text/random.go b/text/random.go new file mode 100644 index 0000000..699b202 --- /dev/null +++ b/text/random.go @@ -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) +} diff --git a/text/segment.go b/text/segment.go new file mode 100644 index 0000000..7c72ef7 --- /dev/null +++ b/text/segment.go @@ -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] +} diff --git a/util/ulid.go b/ulid/generate.go similarity index 76% rename from util/ulid.go rename to ulid/generate.go index 52ae290..c60203a 100644 --- a/util/ulid.go +++ b/ulid/generate.go @@ -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() diff --git a/util/text.go b/util/text.go deleted file mode 100644 index 73af946..0000000 --- a/util/text.go +++ /dev/null @@ -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", "
", -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("%s", 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) -}