Archived
Template
1
0

Adds godoc comments

This commit is contained in:
uberswe
2022-01-09 14:22:43 +01:00
parent 2d3c0df4e5
commit 8c37faf016
30 changed files with 154 additions and 68 deletions

View File

@ -1,5 +1,7 @@
# Golang Base Project # 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. 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).

View File

@ -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

View File

@ -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
View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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) {

View File

@ -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{

View File

@ -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()

View File

@ -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())
} }

View File

@ -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"
) )

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -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",

View File

@ -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,
} }

View File

@ -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",

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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",

View File

@ -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,
} }

View File

@ -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",

View File

@ -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"

View File

@ -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
View 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
View 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
View 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]
}

View File

@ -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()

View File

@ -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)
}