Archived
Template
1
0

Improves documentation (#13)

* Documents all env variables and adds an example project

* Adds godoc comments

* Fixed package naming issue
This commit is contained in:
Markus Tenghamn
2022-01-09 14:42:03 +01:00
committed by GitHub
parent 5d01717111
commit e586933a6b
31 changed files with 248 additions and 78 deletions

104
README.md
View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import (
"time"
)
// User holds information relating to users that use the application
type User struct {
gorm.Model
Email string

View File

@ -2,6 +2,7 @@ package models
import "gorm.io/gorm"
// Website holds information about different websites
type Website struct {
gorm.Model
Title string

View File

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

View File

@ -5,6 +5,7 @@ import (
"net/http"
)
// Admin renders the admin dashboard
func (controller Controller) Admin(c *gin.Context) {
pd := PageData{
Title: "Admin",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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