You've already forked golang-base-project
Adds godoc comments
This commit is contained in:
@ -1,5 +1,7 @@
|
||||
# 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)
|
||||
@ -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.
|
||||
|
||||
## 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.
|
||||
|
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