Added license. Cleanup and expanded documentation.
This commit is contained in:
parent
d35cf1d8a6
commit
86774ae781
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Mike Stefanello
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
26
Makefile
26
Makefile
@ -1,42 +1,52 @@
|
|||||||
|
# Connect to the primary database
|
||||||
.PHONY: db
|
.PHONY: db
|
||||||
db:
|
db:
|
||||||
psql postgresql://admin:admin@localhost:5432/app
|
psql postgresql://admin:admin@localhost:5432/app
|
||||||
|
|
||||||
|
# Connect to the test database
|
||||||
.PHONY: db-test
|
.PHONY: db-test
|
||||||
db-test:
|
db-test:
|
||||||
psql postgresql://admin:admin@localhost:5432/app_test
|
psql postgresql://admin:admin@localhost:5432/app_test
|
||||||
|
|
||||||
|
# Connect to the cache
|
||||||
.PHONY: cache
|
.PHONY: cache
|
||||||
cache:
|
cache:
|
||||||
redis-cli
|
redis-cli
|
||||||
|
|
||||||
.PHONY: ent-gen
|
# Install Ent code-generation module
|
||||||
ent-gen:
|
|
||||||
go generate ./ent
|
|
||||||
|
|
||||||
.PHONY: ent-new
|
|
||||||
ent-new:
|
|
||||||
go run entgo.io/ent/cmd/ent init $(name)
|
|
||||||
|
|
||||||
.PHONY: ent-install
|
.PHONY: ent-install
|
||||||
ent-install:
|
ent-install:
|
||||||
go get -d entgo.io/ent/cmd/ent
|
go get -d entgo.io/ent/cmd/ent
|
||||||
|
|
||||||
|
# Generate Ent code
|
||||||
|
.PHONY: ent-gen
|
||||||
|
ent-gen:
|
||||||
|
go generate ./ent
|
||||||
|
|
||||||
|
# Create a new Ent entity
|
||||||
|
.PHONY: ent-new
|
||||||
|
ent-new:
|
||||||
|
go run entgo.io/ent/cmd/ent init $(name)
|
||||||
|
|
||||||
|
# Start the Docker containers
|
||||||
.PHONY: up
|
.PHONY: up
|
||||||
up:
|
up:
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
sleep 3
|
sleep 3
|
||||||
|
|
||||||
|
# Rebuild Docker containers to wipe all data
|
||||||
.PHONY: reset
|
.PHONY: reset
|
||||||
reset:
|
reset:
|
||||||
docker-compose down
|
docker-compose down
|
||||||
make up
|
make up
|
||||||
|
|
||||||
|
# Run the application
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run:
|
run:
|
||||||
clear
|
clear
|
||||||
go run main.go
|
go run main.go
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
go test -p 1 ./...
|
go test -p 1 ./...
|
53
README.md
53
README.md
@ -1,6 +1,5 @@
|
|||||||
## (NAME) - Rapid, easy full-stack web development starter kit in Go
|
## (NAME) - Rapid, easy full-stack web development starter kit in Go
|
||||||
|
|
||||||
----
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
* [Introduction](#introduction)
|
* [Introduction](#introduction)
|
||||||
* [Overview](#overview)
|
* [Overview](#overview)
|
||||||
@ -59,6 +58,7 @@
|
|||||||
* [Headers](#headers)
|
* [Headers](#headers)
|
||||||
* [Status code](#status-code)
|
* [Status code](#status-code)
|
||||||
* [Metatags](#metatags)
|
* [Metatags](#metatags)
|
||||||
|
* [URL and link generation](#url-and-link-generation)
|
||||||
* [HTMX support](#htmx-support)
|
* [HTMX support](#htmx-support)
|
||||||
* [Rendering the page](#rendering-the-page)
|
* [Rendering the page](#rendering-the-page)
|
||||||
* [Template renderer](#template-renderer)
|
* [Template renderer](#template-renderer)
|
||||||
@ -175,7 +175,7 @@ It is common that your tests will require access to dependencies, like the datab
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The `config` package provides a flexible, extensible way to store all configuration for the application. Configuration is added to the _Container_ as a _Service_, making it accessible across most of the application.
|
The `config` package provides a flexible, extensible way to store all configuration for the application. Configuration is added to the `Container` as a _Service_, making it accessible across most of the application.
|
||||||
|
|
||||||
Be sure to review and adjust all of the default configuration values provided.
|
Be sure to review and adjust all of the default configuration values provided.
|
||||||
|
|
||||||
@ -256,7 +256,7 @@ While you should refer to their [documentation](https://entgo.io/docs/getting-st
|
|||||||
The generated code is extremely flexible and impressive. An example to highlight this is one used within this application:
|
The generated code is extremely flexible and impressive. An example to highlight this is one used within this application:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
entity, err := ORM.PasswordToken.
|
entity, err := c.ORM.PasswordToken.
|
||||||
Query().
|
Query().
|
||||||
Where(passwordtoken.HasUserWith(user.ID(userID))).
|
Where(passwordtoken.HasUserWith(user.ID(userID))).
|
||||||
Where(passwordtoken.CreatedAtGTE(expiration)).
|
Where(passwordtoken.CreatedAtGTE(expiration)).
|
||||||
@ -327,7 +327,7 @@ If you wish to require either authentication or non-authentication for a given r
|
|||||||
|
|
||||||
## Routes
|
## Routes
|
||||||
|
|
||||||
The router functionality is provided by [Echo](https://echo.labstack.com/guide/routing/) and constructed within via the `BuildRouter()` function inside `routes/router.go`. Since the _Echo_ instance is a _Service_ on the _Container_ which is passed in to `BuildRouter()`, middleware and routes can be added directly to it.
|
The router functionality is provided by [Echo](https://echo.labstack.com/guide/routing/) and constructed within via the `BuildRouter()` function inside `routes/router.go`. Since the _Echo_ instance is a _Service_ on the `Container` which is passed in to `BuildRouter()`, middleware and routes can be added directly to it.
|
||||||
|
|
||||||
### Custom middleware
|
### Custom middleware
|
||||||
|
|
||||||
@ -365,14 +365,14 @@ func (c *Home) Post(ctx echo.Context) error {}
|
|||||||
Then create the route and add to the router:
|
Then create the route and add to the router:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
home := Home{Controller: controller.NewController(c)}
|
home := Home{Controller: controller.NewController(c)}
|
||||||
g.GET("/", home.Get).Name = "home"
|
g.GET("/", home.Get).Name = "home"
|
||||||
g.POST("/", home.Post).Name = "home.post"
|
g.POST("/", home.Post).Name = "home.post"
|
||||||
```
|
```
|
||||||
|
|
||||||
Your route will now have all methods available on the `Controller` as well as access to the `Container`. It's not required to name the route methods to match the HTTP method.
|
Your route will now have all methods available on the `Controller` as well as access to the `Container`. It's not required to name the route methods to match the HTTP method.
|
||||||
|
|
||||||
**It is highly recommended** that you name your routes. Most methods on the back and frontend leverage the route name and parameters in order to generate URLs.
|
**It is highly recommended** that you provide a `Name` for your routes. Most methods on the back and frontend leverage the route name and parameters in order to generate URLs.
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
@ -587,14 +587,14 @@ var form ContactForm
|
|||||||
ctx.Set(context.FormKey, &form)
|
ctx.Set(context.FormKey, &form)
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the input in the POST data to map to the struct so it becomes populated:
|
Parse the input in the POST data to map to the struct so it becomes populated. This uses the `form` struct tags to map form values to the struct fields.
|
||||||
```go
|
```go
|
||||||
if err := ctx.Bind(&form); err != nil {
|
if err := ctx.Bind(&form); err != nil {
|
||||||
// Something went wrong...
|
// Something went wrong...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Process the submissions which uses [validator](https://github.com/go-playground/validator) to check for validation errors:
|
Process the submission which uses [validator](https://github.com/go-playground/validator) to check for validation errors:
|
||||||
```go
|
```go
|
||||||
if err := form.Submission.Process(ctx, form); err != nil {
|
if err := form.Submission.Process(ctx, form); err != nil {
|
||||||
// Something went wrong...
|
// Something went wrong...
|
||||||
@ -644,7 +644,7 @@ First, include a status class on the element so it will highlight green or red b
|
|||||||
```
|
```
|
||||||
|
|
||||||
Second, render the error messages, if there are any for a given field:
|
Second, render the error messages, if there are any for a given field:
|
||||||
```
|
```go
|
||||||
{{template "field-errors" (.Form.Submission.GetFieldErrors "Email")}}
|
{{template "field-errors" (.Form.Submission.GetFieldErrors "Email")}}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -686,6 +686,35 @@ page.Metatags.Keywords = []string{"Go", "Software"}
|
|||||||
|
|
||||||
A _component_ template is included to render metatags in `core.gohtml` which can be used by adding `{{template "metatags" .}}` to your _layout_.
|
A _component_ template is included to render metatags in `core.gohtml` which can be used by adding `{{template "metatags" .}}` to your _layout_.
|
||||||
|
|
||||||
|
### URL and link generation
|
||||||
|
|
||||||
|
Generating URLs in the templates is made easy if you follow the [routing patterns](#patterns) and provide names for your routes. Echo provides a `Reverse` function to generate a route URL with a given route name and optional parameters. This function is made accessible to the templates via the `Page` field `ToURL`.
|
||||||
|
|
||||||
|
As an example, if you have route such as:
|
||||||
|
```go
|
||||||
|
profile := Profile{Controller: ctr}
|
||||||
|
e.GET("/user/profile/:user", profile.Get).Name = "user_profile"
|
||||||
|
```
|
||||||
|
|
||||||
|
And you want to generate a URL in the template, you can:
|
||||||
|
```go
|
||||||
|
{{call .ToURL "user_profile" 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
Which will generate: `/user/profile/1`
|
||||||
|
|
||||||
|
There is also a helper function provided in the [funcmap](#funcmap) to generate links which has the benefit of adding an _active_ class if the link URL matches the current path. This is especially useful for navigation menus.
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{link (call .ToURL "user_profile" .AuthUser.ID) "Profile" .Path "extra-class"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Will generate:
|
||||||
|
```html
|
||||||
|
<a href="/user/profile/1" class="is-active extra-class">Profile</a>
|
||||||
|
```
|
||||||
|
Assuming the current _path_ is `/user/profile/1`; otherwise the `is-active` class will be excluded.
|
||||||
|
|
||||||
### HTMX support
|
### HTMX support
|
||||||
|
|
||||||
[HTMX](https://htmx.org/) is an incredible JavaScript library allows you to access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext.
|
[HTMX](https://htmx.org/) is an incredible JavaScript library allows you to access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext.
|
||||||
@ -818,7 +847,7 @@ For example, to render a file located in `static/picture.png`, you would use:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Which would result in:
|
Which would result in:
|
||||||
```go
|
```html
|
||||||
<img src="/files/picture.png?v=9fhe73kaf3"/>
|
<img src="/files/picture.png?v=9fhe73kaf3"/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Headers (https://.org/docs/#requests)
|
// Headers (https://htmx.org/docs/#requests)
|
||||||
const (
|
const (
|
||||||
HeaderRequest = "HX-Request"
|
HeaderRequest = "HX-Request"
|
||||||
HeaderBoosted = "HX-Boosted"
|
HeaderBoosted = "HX-Boosted"
|
||||||
|
2
main.go
2
main.go
@ -52,9 +52,9 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds.
|
// Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds.
|
||||||
// Use a buffered channel to avoid missing signals as recommended for signal.Notify
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, os.Interrupt)
|
signal.Notify(quit, os.Interrupt)
|
||||||
|
signal.Notify(quit, os.Kill)
|
||||||
<-quit
|
<-quit
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -84,12 +84,10 @@ func (c *ForgotPassword) Post(ctx echo.Context) error {
|
|||||||
ctx.Logger().Infof("generated password reset token for user %d", u.ID)
|
ctx.Logger().Infof("generated password reset token for user %d", u.ID)
|
||||||
|
|
||||||
// Email the user
|
// Email the user
|
||||||
body := fmt.Sprintf(
|
err = c.Container.Mail.Send(ctx, u.Email, fmt.Sprintf(
|
||||||
"Go here to reset your password: %s",
|
"Go here to reset your password: %s",
|
||||||
ctx.Echo().Reverse("reset_password", u.ID, token),
|
ctx.Echo().Reverse("reset_password", u.ID, token),
|
||||||
)
|
))
|
||||||
ctx.Logger().Info(body)
|
|
||||||
err = c.Container.Mail.Send(ctx, u.Email, body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Fail(ctx, err, "error sending password reset email")
|
return c.Fail(ctx, err, "error sending password reset email")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user