Compare commits
7 commits
60009df0bf
...
fc5db0e95a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5db0e95a | ||
|
|
9e15bceace | ||
|
|
3cfcb43031 | ||
|
|
52f87580a0 | ||
|
|
a53bdf9a1b | ||
|
|
77cabe5f12 | ||
|
|
1a6874fd82 |
54 changed files with 2299 additions and 353 deletions
6
Makefile
6
Makefile
|
|
@ -15,9 +15,13 @@ ent-gen: ## Generate Ent code
|
|||
go generate ./ent
|
||||
|
||||
.PHONY: ent-new
|
||||
ent-new: ## Create a new Ent entity (ie, make ent-new NAME=MyEntity)
|
||||
ent-new: ## Create a new Ent entity (ie, make ent-new name=MyEntity)
|
||||
go run entgo.io/ent/cmd/ent new $(name)
|
||||
|
||||
.PHONY: admin
|
||||
admin: ## Create a new admin user (ie, make admin email=myemail@web.com)
|
||||
go run cmd/admin/main.go --email=$(email)
|
||||
|
||||
.PHONY: run
|
||||
run: ## Run the application
|
||||
clear
|
||||
|
|
|
|||
108
README.md
108
README.md
|
|
@ -19,6 +19,8 @@
|
|||
* [Screenshots](#screenshots)
|
||||
* [Getting started](#getting-started)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Getting the code](#getting-the-code)
|
||||
* [Create an admin account](#create-an-admin-account)
|
||||
* [Start the application](#start-the-application)
|
||||
* [Live reloading](#live-reloading)
|
||||
* [Service container](#service-container)
|
||||
|
|
@ -39,9 +41,15 @@
|
|||
* [Login / Logout](#login--logout)
|
||||
* [Forgot password](#forgot-password)
|
||||
* [Registration](#registration)
|
||||
* [Admins](#admins)
|
||||
* [Authenticated user](#authenticated-user)
|
||||
* [Middleware](#middleware)
|
||||
* [Email verification](#email-verification)
|
||||
* [Admin panel](#admin-panel)
|
||||
* [Code generation](#code-generation)
|
||||
* [Access](#access)
|
||||
* [Considerations](#considerations)
|
||||
* [Roadmap](#roadmap)
|
||||
* [Routes](#routes)
|
||||
* [Custom middleware](#custom-middleware)
|
||||
* [Handlers](#handlers)
|
||||
|
|
@ -80,6 +88,7 @@
|
|||
* [Tasks](#tasks)
|
||||
* [Queues](#queues)
|
||||
* [Dispatcher](#dispatcher)
|
||||
* [Monitoring tasks and queues](#monitoring-tasks-and-queues)
|
||||
* [Cron](#cron)
|
||||
* [Files](#files)
|
||||
* [Static files](#static-files)
|
||||
|
|
@ -88,7 +97,6 @@
|
|||
* [Email](#email)
|
||||
* [HTTPS](#https)
|
||||
* [Logging](#logging)
|
||||
* [Roadmap](#roadmap)
|
||||
* [Credits](#credits)
|
||||
|
||||
## Introduction
|
||||
|
|
@ -129,15 +137,27 @@ Originally, Postgres and Redis were chosen as defaults but since the aim of this
|
|||
|
||||
#### Inline form validation
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/552328/147838632-570a3116-1e74-428f-8bfc-523ed309ef06.png" alt="Inline validation"/>
|
||||
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/pagoda/inline-validation.png" alt="Inline validation"/>
|
||||
|
||||
#### Switch layout templates, user registration
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/552328/147838633-c1b3e4f6-bbfd-44e1-b0be-884d1a83f8f4.png" alt="Registration"/>
|
||||
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/pagoda/register.png" alt="Registration"/>
|
||||
|
||||
#### Alpine.js modal, HTMX AJAX request
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/552328/147838634-4b84c5d5-dc3b-4280-ac12-247ab22184a3.png" alt="Alpine and HTMX"/>
|
||||
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/pagoda/modal.png" alt="Alpine and HTMX"/>
|
||||
|
||||
#### User entity list (admin panel)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/pagoda/admin-user_list.png" alt="User entity list"/>
|
||||
|
||||
#### User entity edit (admin panel)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/pagoda/admin-user_edit.png" alt="User entity edit"/>
|
||||
|
||||
#### Monitor task queues (provided by Backlite via the admin panel)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/backlite/failed.png" alt="Manage task queues"/>
|
||||
|
||||
## Getting started
|
||||
|
||||
|
|
@ -145,17 +165,24 @@ Originally, Postgres and Redis were chosen as defaults but since the aim of this
|
|||
|
||||
Ensure that [Go](https://go.dev/) is installed on your system.
|
||||
|
||||
### Start the application
|
||||
### Getting the code
|
||||
|
||||
After checking out the repository, from within the root, simply run `make run`:
|
||||
Start by checking out the repository. Since this repository is a _template_ and not a Go _library_, you **do not** use `go get`.
|
||||
|
||||
```
|
||||
git clone git@github.com:mikestefanello/pagoda.git
|
||||
cd pagoda
|
||||
make run
|
||||
```
|
||||
|
||||
Since this repository is a _template_ and not a Go _library_, you **do not** use `go get`.
|
||||
### Create an admin account
|
||||
|
||||
In order to access the [admin panel](#admin-panel), you must log in with an admin user and in order to create your first admin user account, you must use the command-line. Execute `make admin email=your@email.com` from the root of the codebase, and an admin account will be generated using that email address. The console will print the randomly-generated password for the account.
|
||||
|
||||
Once you have one admin account, you can use that account to manage other users and admins from within the UI.
|
||||
|
||||
### Start the application
|
||||
|
||||
From within the root of the codebase, simply run `make run`.
|
||||
|
||||
By default, you should be able to access the application in your browser at `localhost:8000`. Your data will be stored within the `dbs` directory. If you ever want to quickly delete all data, just remove this directory.
|
||||
|
||||
|
|
@ -174,6 +201,7 @@ The container is located at `pkg/services/container.go` and is meant to house al
|
|||
- Configuration
|
||||
- Database
|
||||
- Files
|
||||
- Graph
|
||||
- Mail
|
||||
- ORM
|
||||
- Tasks
|
||||
|
|
@ -184,7 +212,7 @@ A new container can be created and initialized via `services.NewContainer()`. It
|
|||
|
||||
### Dependency injection
|
||||
|
||||
The container exists to faciliate easy dependency-injection both for services within the container as well as areas of your application that require any of these dependencies. For example, the container is automatically passed to the `Init()` method of your route [handlers](#handlers) so that the handlers have full, easy access to all services.
|
||||
The container exists to facilitate easy dependency-injection both for services within the container and areas of your application that require any of these dependencies. For example, the container is automatically passed to the `Init()` method of your route [handlers](#handlers) so that the handlers have full, easy access to all services.
|
||||
|
||||
### Test dependencies
|
||||
|
||||
|
|
@ -255,11 +283,11 @@ When this project was using Postgres, it would automatically drop and recreate t
|
|||
|
||||
## ORM
|
||||
|
||||
As previously mentioned, [Ent](https://entgo.io/) is the supplied ORM. It can swapped out, but I highly recommend it. I don't think there is anything comparable for Go, at the current time. If you're not familiar with Ent, take a look through their top-notch [documentation](https://entgo.io/docs/getting-started).
|
||||
As previously mentioned, [Ent](https://entgo.io/) is the supplied ORM. It can be swapped out, but I highly recommend it. I don't think there is anything comparable for Go, at the current time. If you decide to remove Ent, you will lose the dynamic [admin panel](#admin-panel) which allows you to administer all entity types from within the UI. If you're not familiar with Ent, take a look through their top-notch [documentation](https://entgo.io/docs/getting-started).
|
||||
|
||||
An Ent client is included in the `Container` to provide easy access to the ORM throughout the application.
|
||||
|
||||
Ent relies on code-generation for the entities you create to provide robust, type-safe data operations. Everything within the `ent` package in this repository is generated code for the two entity types listed below with the exception of the schema declaration.
|
||||
Ent relies on code-generation for the entities you create to provide robust, type-safe data operations. Everything within the `ent` directory in this repository is generated code for the two entity types listed below except the [schema declaration](#https://github.com/mikestefanello/pagoda/tree/main/ent/schema) and [custom extension](https://github.com/mikestefanello/pagoda/tree/main/ent/admin) to generate code for the [admin panel](#admin-panel).
|
||||
|
||||
### Entity types
|
||||
|
||||
|
|
@ -343,6 +371,11 @@ The actual registration of a user is not handled within the `AuthClient` but rat
|
|||
|
||||
A route is provided for the user to register at `user/register`.
|
||||
|
||||
|
||||
### Admins
|
||||
|
||||
A checkbox field has been added to the `User` entity type to indicate if the user has admin access. If your app requires a more robust authorization system, such as roles and permissions, you could easily replace this field and adjust all usage of it accordingly. If a user has this field checked, they will be able to access the [admin panel](#admin-panel). [Middleware](#middleware) is provided to easily restrict access to routes based on admin status.
|
||||
|
||||
### Authenticated user
|
||||
|
||||
The `AuthClient` has two methods available to get either the `User` entity or the ID of the user currently logged in for a given request. Those methods are `GetAuthenticatedUser()` and `GetAuthenticatedUserID()`.
|
||||
|
|
@ -353,6 +386,8 @@ Registered for all routes is middleware that will load the currently logged in u
|
|||
|
||||
If you wish to require either authentication or non-authentication for a given route, you can use either `middleware.RequireAuthentication()` or `middleware.RequireNoAuthentication()`.
|
||||
|
||||
If you wish to restrict a route to admins only, you can use `middleware.RequireAdmin`.
|
||||
|
||||
### Email verification
|
||||
|
||||
Most web applications require the user to verify their email address (or other form of contact information). The `User` entity has a field `Verified` to indicate if they have verified themself. When a user successfully registers, an email is sent to them containing a link with a token that will verify their account when visited. This route is currently accessible at `/email/verify/:token` and handled by `pkg/handlers/auth.go`.
|
||||
|
|
@ -367,6 +402,45 @@ Be sure to review the [email](#email) section since actual email sending is not
|
|||
|
||||
To generate a new verification token, the `AuthClient` has a method `GenerateEmailVerificationToken()` which creates a token for a given email address. To verify the token, pass it in to `ValidateEmailVerificationToken()` which will return the email address associated with the token and an error if the token is invalid.
|
||||
|
||||
## Admin panel
|
||||
|
||||
The admin panel functionality is considered to be in _beta_ and remains under active development, though all features described here are expected to be fully-functional. Please use caution when using these features and be sure to report any issues you encounter.
|
||||
|
||||
The _admin panel_ currently includes:
|
||||
* A completely dynamic UI to manage all entities defined by _Ent_.
|
||||
* A section to monitor all [background tasks and queues](#tasks).
|
||||
|
||||
There are no separate templates or interfaces for the admin section (see [screenshots](#screenshots)).
|
||||
|
||||
Users with admin [access](#access) will see additional links on the default sidebar at the bottom. As with all default UI components, you can easily move these pages and links to a dedicated section, layout, etc. Clicking on the link for any given entity type will provide a pageable table of entities and the ability to add/edit/delete.
|
||||
|
||||
### Code generation
|
||||
|
||||
In order to automatically and dynamically provide admin functionality for entities, code generation is used by means of leveraging Ent's [extension API](https://entgo.io/docs/extensions) which makes generating code using the Ent graph schema very easy. A [custom extension](https://github.com/mikestefanello/pagoda/blob/master/ent/admin/extension.go) is provided to generate code that provides flat entity type structs and handler code that work directly with Echo. So, both of those are required in order for any of this to work. Whenever you modify one of your entity types or generate a new one, the admin code will also automatically generate.
|
||||
|
||||
Without going in to too much detail here, the generated code provides a [handler](https://github.com/mikestefanello/pagoda/blob/master/ent/admin/handler.go) that is then used by a provided [web handler](https://github.com/mikestefanello/pagoda/blob/master/pkg/handlers/admin.go) to power all the routes used in the admin UI. While the rest of the related code should be simple enough to follow, it's worth calling attention to the highly-dynamic [entity form](https://github.com/mikestefanello/pagoda/blob/master/pkg/ui/forms/admin_entity.go) that is constructed using the _Ent_ graph data structure.
|
||||
|
||||
### Access
|
||||
|
||||
Only admin users can access the admin panel. The details are outlined in the [admins](#admins) and [middleware](#middleware) sections. If you haven't yet generated an admin user, follow [these instructions](#create-an-admin-account).
|
||||
|
||||
### Considerations
|
||||
|
||||
Since the generated code is completely dynamic, all entity functionality related to creating and editing must be defined within your _Ent_ schema. Refer to the [User](https://github.com/mikestefanello/pagoda/blob/master/ent/schema/user.go) entity schema as an example.
|
||||
- Field validation must be defined within each entity field (ie, validating an email address in a _string_ field).
|
||||
- Pre-processing must be defined within entity hooks (ie, hashing the user's password).
|
||||
- _Sensitive_ fields will be omitted from the UI, and only modified if a value is provided during creation or editing.
|
||||
- _Edges_ must be bound to an [edge field](https://entgo.io/docs/schema-edges#edge-field) if you want them visible and editable.
|
||||
|
||||
### Roadmap
|
||||
|
||||
* Determine which tests should be included and provide them.
|
||||
* Inline validation.
|
||||
* Either exposed sorting, or allow the _handler_ to be configured with sort criteria for each type.
|
||||
* Exposed filters.
|
||||
* Support all field types (types such as _JSON_ as currently not supported).
|
||||
* Control which fields appear in the entity list table.
|
||||
|
||||
## Routes
|
||||
|
||||
The router functionality is provided by [Echo](https://echo.labstack.com/guide/routing/) and constructed within via the `BuildRouter()` function inside `pkg/handlers/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.
|
||||
|
|
@ -923,6 +997,10 @@ The app [configuration](#configuration) contains values to configure the client
|
|||
|
||||
When the app is shutdown, the dispatcher is given 10 seconds to wait for any in-progress tasks to finish execution. This can be changed in `cmd/web/main.go`.
|
||||
|
||||
### Monitoring tasks and queues
|
||||
|
||||
The [admin panel](#admin-panel) contains the UI provided by [Backlite](https://github.com/mikestefanello/backlite) in order to fully monitor all tasks and queues from within your browser.
|
||||
|
||||
## Cron
|
||||
|
||||
By default, no cron solution is provided because it's very easy to add yourself if you need this. You can either use a [ticker](https://pkg.go.dev/time#Ticker) or a [library](https://github.com/robfig/cron).
|
||||
|
|
@ -1051,14 +1129,6 @@ The `LogRequest()` middleware is a replacement for Echo's `Logger()` middleware
|
|||
2024/06/15 09:07:11 INFO GET /contact request_id=gNblvugTKcyLnBYPMPTwMPEqDOioVLKp ip=::1 host=localhost:8000 referer="" status=200 bytes_in=0 bytes_out=5925 latency=107.527803ms
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
Future work includes but is not limited to:
|
||||
|
||||
- Admin section
|
||||
- OAuth
|
||||
- Flexible pager templates
|
||||
|
||||
## Credits
|
||||
|
||||
Thank you to all the following amazing projects for making this possible.
|
||||
|
|
|
|||
63
cmd/admin/main.go
Normal file
63
cmd/admin/main.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mikestefanello/pagoda/pkg/log"
|
||||
"github.com/mikestefanello/pagoda/pkg/services"
|
||||
)
|
||||
|
||||
// main creates a new admin user with the email passed in via the flag.
|
||||
func main() {
|
||||
// Start a new container.
|
||||
c := services.NewContainer()
|
||||
defer func() {
|
||||
// Gracefully shutdown all services.
|
||||
if err := c.Shutdown(); err != nil {
|
||||
log.Default().Error("shutdown failed", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var email string
|
||||
flag.StringVar(&email, "email", "", "email address for the admin user")
|
||||
flag.Parse()
|
||||
|
||||
if len(email) == 0 {
|
||||
invalid("email is required")
|
||||
}
|
||||
|
||||
// Generate a password.
|
||||
pw, err := c.Auth.RandomToken(10)
|
||||
if err != nil {
|
||||
invalid("failed to generate a random password")
|
||||
}
|
||||
|
||||
// Create the admin user.
|
||||
err = c.ORM.User.
|
||||
Create().
|
||||
SetEmail(email).
|
||||
SetName("Admin").
|
||||
SetAdmin(true).
|
||||
SetVerified(true).
|
||||
SetPassword(pw).
|
||||
Exec(context.Background())
|
||||
|
||||
if err != nil {
|
||||
invalid(err.Error())
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Println("-- ADMIN USER CREATED --")
|
||||
fmt.Printf("Email: %s\n", email)
|
||||
fmt.Printf("Password: %s\n", pw)
|
||||
fmt.Println("----")
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
func invalid(msg string) {
|
||||
fmt.Printf("[ERROR] %s\n", msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
@ -93,7 +93,6 @@ type (
|
|||
Capacity int
|
||||
Expiration struct {
|
||||
StaticFile time.Duration
|
||||
Page time.Duration
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@ cache:
|
|||
capacity: 100000
|
||||
expiration:
|
||||
staticFile: "4380h"
|
||||
page: "24h"
|
||||
|
||||
database:
|
||||
driver: "sqlite3"
|
||||
connection: "dbs/main.db?_journal=WAL&_timeout=5000&_fk=true"
|
||||
testConnection: ":memory:?_journal=WAL&_timeout=5000&_fk=true"
|
||||
# $RAND will be automatically replaced with a random value.
|
||||
# memdb is more robust for an in-memory database rather than :memory: because the latter has the potential
|
||||
# retain data even after you close and re-open the connection.
|
||||
testConnection: "file:/$RAND?vfs=memdb&_timeout=1000&_fk=true"
|
||||
|
||||
files:
|
||||
directory: "uploads"
|
||||
|
|
|
|||
97
ent/admin/extension.go
Normal file
97
ent/admin/extension.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"entgo.io/ent/entc"
|
||||
"entgo.io/ent/entc/gen"
|
||||
"entgo.io/ent/schema/field"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed templates
|
||||
templateDir embed.FS
|
||||
)
|
||||
|
||||
// Extension is the Ent extension that generates code to support the entity admin panel.
|
||||
type Extension struct {
|
||||
entc.DefaultExtension
|
||||
}
|
||||
|
||||
func (*Extension) Templates() []*gen.Template {
|
||||
return []*gen.Template{
|
||||
gen.MustParse(
|
||||
gen.NewTemplate("admin").
|
||||
Funcs(template.FuncMap{
|
||||
"fieldName": fieldName,
|
||||
"fieldLabel": FieldLabel,
|
||||
"fieldIsPointer": fieldIsPointer,
|
||||
}).
|
||||
ParseFS(templateDir, "templates/*tmpl"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// fieldName provides a struct field name from an entity field name (ie, user_id -> UserID).
|
||||
func fieldName(name string) string {
|
||||
if len(name) == 0 {
|
||||
return name
|
||||
}
|
||||
|
||||
parts := strings.Split(name, "_")
|
||||
for i := 0; i < len(parts); i++ {
|
||||
if parts[i] == "id" {
|
||||
parts[i] = "ID"
|
||||
} else {
|
||||
parts[i] = upperFirst(parts[i])
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
// FieldLabel provides a label for an entity field name (ie, user_id -> User ID).
|
||||
func FieldLabel(name string) string {
|
||||
if len(name) == 0 {
|
||||
return name
|
||||
}
|
||||
|
||||
parts := strings.Split(name, "_")
|
||||
for i := 0; i < len(parts); i++ {
|
||||
if parts[i] == "id" {
|
||||
parts[i] = "ID"
|
||||
}
|
||||
if i == 0 {
|
||||
parts[i] = upperFirst(parts[i])
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// fieldIsPointer determines if a given entity field should be a pointer on the struct.
|
||||
func fieldIsPointer(f *gen.Field) bool {
|
||||
switch {
|
||||
case f.Type.Type == field.TypeBool:
|
||||
return false
|
||||
case f.Optional,
|
||||
f.Default,
|
||||
f.Sensitive(),
|
||||
f.Nillable:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// upperFirst uppercases the first character of a given string.
|
||||
func upperFirst(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
out := []rune(s)
|
||||
out[0] = unicode.ToUpper(out[0])
|
||||
return string(out)
|
||||
}
|
||||
319
ent/admin/handler.go
Normal file
319
ent/admin/handler.go
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
// Code generated by ent, DO NOT EDIT.
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/ent/passwordtoken"
|
||||
"github.com/mikestefanello/pagoda/ent/user"
|
||||
)
|
||||
|
||||
const dateTimeFormat = "2006-01-02T15:04:05"
|
||||
const dateTimeFormatNoSeconds = "2006-01-02T15:04"
|
||||
|
||||
type Handler struct {
|
||||
client *ent.Client
|
||||
Config HandlerConfig
|
||||
}
|
||||
|
||||
func NewHandler(client *ent.Client, cfg HandlerConfig) *Handler {
|
||||
return &Handler{
|
||||
client: client,
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Create(ctx echo.Context, entityType string) error {
|
||||
switch entityType {
|
||||
case "PasswordToken":
|
||||
return h.PasswordTokenCreate(ctx)
|
||||
case "User":
|
||||
return h.UserCreate(ctx)
|
||||
default:
|
||||
return fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Get(ctx echo.Context, entityType string, id int) (url.Values, error) {
|
||||
switch entityType {
|
||||
case "PasswordToken":
|
||||
return h.PasswordTokenGet(ctx, id)
|
||||
case "User":
|
||||
return h.UserGet(ctx, id)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(ctx echo.Context, entityType string, id int) error {
|
||||
switch entityType {
|
||||
case "PasswordToken":
|
||||
return h.PasswordTokenDelete(ctx, id)
|
||||
case "User":
|
||||
return h.UserDelete(ctx, id)
|
||||
default:
|
||||
return fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Update(ctx echo.Context, entityType string, id int) error {
|
||||
switch entityType {
|
||||
case "PasswordToken":
|
||||
return h.PasswordTokenUpdate(ctx, id)
|
||||
case "User":
|
||||
return h.UserUpdate(ctx, id)
|
||||
default:
|
||||
return fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) List(ctx echo.Context, entityType string) (*EntityList, error) {
|
||||
switch entityType {
|
||||
case "PasswordToken":
|
||||
return h.PasswordTokenList(ctx)
|
||||
case "User":
|
||||
return h.UserList(ctx)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) PasswordTokenCreate(ctx echo.Context) error {
|
||||
var payload PasswordToken
|
||||
if err := h.bind(ctx, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op := h.client.PasswordToken.Create()
|
||||
if payload.Token != nil {
|
||||
op.SetToken(*payload.Token)
|
||||
}
|
||||
op.SetUserID(payload.UserID)
|
||||
if payload.CreatedAt != nil {
|
||||
op.SetCreatedAt(*payload.CreatedAt)
|
||||
}
|
||||
_, err := op.Save(ctx.Request().Context())
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) PasswordTokenUpdate(ctx echo.Context, id int) error {
|
||||
entity, err := h.client.PasswordToken.Get(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload PasswordToken
|
||||
if err = h.bind(ctx, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op := entity.Update()
|
||||
if payload.Token != nil {
|
||||
op.SetToken(*payload.Token)
|
||||
}
|
||||
op.SetUserID(payload.UserID)
|
||||
if payload.CreatedAt == nil {
|
||||
var empty time.Time
|
||||
op.SetCreatedAt(empty)
|
||||
} else {
|
||||
op.SetCreatedAt(*payload.CreatedAt)
|
||||
}
|
||||
_, err = op.Save(ctx.Request().Context())
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) PasswordTokenDelete(ctx echo.Context, id int) error {
|
||||
return h.client.PasswordToken.DeleteOneID(id).
|
||||
Exec(ctx.Request().Context())
|
||||
}
|
||||
|
||||
func (h *Handler) PasswordTokenList(ctx echo.Context) (*EntityList, error) {
|
||||
page, offset := h.getPageAndOffset(ctx)
|
||||
res, err := h.client.PasswordToken.
|
||||
Query().
|
||||
Limit(h.Config.ItemsPerPage + 1).
|
||||
Offset(offset).
|
||||
Order(passwordtoken.ByID(sql.OrderDesc())).
|
||||
All(ctx.Request().Context())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := &EntityList{
|
||||
Columns: []string{
|
||||
"User ID",
|
||||
"Created at",
|
||||
},
|
||||
Entities: make([]EntityValues, 0, len(res)),
|
||||
Page: page,
|
||||
HasNextPage: len(res) > h.Config.ItemsPerPage,
|
||||
}
|
||||
|
||||
for i := 0; i <= len(res)-1; i++ {
|
||||
list.Entities = append(list.Entities, EntityValues{
|
||||
ID: res[i].ID,
|
||||
Values: []string{
|
||||
fmt.Sprint(res[i].UserID),
|
||||
res[i].CreatedAt.Format(h.Config.TimeFormat),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (h *Handler) PasswordTokenGet(ctx echo.Context, id int) (url.Values, error) {
|
||||
entity, err := h.client.PasswordToken.Get(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("user_id", fmt.Sprint(entity.UserID))
|
||||
v.Set("created_at", entity.CreatedAt.Format(dateTimeFormat))
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (h *Handler) UserCreate(ctx echo.Context) error {
|
||||
var payload User
|
||||
if err := h.bind(ctx, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op := h.client.User.Create()
|
||||
op.SetName(payload.Name)
|
||||
op.SetEmail(payload.Email)
|
||||
if payload.Password != nil {
|
||||
op.SetPassword(*payload.Password)
|
||||
}
|
||||
op.SetVerified(payload.Verified)
|
||||
op.SetAdmin(payload.Admin)
|
||||
if payload.CreatedAt != nil {
|
||||
op.SetCreatedAt(*payload.CreatedAt)
|
||||
}
|
||||
_, err := op.Save(ctx.Request().Context())
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) UserUpdate(ctx echo.Context, id int) error {
|
||||
entity, err := h.client.User.Get(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload User
|
||||
if err = h.bind(ctx, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op := entity.Update()
|
||||
op.SetName(payload.Name)
|
||||
op.SetEmail(payload.Email)
|
||||
if payload.Password != nil {
|
||||
op.SetPassword(*payload.Password)
|
||||
}
|
||||
op.SetVerified(payload.Verified)
|
||||
op.SetAdmin(payload.Admin)
|
||||
_, err = op.Save(ctx.Request().Context())
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) UserDelete(ctx echo.Context, id int) error {
|
||||
return h.client.User.DeleteOneID(id).
|
||||
Exec(ctx.Request().Context())
|
||||
}
|
||||
|
||||
func (h *Handler) UserList(ctx echo.Context) (*EntityList, error) {
|
||||
page, offset := h.getPageAndOffset(ctx)
|
||||
res, err := h.client.User.
|
||||
Query().
|
||||
Limit(h.Config.ItemsPerPage + 1).
|
||||
Offset(offset).
|
||||
Order(user.ByID(sql.OrderDesc())).
|
||||
All(ctx.Request().Context())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := &EntityList{
|
||||
Columns: []string{
|
||||
"Name",
|
||||
"Email",
|
||||
"Verified",
|
||||
"Admin",
|
||||
"Created at",
|
||||
},
|
||||
Entities: make([]EntityValues, 0, len(res)),
|
||||
Page: page,
|
||||
HasNextPage: len(res) > h.Config.ItemsPerPage,
|
||||
}
|
||||
|
||||
for i := 0; i <= len(res)-1; i++ {
|
||||
list.Entities = append(list.Entities, EntityValues{
|
||||
ID: res[i].ID,
|
||||
Values: []string{
|
||||
res[i].Name,
|
||||
res[i].Email,
|
||||
fmt.Sprint(res[i].Verified),
|
||||
fmt.Sprint(res[i].Admin),
|
||||
res[i].CreatedAt.Format(h.Config.TimeFormat),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (h *Handler) UserGet(ctx echo.Context, id int) (url.Values, error) {
|
||||
entity, err := h.client.User.Get(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("name", entity.Name)
|
||||
v.Set("email", entity.Email)
|
||||
v.Set("verified", fmt.Sprint(entity.Verified))
|
||||
v.Set("admin", fmt.Sprint(entity.Admin))
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (h *Handler) getPageAndOffset(ctx echo.Context) (int, int) {
|
||||
if page, err := strconv.Atoi(ctx.QueryParam(h.Config.PageQueryKey)); err == nil {
|
||||
if page > 1 {
|
||||
return page, (page - 1) * h.Config.ItemsPerPage
|
||||
}
|
||||
}
|
||||
return 1, 0
|
||||
}
|
||||
|
||||
func (h *Handler) bind(ctx echo.Context, entity any) error {
|
||||
// Echo requires some pre-processing of form values to avoid problems.
|
||||
for k, v := range ctx.Request().Form {
|
||||
// Remove empty field values so Echo's bind does not fail when trying to parse things like
|
||||
// times, etc.
|
||||
if len(v) == 1 && len(v[0]) == 0 {
|
||||
delete(ctx.Request().Form, k)
|
||||
continue
|
||||
}
|
||||
|
||||
// Echo expects datetime values to be in a certain format but that does not align with the datetime-local
|
||||
// HTML form element format, so we will attempt to convert it here.
|
||||
for _, format := range []string{dateTimeFormatNoSeconds, dateTimeFormat} {
|
||||
if t, err := time.Parse(format, v[0]); err == nil {
|
||||
ctx.Request().Form[k][0] = t.Format(time.RFC3339)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctx.Bind(entity)
|
||||
}
|
||||
262
ent/admin/templates/handler.tmpl
Normal file
262
ent/admin/templates/handler.tmpl
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
{{/* Tell Intellij/GoLand to enable the autocompletion based on the *gen.Graph type. */}}
|
||||
{{/* gotype: entgo.io/ent/entc/gen.Graph */}}
|
||||
|
||||
{{ define "admin/handler" }}
|
||||
// Code generated by ent, DO NOT EDIT.
|
||||
{{- $pkg := base $.Config.Package }}
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"{{ $.Config.Package }}"
|
||||
{{- range $n := $.Nodes }}
|
||||
"{{ $.Config.Package }}/{{ $n.Package }}"
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
const dateTimeFormat = "2006-01-02T15:04:05"
|
||||
const dateTimeFormatNoSeconds = "2006-01-02T15:04"
|
||||
|
||||
type Handler struct {
|
||||
client *{{ $pkg }}.Client
|
||||
Config HandlerConfig
|
||||
}
|
||||
|
||||
func NewHandler(client *{{ $pkg }}.Client, cfg HandlerConfig) *Handler {
|
||||
return &Handler{
|
||||
client: client,
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Create(ctx echo.Context, entityType string) error {
|
||||
switch entityType {
|
||||
{{- range $n := $.Nodes }}
|
||||
case "{{ $n.Name }}":
|
||||
return h.{{ $n.Name }}Create(ctx)
|
||||
{{- end }}
|
||||
default:
|
||||
return fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Get(ctx echo.Context, entityType string, id int) (url.Values, error) {
|
||||
switch entityType {
|
||||
{{- range $n := $.Nodes }}
|
||||
case "{{ $n.Name }}":
|
||||
return h.{{ $n.Name }}Get(ctx, id)
|
||||
{{- end }}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(ctx echo.Context, entityType string, id int) error {
|
||||
switch entityType {
|
||||
{{- range $n := $.Nodes }}
|
||||
case "{{ $n.Name }}":
|
||||
return h.{{ $n.Name }}Delete(ctx, id)
|
||||
{{- end }}
|
||||
default:
|
||||
return fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Update(ctx echo.Context, entityType string, id int) error {
|
||||
switch entityType {
|
||||
{{- range $n := $.Nodes }}
|
||||
case "{{ $n.Name }}":
|
||||
return h.{{ $n.Name }}Update(ctx, id)
|
||||
{{- end }}
|
||||
default:
|
||||
return fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) List(ctx echo.Context, entityType string) (*EntityList, error) {
|
||||
switch entityType {
|
||||
{{- range $n := $.Nodes }}
|
||||
case "{{ $n.Name }}":
|
||||
return h.{{ $n.Name }}List(ctx)
|
||||
{{- end }}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
{{ range $n := $.Nodes }}
|
||||
func (h *Handler) {{ $n.Name }}Create(ctx echo.Context) error {
|
||||
var payload {{ $n.Name }}
|
||||
if err := h.bind(ctx, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op := h.client.{{ $n.Name }}.Create()
|
||||
{{- range $f := $n.Fields }}
|
||||
{{- if (fieldIsPointer $f) }}
|
||||
if payload.{{ fieldName $f.Name }} != nil {
|
||||
op.Set{{ fieldName $f.Name }}(*payload.{{ fieldName $f.Name }})
|
||||
}
|
||||
{{- else }}
|
||||
op.Set{{ fieldName $f.Name }}(payload.{{ fieldName $f.Name }})
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
_, err := op.Save(ctx.Request().Context())
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) {{ $n.Name }}Update(ctx echo.Context, id int) error {
|
||||
entity, err := h.client.{{ $n.Name }}.Get(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload {{ $n.Name }}
|
||||
if err = h.bind(ctx, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op := entity.Update()
|
||||
{{- range $f := $n.Fields }}
|
||||
{{- if not $f.Immutable }}
|
||||
{{- if $f.Sensitive }}
|
||||
if payload.{{ fieldName $f.Name }} != nil {
|
||||
op.Set{{ fieldName $f.Name }}(*payload.{{ fieldName $f.Name }})
|
||||
}
|
||||
{{- else if $f.Nillable }}
|
||||
op.SetNillable{{ fieldName $f.Name }}(payload.{{ fieldName $f.Name }})
|
||||
{{- else if $f.Optional }}
|
||||
if payload.{{ fieldName $f.Name }} == nil {
|
||||
op.Clear{{ fieldName $f.Name }}()
|
||||
} else {
|
||||
op.Set{{ fieldName $f.Name }}(*payload.{{ fieldName $f.Name }})
|
||||
}
|
||||
{{- else if (fieldIsPointer $f) }}
|
||||
if payload.{{ fieldName $f.Name }} == nil {
|
||||
var empty {{ $f.Type }}
|
||||
op.Set{{ fieldName $f.Name }}(empty)
|
||||
} else {
|
||||
op.Set{{ fieldName $f.Name }}(*payload.{{ fieldName $f.Name }})
|
||||
}
|
||||
{{- else }}
|
||||
op.Set{{ fieldName $f.Name }}(payload.{{ fieldName $f.Name }})
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
_, err = op.Save(ctx.Request().Context())
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) {{ $n.Name }}Delete(ctx echo.Context, id int) error {
|
||||
return h.client.{{ $n.Name }}.DeleteOneID(id).
|
||||
Exec(ctx.Request().Context())
|
||||
}
|
||||
|
||||
func (h *Handler) {{ $n.Name }}List(ctx echo.Context) (*EntityList, error) {
|
||||
page, offset := h.getPageAndOffset(ctx)
|
||||
res, err := h.client.{{ $n.Name }}.
|
||||
Query().
|
||||
Limit(h.Config.ItemsPerPage+1).
|
||||
Offset(offset).
|
||||
Order({{ $n.Package }}.ByID(sql.OrderDesc())).
|
||||
All(ctx.Request().Context())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := &EntityList{
|
||||
Columns: []string{
|
||||
{{- range $f := $n.Fields }}
|
||||
{{- if not $f.Sensitive }}
|
||||
"{{ fieldLabel $f.Name }}",
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
},
|
||||
Entities: make([]EntityValues, 0, len(res)),
|
||||
Page: page,
|
||||
HasNextPage: len(res) > h.Config.ItemsPerPage,
|
||||
}
|
||||
|
||||
for i := 0; i <= len(res)-1; i++ {
|
||||
list.Entities = append(list.Entities, EntityValues{
|
||||
ID: res[i].ID,
|
||||
Values: []string{
|
||||
{{- range $f := $n.Fields }}
|
||||
{{- if not $f.Sensitive }}
|
||||
{{- if eq $f.Type.String "string" }}
|
||||
res[i].{{ fieldName $f.Name }},
|
||||
{{- else if eq $f.Type.String "time.Time" }}
|
||||
res[i].{{ fieldName $f.Name }}.Format(h.Config.TimeFormat),
|
||||
{{- else }}
|
||||
fmt.Sprint(res[i].{{ fieldName $f.Name }}),
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (h *Handler) {{ $n.Name }}Get(ctx echo.Context, id int) (url.Values, error) {
|
||||
entity, err := h.client.{{ $n.Name }}.Get(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
{{- range $f := $n.Fields }}
|
||||
{{- if and (not $f.Sensitive) (not $f.Immutable) }}
|
||||
{{- if eq $f.Type.String "string" }}
|
||||
v.Set("{{ $f.Name }}", entity.{{ fieldName $f.Name }})
|
||||
{{- else if eq $f.Type.String "time.Time" }}
|
||||
v.Set("{{ $f.Name }}", entity.{{ fieldName $f.Name }}.Format(dateTimeFormat))
|
||||
{{- else }}
|
||||
v.Set("{{ $f.Name }}", fmt.Sprint(entity.{{ fieldName $f.Name }}))
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
return v, err
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
func (h *Handler) getPageAndOffset(ctx echo.Context) (int, int) {
|
||||
if page, err := strconv.Atoi(ctx.QueryParam(h.Config.PageQueryKey)); err == nil {
|
||||
if page > 1 {
|
||||
return page, (page-1) * h.Config.ItemsPerPage
|
||||
}
|
||||
}
|
||||
return 1, 0
|
||||
}
|
||||
|
||||
func (h *Handler) bind(ctx echo.Context, entity any) error {
|
||||
// Echo requires some pre-processing of form values to avoid problems.
|
||||
for k, v := range ctx.Request().Form {
|
||||
// Remove empty field values so Echo's bind does not fail when trying to parse things like
|
||||
// times, etc.
|
||||
if len(v) == 1 && len(v[0]) == 0 {
|
||||
delete(ctx.Request().Form, k)
|
||||
continue
|
||||
}
|
||||
|
||||
// Echo expects datetime values to be in a certain format but that does not align with the datetime-local
|
||||
// HTML form element format, so we will attempt to convert it here.
|
||||
for _, format := range []string{dateTimeFormatNoSeconds, dateTimeFormat} {
|
||||
if t, err := time.Parse(format, v[0]); err == nil {
|
||||
ctx.Request().Form[k][0] = t.Format(time.RFC3339)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctx.Bind(entity)
|
||||
}
|
||||
|
||||
{{ end }}
|
||||
42
ent/admin/templates/types.tmpl
Normal file
42
ent/admin/templates/types.tmpl
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{{/* Tell Intellij/GoLand to enable the autocompletion based on the *gen.Graph type. */}}
|
||||
{{/* gotype: entgo.io/ent/entc/gen.Graph */}}
|
||||
|
||||
{{ define "admin/types" }}
|
||||
// Code generated by ent, DO NOT EDIT.
|
||||
package admin
|
||||
|
||||
{{- range $n := $.Nodes }}
|
||||
type {{ $n.Name }} struct {
|
||||
{{- range $f := $n.Fields }}
|
||||
{{ fieldName $f.Name }} {{ if (fieldIsPointer $f) }}*{{ end }}{{ $f.Type }} `form:"{{ $f.Name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
type EntityList struct {
|
||||
Columns []string
|
||||
Entities []EntityValues
|
||||
Page int
|
||||
HasNextPage bool
|
||||
}
|
||||
|
||||
type EntityValues struct {
|
||||
ID int
|
||||
Values []string
|
||||
}
|
||||
|
||||
type HandlerConfig struct {
|
||||
ItemsPerPage int
|
||||
PageQueryKey string
|
||||
TimeFormat string
|
||||
}
|
||||
|
||||
func GetEntityTypeNames() []string {
|
||||
return []string{
|
||||
{{- range $n := $.Nodes }}
|
||||
"{{ $n.Name }}",
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
|
||||
{{ end }}
|
||||
44
ent/admin/types.go
Normal file
44
ent/admin/types.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Code generated by ent, DO NOT EDIT.
|
||||
package admin
|
||||
|
||||
import "time"
|
||||
|
||||
type PasswordToken struct {
|
||||
Token *string `form:"token"`
|
||||
UserID int `form:"user_id"`
|
||||
CreatedAt *time.Time `form:"created_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `form:"name"`
|
||||
Email string `form:"email"`
|
||||
Password *string `form:"password"`
|
||||
Verified bool `form:"verified"`
|
||||
Admin bool `form:"admin"`
|
||||
CreatedAt *time.Time `form:"created_at"`
|
||||
}
|
||||
|
||||
type EntityList struct {
|
||||
Columns []string
|
||||
Entities []EntityValues
|
||||
Page int
|
||||
HasNextPage bool
|
||||
}
|
||||
|
||||
type EntityValues struct {
|
||||
ID int
|
||||
Values []string
|
||||
}
|
||||
|
||||
type HandlerConfig struct {
|
||||
ItemsPerPage int
|
||||
PageQueryKey string
|
||||
TimeFormat string
|
||||
}
|
||||
|
||||
func GetEntityTypeNames() []string {
|
||||
return []string{
|
||||
"PasswordToken",
|
||||
"User",
|
||||
}
|
||||
}
|
||||
|
|
@ -333,7 +333,8 @@ func (c *PasswordTokenClient) QueryUser(pt *PasswordToken) *UserQuery {
|
|||
|
||||
// Hooks returns the client hooks.
|
||||
func (c *PasswordTokenClient) Hooks() []Hook {
|
||||
return c.hooks.PasswordToken
|
||||
hooks := c.hooks.PasswordToken
|
||||
return append(hooks[:len(hooks):len(hooks)], passwordtoken.Hooks[:]...)
|
||||
}
|
||||
|
||||
// Interceptors returns the client interceptors.
|
||||
|
|
|
|||
22
ent/entc.go
Normal file
22
ent/entc.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"entgo.io/ent/entc"
|
||||
"entgo.io/ent/entc/gen"
|
||||
"github.com/mikestefanello/pagoda/ent/admin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := entc.Generate("./schema",
|
||||
&gen.Config{},
|
||||
entc.Extensions(&admin.Extension{}),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal("running ent codegen:", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
package ent
|
||||
|
||||
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
|
||||
//go:generate go run -mod=mod entc.go
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ var (
|
|||
// PasswordTokensColumns holds the columns for the "password_tokens" table.
|
||||
PasswordTokensColumns = []*schema.Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "hash", Type: field.TypeString},
|
||||
{Name: "token", Type: field.TypeString},
|
||||
{Name: "created_at", Type: field.TypeTime},
|
||||
{Name: "password_token_user", Type: field.TypeInt},
|
||||
{Name: "user_id", Type: field.TypeInt},
|
||||
}
|
||||
// PasswordTokensTable holds the schema information for the "password_tokens" table.
|
||||
PasswordTokensTable = &schema.Table{
|
||||
|
|
@ -36,6 +36,7 @@ var (
|
|||
{Name: "email", Type: field.TypeString, Unique: true},
|
||||
{Name: "password", Type: field.TypeString},
|
||||
{Name: "verified", Type: field.TypeBool, Default: false},
|
||||
{Name: "admin", Type: field.TypeBool, Default: false},
|
||||
{Name: "created_at", Type: field.TypeTime},
|
||||
}
|
||||
// UsersTable holds the schema information for the "users" table.
|
||||
|
|
|
|||
182
ent/mutation.go
182
ent/mutation.go
|
|
@ -35,7 +35,7 @@ type PasswordTokenMutation struct {
|
|||
op Op
|
||||
typ string
|
||||
id *int
|
||||
hash *string
|
||||
token *string
|
||||
created_at *time.Time
|
||||
clearedFields map[string]struct{}
|
||||
user *int
|
||||
|
|
@ -143,40 +143,76 @@ func (m *PasswordTokenMutation) IDs(ctx context.Context) ([]int, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetHash sets the "hash" field.
|
||||
func (m *PasswordTokenMutation) SetHash(s string) {
|
||||
m.hash = &s
|
||||
// SetToken sets the "token" field.
|
||||
func (m *PasswordTokenMutation) SetToken(s string) {
|
||||
m.token = &s
|
||||
}
|
||||
|
||||
// Hash returns the value of the "hash" field in the mutation.
|
||||
func (m *PasswordTokenMutation) Hash() (r string, exists bool) {
|
||||
v := m.hash
|
||||
// Token returns the value of the "token" field in the mutation.
|
||||
func (m *PasswordTokenMutation) Token() (r string, exists bool) {
|
||||
v := m.token
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldHash returns the old "hash" field's value of the PasswordToken entity.
|
||||
// OldToken returns the old "token" field's value of the PasswordToken entity.
|
||||
// If the PasswordToken object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *PasswordTokenMutation) OldHash(ctx context.Context) (v string, err error) {
|
||||
func (m *PasswordTokenMutation) OldToken(ctx context.Context) (v string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldHash is only allowed on UpdateOne operations")
|
||||
return v, errors.New("OldToken is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldHash requires an ID field in the mutation")
|
||||
return v, errors.New("OldToken requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldHash: %w", err)
|
||||
return v, fmt.Errorf("querying old value for OldToken: %w", err)
|
||||
}
|
||||
return oldValue.Hash, nil
|
||||
return oldValue.Token, nil
|
||||
}
|
||||
|
||||
// ResetHash resets all changes to the "hash" field.
|
||||
func (m *PasswordTokenMutation) ResetHash() {
|
||||
m.hash = nil
|
||||
// ResetToken resets all changes to the "token" field.
|
||||
func (m *PasswordTokenMutation) ResetToken() {
|
||||
m.token = nil
|
||||
}
|
||||
|
||||
// SetUserID sets the "user_id" field.
|
||||
func (m *PasswordTokenMutation) SetUserID(i int) {
|
||||
m.user = &i
|
||||
}
|
||||
|
||||
// UserID returns the value of the "user_id" field in the mutation.
|
||||
func (m *PasswordTokenMutation) UserID() (r int, exists bool) {
|
||||
v := m.user
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldUserID returns the old "user_id" field's value of the PasswordToken entity.
|
||||
// If the PasswordToken object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *PasswordTokenMutation) OldUserID(ctx context.Context) (v int, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldUserID is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldUserID requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldUserID: %w", err)
|
||||
}
|
||||
return oldValue.UserID, nil
|
||||
}
|
||||
|
||||
// ResetUserID resets all changes to the "user_id" field.
|
||||
func (m *PasswordTokenMutation) ResetUserID() {
|
||||
m.user = nil
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the "created_at" field.
|
||||
|
|
@ -215,14 +251,10 @@ func (m *PasswordTokenMutation) ResetCreatedAt() {
|
|||
m.created_at = nil
|
||||
}
|
||||
|
||||
// SetUserID sets the "user" edge to the User entity by id.
|
||||
func (m *PasswordTokenMutation) SetUserID(id int) {
|
||||
m.user = &id
|
||||
}
|
||||
|
||||
// ClearUser clears the "user" edge to the User entity.
|
||||
func (m *PasswordTokenMutation) ClearUser() {
|
||||
m.cleareduser = true
|
||||
m.clearedFields[passwordtoken.FieldUserID] = struct{}{}
|
||||
}
|
||||
|
||||
// UserCleared reports if the "user" edge to the User entity was cleared.
|
||||
|
|
@ -230,14 +262,6 @@ func (m *PasswordTokenMutation) UserCleared() bool {
|
|||
return m.cleareduser
|
||||
}
|
||||
|
||||
// UserID returns the "user" edge ID in the mutation.
|
||||
func (m *PasswordTokenMutation) UserID() (id int, exists bool) {
|
||||
if m.user != nil {
|
||||
return *m.user, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UserIDs returns the "user" edge IDs in the mutation.
|
||||
// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use
|
||||
// UserID instead. It exists only for internal usage by the builders.
|
||||
|
|
@ -288,9 +312,12 @@ func (m *PasswordTokenMutation) Type() string {
|
|||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *PasswordTokenMutation) Fields() []string {
|
||||
fields := make([]string, 0, 2)
|
||||
if m.hash != nil {
|
||||
fields = append(fields, passwordtoken.FieldHash)
|
||||
fields := make([]string, 0, 3)
|
||||
if m.token != nil {
|
||||
fields = append(fields, passwordtoken.FieldToken)
|
||||
}
|
||||
if m.user != nil {
|
||||
fields = append(fields, passwordtoken.FieldUserID)
|
||||
}
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, passwordtoken.FieldCreatedAt)
|
||||
|
|
@ -303,8 +330,10 @@ func (m *PasswordTokenMutation) Fields() []string {
|
|||
// schema.
|
||||
func (m *PasswordTokenMutation) Field(name string) (ent.Value, bool) {
|
||||
switch name {
|
||||
case passwordtoken.FieldHash:
|
||||
return m.Hash()
|
||||
case passwordtoken.FieldToken:
|
||||
return m.Token()
|
||||
case passwordtoken.FieldUserID:
|
||||
return m.UserID()
|
||||
case passwordtoken.FieldCreatedAt:
|
||||
return m.CreatedAt()
|
||||
}
|
||||
|
|
@ -316,8 +345,10 @@ func (m *PasswordTokenMutation) Field(name string) (ent.Value, bool) {
|
|||
// database failed.
|
||||
func (m *PasswordTokenMutation) OldField(ctx context.Context, name string) (ent.Value, error) {
|
||||
switch name {
|
||||
case passwordtoken.FieldHash:
|
||||
return m.OldHash(ctx)
|
||||
case passwordtoken.FieldToken:
|
||||
return m.OldToken(ctx)
|
||||
case passwordtoken.FieldUserID:
|
||||
return m.OldUserID(ctx)
|
||||
case passwordtoken.FieldCreatedAt:
|
||||
return m.OldCreatedAt(ctx)
|
||||
}
|
||||
|
|
@ -329,12 +360,19 @@ func (m *PasswordTokenMutation) OldField(ctx context.Context, name string) (ent.
|
|||
// type.
|
||||
func (m *PasswordTokenMutation) SetField(name string, value ent.Value) error {
|
||||
switch name {
|
||||
case passwordtoken.FieldHash:
|
||||
case passwordtoken.FieldToken:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetHash(v)
|
||||
m.SetToken(v)
|
||||
return nil
|
||||
case passwordtoken.FieldUserID:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetUserID(v)
|
||||
return nil
|
||||
case passwordtoken.FieldCreatedAt:
|
||||
v, ok := value.(time.Time)
|
||||
|
|
@ -350,13 +388,16 @@ func (m *PasswordTokenMutation) SetField(name string, value ent.Value) error {
|
|||
// AddedFields returns all numeric fields that were incremented/decremented during
|
||||
// this mutation.
|
||||
func (m *PasswordTokenMutation) AddedFields() []string {
|
||||
return nil
|
||||
var fields []string
|
||||
return fields
|
||||
}
|
||||
|
||||
// AddedField returns the numeric value that was incremented/decremented on a field
|
||||
// with the given name. The second boolean return value indicates that this field
|
||||
// was not set, or was not defined in the schema.
|
||||
func (m *PasswordTokenMutation) AddedField(name string) (ent.Value, bool) {
|
||||
switch name {
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
|
@ -392,8 +433,11 @@ func (m *PasswordTokenMutation) ClearField(name string) error {
|
|||
// It returns an error if the field is not defined in the schema.
|
||||
func (m *PasswordTokenMutation) ResetField(name string) error {
|
||||
switch name {
|
||||
case passwordtoken.FieldHash:
|
||||
m.ResetHash()
|
||||
case passwordtoken.FieldToken:
|
||||
m.ResetToken()
|
||||
return nil
|
||||
case passwordtoken.FieldUserID:
|
||||
m.ResetUserID()
|
||||
return nil
|
||||
case passwordtoken.FieldCreatedAt:
|
||||
m.ResetCreatedAt()
|
||||
|
|
@ -486,6 +530,7 @@ type UserMutation struct {
|
|||
email *string
|
||||
password *string
|
||||
verified *bool
|
||||
admin *bool
|
||||
created_at *time.Time
|
||||
clearedFields map[string]struct{}
|
||||
owner map[int]struct{}
|
||||
|
|
@ -738,6 +783,42 @@ func (m *UserMutation) ResetVerified() {
|
|||
m.verified = nil
|
||||
}
|
||||
|
||||
// SetAdmin sets the "admin" field.
|
||||
func (m *UserMutation) SetAdmin(b bool) {
|
||||
m.admin = &b
|
||||
}
|
||||
|
||||
// Admin returns the value of the "admin" field in the mutation.
|
||||
func (m *UserMutation) Admin() (r bool, exists bool) {
|
||||
v := m.admin
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldAdmin returns the old "admin" field's value of the User entity.
|
||||
// If the User object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *UserMutation) OldAdmin(ctx context.Context) (v bool, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldAdmin is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldAdmin requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldAdmin: %w", err)
|
||||
}
|
||||
return oldValue.Admin, nil
|
||||
}
|
||||
|
||||
// ResetAdmin resets all changes to the "admin" field.
|
||||
func (m *UserMutation) ResetAdmin() {
|
||||
m.admin = nil
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the "created_at" field.
|
||||
func (m *UserMutation) SetCreatedAt(t time.Time) {
|
||||
m.created_at = &t
|
||||
|
|
@ -862,7 +943,7 @@ func (m *UserMutation) Type() string {
|
|||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *UserMutation) Fields() []string {
|
||||
fields := make([]string, 0, 5)
|
||||
fields := make([]string, 0, 6)
|
||||
if m.name != nil {
|
||||
fields = append(fields, user.FieldName)
|
||||
}
|
||||
|
|
@ -875,6 +956,9 @@ func (m *UserMutation) Fields() []string {
|
|||
if m.verified != nil {
|
||||
fields = append(fields, user.FieldVerified)
|
||||
}
|
||||
if m.admin != nil {
|
||||
fields = append(fields, user.FieldAdmin)
|
||||
}
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, user.FieldCreatedAt)
|
||||
}
|
||||
|
|
@ -894,6 +978,8 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
|
|||
return m.Password()
|
||||
case user.FieldVerified:
|
||||
return m.Verified()
|
||||
case user.FieldAdmin:
|
||||
return m.Admin()
|
||||
case user.FieldCreatedAt:
|
||||
return m.CreatedAt()
|
||||
}
|
||||
|
|
@ -913,6 +999,8 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
|
|||
return m.OldPassword(ctx)
|
||||
case user.FieldVerified:
|
||||
return m.OldVerified(ctx)
|
||||
case user.FieldAdmin:
|
||||
return m.OldAdmin(ctx)
|
||||
case user.FieldCreatedAt:
|
||||
return m.OldCreatedAt(ctx)
|
||||
}
|
||||
|
|
@ -952,6 +1040,13 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
|
|||
}
|
||||
m.SetVerified(v)
|
||||
return nil
|
||||
case user.FieldAdmin:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetAdmin(v)
|
||||
return nil
|
||||
case user.FieldCreatedAt:
|
||||
v, ok := value.(time.Time)
|
||||
if !ok {
|
||||
|
|
@ -1020,6 +1115,9 @@ func (m *UserMutation) ResetField(name string) error {
|
|||
case user.FieldVerified:
|
||||
m.ResetVerified()
|
||||
return nil
|
||||
case user.FieldAdmin:
|
||||
m.ResetAdmin()
|
||||
return nil
|
||||
case user.FieldCreatedAt:
|
||||
m.ResetCreatedAt()
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -18,14 +18,15 @@ type PasswordToken struct {
|
|||
config `json:"-"`
|
||||
// ID of the ent.
|
||||
ID int `json:"id,omitempty"`
|
||||
// Hash holds the value of the "hash" field.
|
||||
Hash string `json:"-"`
|
||||
// Token holds the value of the "token" field.
|
||||
Token string `json:"-"`
|
||||
// UserID holds the value of the "user_id" field.
|
||||
UserID int `json:"user_id,omitempty"`
|
||||
// CreatedAt holds the value of the "created_at" field.
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
// The values are being populated by the PasswordTokenQuery when eager-loading is set.
|
||||
Edges PasswordTokenEdges `json:"edges"`
|
||||
password_token_user *int
|
||||
selectValues sql.SelectValues
|
||||
}
|
||||
|
||||
|
|
@ -54,14 +55,12 @@ func (*PasswordToken) scanValues(columns []string) ([]any, error) {
|
|||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case passwordtoken.FieldID:
|
||||
case passwordtoken.FieldID, passwordtoken.FieldUserID:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case passwordtoken.FieldHash:
|
||||
case passwordtoken.FieldToken:
|
||||
values[i] = new(sql.NullString)
|
||||
case passwordtoken.FieldCreatedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
case passwordtoken.ForeignKeys[0]: // password_token_user
|
||||
values[i] = new(sql.NullInt64)
|
||||
default:
|
||||
values[i] = new(sql.UnknownType)
|
||||
}
|
||||
|
|
@ -83,11 +82,17 @@ func (pt *PasswordToken) assignValues(columns []string, values []any) error {
|
|||
return fmt.Errorf("unexpected type %T for field id", value)
|
||||
}
|
||||
pt.ID = int(value.Int64)
|
||||
case passwordtoken.FieldHash:
|
||||
case passwordtoken.FieldToken:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field hash", values[i])
|
||||
return fmt.Errorf("unexpected type %T for field token", values[i])
|
||||
} else if value.Valid {
|
||||
pt.Hash = value.String
|
||||
pt.Token = value.String
|
||||
}
|
||||
case passwordtoken.FieldUserID:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field user_id", values[i])
|
||||
} else if value.Valid {
|
||||
pt.UserID = int(value.Int64)
|
||||
}
|
||||
case passwordtoken.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||
|
|
@ -95,13 +100,6 @@ func (pt *PasswordToken) assignValues(columns []string, values []any) error {
|
|||
} else if value.Valid {
|
||||
pt.CreatedAt = value.Time
|
||||
}
|
||||
case passwordtoken.ForeignKeys[0]:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for edge-field password_token_user", value)
|
||||
} else if value.Valid {
|
||||
pt.password_token_user = new(int)
|
||||
*pt.password_token_user = int(value.Int64)
|
||||
}
|
||||
default:
|
||||
pt.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
|
|
@ -143,7 +141,10 @@ func (pt *PasswordToken) String() string {
|
|||
var builder strings.Builder
|
||||
builder.WriteString("PasswordToken(")
|
||||
builder.WriteString(fmt.Sprintf("id=%v, ", pt.ID))
|
||||
builder.WriteString("hash=<sensitive>")
|
||||
builder.WriteString("token=<sensitive>")
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("user_id=")
|
||||
builder.WriteString(fmt.Sprintf("%v", pt.UserID))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(pt.CreatedAt.Format(time.ANSIC))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package passwordtoken
|
|||
import (
|
||||
"time"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
)
|
||||
|
|
@ -14,8 +15,10 @@ const (
|
|||
Label = "password_token"
|
||||
// FieldID holds the string denoting the id field in the database.
|
||||
FieldID = "id"
|
||||
// FieldHash holds the string denoting the hash field in the database.
|
||||
FieldHash = "hash"
|
||||
// FieldToken holds the string denoting the token field in the database.
|
||||
FieldToken = "token"
|
||||
// FieldUserID holds the string denoting the user_id field in the database.
|
||||
FieldUserID = "user_id"
|
||||
// FieldCreatedAt holds the string denoting the created_at field in the database.
|
||||
FieldCreatedAt = "created_at"
|
||||
// EdgeUser holds the string denoting the user edge name in mutations.
|
||||
|
|
@ -28,22 +31,17 @@ const (
|
|||
// It exists in this package in order to avoid circular dependency with the "user" package.
|
||||
UserInverseTable = "users"
|
||||
// UserColumn is the table column denoting the user relation/edge.
|
||||
UserColumn = "password_token_user"
|
||||
UserColumn = "user_id"
|
||||
)
|
||||
|
||||
// Columns holds all SQL columns for passwordtoken fields.
|
||||
var Columns = []string{
|
||||
FieldID,
|
||||
FieldHash,
|
||||
FieldToken,
|
||||
FieldUserID,
|
||||
FieldCreatedAt,
|
||||
}
|
||||
|
||||
// ForeignKeys holds the SQL foreign-keys that are owned by the "password_tokens"
|
||||
// table and are not defined as standalone fields in the schema.
|
||||
var ForeignKeys = []string{
|
||||
"password_token_user",
|
||||
}
|
||||
|
||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||
func ValidColumn(column string) bool {
|
||||
for i := range Columns {
|
||||
|
|
@ -51,17 +49,18 @@ func ValidColumn(column string) bool {
|
|||
return true
|
||||
}
|
||||
}
|
||||
for i := range ForeignKeys {
|
||||
if column == ForeignKeys[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Note that the variables below are initialized by the runtime
|
||||
// package on the initialization of the application. Therefore,
|
||||
// it should be imported in the main as follows:
|
||||
//
|
||||
// import _ "github.com/mikestefanello/pagoda/ent/runtime"
|
||||
var (
|
||||
// HashValidator is a validator for the "hash" field. It is called by the builders before save.
|
||||
HashValidator func(string) error
|
||||
Hooks [1]ent.Hook
|
||||
// TokenValidator is a validator for the "token" field. It is called by the builders before save.
|
||||
TokenValidator func(string) error
|
||||
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
||||
DefaultCreatedAt func() time.Time
|
||||
)
|
||||
|
|
@ -74,9 +73,14 @@ func ByID(opts ...sql.OrderTermOption) OrderOption {
|
|||
return sql.OrderByField(FieldID, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByHash orders the results by the hash field.
|
||||
func ByHash(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldHash, opts...).ToFunc()
|
||||
// ByToken orders the results by the token field.
|
||||
func ByToken(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldToken, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByUserID orders the results by the user_id field.
|
||||
func ByUserID(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUserID, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCreatedAt orders the results by the created_at field.
|
||||
|
|
|
|||
|
|
@ -55,9 +55,14 @@ func IDLTE(id int) predicate.PasswordToken {
|
|||
return predicate.PasswordToken(sql.FieldLTE(FieldID, id))
|
||||
}
|
||||
|
||||
// Hash applies equality check predicate on the "hash" field. It's identical to HashEQ.
|
||||
func Hash(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldEQ(FieldHash, v))
|
||||
// Token applies equality check predicate on the "token" field. It's identical to TokenEQ.
|
||||
func Token(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldEQ(FieldToken, v))
|
||||
}
|
||||
|
||||
// UserID applies equality check predicate on the "user_id" field. It's identical to UserIDEQ.
|
||||
func UserID(v int) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldEQ(FieldUserID, v))
|
||||
}
|
||||
|
||||
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
|
||||
|
|
@ -65,69 +70,89 @@ func CreatedAt(v time.Time) predicate.PasswordToken {
|
|||
return predicate.PasswordToken(sql.FieldEQ(FieldCreatedAt, v))
|
||||
}
|
||||
|
||||
// HashEQ applies the EQ predicate on the "hash" field.
|
||||
func HashEQ(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldEQ(FieldHash, v))
|
||||
// TokenEQ applies the EQ predicate on the "token" field.
|
||||
func TokenEQ(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldEQ(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashNEQ applies the NEQ predicate on the "hash" field.
|
||||
func HashNEQ(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldNEQ(FieldHash, v))
|
||||
// TokenNEQ applies the NEQ predicate on the "token" field.
|
||||
func TokenNEQ(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldNEQ(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashIn applies the In predicate on the "hash" field.
|
||||
func HashIn(vs ...string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldIn(FieldHash, vs...))
|
||||
// TokenIn applies the In predicate on the "token" field.
|
||||
func TokenIn(vs ...string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldIn(FieldToken, vs...))
|
||||
}
|
||||
|
||||
// HashNotIn applies the NotIn predicate on the "hash" field.
|
||||
func HashNotIn(vs ...string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldNotIn(FieldHash, vs...))
|
||||
// TokenNotIn applies the NotIn predicate on the "token" field.
|
||||
func TokenNotIn(vs ...string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldNotIn(FieldToken, vs...))
|
||||
}
|
||||
|
||||
// HashGT applies the GT predicate on the "hash" field.
|
||||
func HashGT(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldGT(FieldHash, v))
|
||||
// TokenGT applies the GT predicate on the "token" field.
|
||||
func TokenGT(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldGT(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashGTE applies the GTE predicate on the "hash" field.
|
||||
func HashGTE(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldGTE(FieldHash, v))
|
||||
// TokenGTE applies the GTE predicate on the "token" field.
|
||||
func TokenGTE(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldGTE(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashLT applies the LT predicate on the "hash" field.
|
||||
func HashLT(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldLT(FieldHash, v))
|
||||
// TokenLT applies the LT predicate on the "token" field.
|
||||
func TokenLT(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldLT(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashLTE applies the LTE predicate on the "hash" field.
|
||||
func HashLTE(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldLTE(FieldHash, v))
|
||||
// TokenLTE applies the LTE predicate on the "token" field.
|
||||
func TokenLTE(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldLTE(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashContains applies the Contains predicate on the "hash" field.
|
||||
func HashContains(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldContains(FieldHash, v))
|
||||
// TokenContains applies the Contains predicate on the "token" field.
|
||||
func TokenContains(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldContains(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashHasPrefix applies the HasPrefix predicate on the "hash" field.
|
||||
func HashHasPrefix(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldHasPrefix(FieldHash, v))
|
||||
// TokenHasPrefix applies the HasPrefix predicate on the "token" field.
|
||||
func TokenHasPrefix(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldHasPrefix(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashHasSuffix applies the HasSuffix predicate on the "hash" field.
|
||||
func HashHasSuffix(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldHasSuffix(FieldHash, v))
|
||||
// TokenHasSuffix applies the HasSuffix predicate on the "token" field.
|
||||
func TokenHasSuffix(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldHasSuffix(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashEqualFold applies the EqualFold predicate on the "hash" field.
|
||||
func HashEqualFold(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldEqualFold(FieldHash, v))
|
||||
// TokenEqualFold applies the EqualFold predicate on the "token" field.
|
||||
func TokenEqualFold(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldEqualFold(FieldToken, v))
|
||||
}
|
||||
|
||||
// HashContainsFold applies the ContainsFold predicate on the "hash" field.
|
||||
func HashContainsFold(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldContainsFold(FieldHash, v))
|
||||
// TokenContainsFold applies the ContainsFold predicate on the "token" field.
|
||||
func TokenContainsFold(v string) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldContainsFold(FieldToken, v))
|
||||
}
|
||||
|
||||
// UserIDEQ applies the EQ predicate on the "user_id" field.
|
||||
func UserIDEQ(v int) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldEQ(FieldUserID, v))
|
||||
}
|
||||
|
||||
// UserIDNEQ applies the NEQ predicate on the "user_id" field.
|
||||
func UserIDNEQ(v int) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldNEQ(FieldUserID, v))
|
||||
}
|
||||
|
||||
// UserIDIn applies the In predicate on the "user_id" field.
|
||||
func UserIDIn(vs ...int) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldIn(FieldUserID, vs...))
|
||||
}
|
||||
|
||||
// UserIDNotIn applies the NotIn predicate on the "user_id" field.
|
||||
func UserIDNotIn(vs ...int) predicate.PasswordToken {
|
||||
return predicate.PasswordToken(sql.FieldNotIn(FieldUserID, vs...))
|
||||
}
|
||||
|
||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||
|
|
|
|||
|
|
@ -21,9 +21,15 @@ type PasswordTokenCreate struct {
|
|||
hooks []Hook
|
||||
}
|
||||
|
||||
// SetHash sets the "hash" field.
|
||||
func (ptc *PasswordTokenCreate) SetHash(s string) *PasswordTokenCreate {
|
||||
ptc.mutation.SetHash(s)
|
||||
// SetToken sets the "token" field.
|
||||
func (ptc *PasswordTokenCreate) SetToken(s string) *PasswordTokenCreate {
|
||||
ptc.mutation.SetToken(s)
|
||||
return ptc
|
||||
}
|
||||
|
||||
// SetUserID sets the "user_id" field.
|
||||
func (ptc *PasswordTokenCreate) SetUserID(i int) *PasswordTokenCreate {
|
||||
ptc.mutation.SetUserID(i)
|
||||
return ptc
|
||||
}
|
||||
|
||||
|
|
@ -41,12 +47,6 @@ func (ptc *PasswordTokenCreate) SetNillableCreatedAt(t *time.Time) *PasswordToke
|
|||
return ptc
|
||||
}
|
||||
|
||||
// SetUserID sets the "user" edge to the User entity by ID.
|
||||
func (ptc *PasswordTokenCreate) SetUserID(id int) *PasswordTokenCreate {
|
||||
ptc.mutation.SetUserID(id)
|
||||
return ptc
|
||||
}
|
||||
|
||||
// SetUser sets the "user" edge to the User entity.
|
||||
func (ptc *PasswordTokenCreate) SetUser(u *User) *PasswordTokenCreate {
|
||||
return ptc.SetUserID(u.ID)
|
||||
|
|
@ -59,7 +59,9 @@ func (ptc *PasswordTokenCreate) Mutation() *PasswordTokenMutation {
|
|||
|
||||
// Save creates the PasswordToken in the database.
|
||||
func (ptc *PasswordTokenCreate) Save(ctx context.Context) (*PasswordToken, error) {
|
||||
ptc.defaults()
|
||||
if err := ptc.defaults(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return withHooks(ctx, ptc.sqlSave, ptc.mutation, ptc.hooks)
|
||||
}
|
||||
|
||||
|
|
@ -86,23 +88,30 @@ func (ptc *PasswordTokenCreate) ExecX(ctx context.Context) {
|
|||
}
|
||||
|
||||
// defaults sets the default values of the builder before save.
|
||||
func (ptc *PasswordTokenCreate) defaults() {
|
||||
func (ptc *PasswordTokenCreate) defaults() error {
|
||||
if _, ok := ptc.mutation.CreatedAt(); !ok {
|
||||
if passwordtoken.DefaultCreatedAt == nil {
|
||||
return fmt.Errorf("ent: uninitialized passwordtoken.DefaultCreatedAt (forgotten import ent/runtime?)")
|
||||
}
|
||||
v := passwordtoken.DefaultCreatedAt()
|
||||
ptc.mutation.SetCreatedAt(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (ptc *PasswordTokenCreate) check() error {
|
||||
if _, ok := ptc.mutation.Hash(); !ok {
|
||||
return &ValidationError{Name: "hash", err: errors.New(`ent: missing required field "PasswordToken.hash"`)}
|
||||
if _, ok := ptc.mutation.Token(); !ok {
|
||||
return &ValidationError{Name: "token", err: errors.New(`ent: missing required field "PasswordToken.token"`)}
|
||||
}
|
||||
if v, ok := ptc.mutation.Hash(); ok {
|
||||
if err := passwordtoken.HashValidator(v); err != nil {
|
||||
return &ValidationError{Name: "hash", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.hash": %w`, err)}
|
||||
if v, ok := ptc.mutation.Token(); ok {
|
||||
if err := passwordtoken.TokenValidator(v); err != nil {
|
||||
return &ValidationError{Name: "token", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.token": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := ptc.mutation.UserID(); !ok {
|
||||
return &ValidationError{Name: "user_id", err: errors.New(`ent: missing required field "PasswordToken.user_id"`)}
|
||||
}
|
||||
if _, ok := ptc.mutation.CreatedAt(); !ok {
|
||||
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "PasswordToken.created_at"`)}
|
||||
}
|
||||
|
|
@ -135,9 +144,9 @@ func (ptc *PasswordTokenCreate) createSpec() (*PasswordToken, *sqlgraph.CreateSp
|
|||
_node = &PasswordToken{config: ptc.config}
|
||||
_spec = sqlgraph.NewCreateSpec(passwordtoken.Table, sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt))
|
||||
)
|
||||
if value, ok := ptc.mutation.Hash(); ok {
|
||||
_spec.SetField(passwordtoken.FieldHash, field.TypeString, value)
|
||||
_node.Hash = value
|
||||
if value, ok := ptc.mutation.Token(); ok {
|
||||
_spec.SetField(passwordtoken.FieldToken, field.TypeString, value)
|
||||
_node.Token = value
|
||||
}
|
||||
if value, ok := ptc.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(passwordtoken.FieldCreatedAt, field.TypeTime, value)
|
||||
|
|
@ -157,7 +166,7 @@ func (ptc *PasswordTokenCreate) createSpec() (*PasswordToken, *sqlgraph.CreateSp
|
|||
for _, k := range nodes {
|
||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||
}
|
||||
_node.password_token_user = &nodes[0]
|
||||
_node.UserID = nodes[0]
|
||||
_spec.Edges = append(_spec.Edges, edge)
|
||||
}
|
||||
return _node, _spec
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ type PasswordTokenQuery struct {
|
|||
inters []Interceptor
|
||||
predicates []predicate.PasswordToken
|
||||
withUser *UserQuery
|
||||
withFKs bool
|
||||
// intermediate query (i.e. traversal path).
|
||||
sql *sql.Selector
|
||||
path func(context.Context) (*sql.Selector, error)
|
||||
|
|
@ -299,12 +298,12 @@ func (ptq *PasswordTokenQuery) WithUser(opts ...func(*UserQuery)) *PasswordToken
|
|||
// Example:
|
||||
//
|
||||
// var v []struct {
|
||||
// Hash string `json:"hash,omitempty"`
|
||||
// Token string `json:"token,omitempty"`
|
||||
// Count int `json:"count,omitempty"`
|
||||
// }
|
||||
//
|
||||
// client.PasswordToken.Query().
|
||||
// GroupBy(passwordtoken.FieldHash).
|
||||
// GroupBy(passwordtoken.FieldToken).
|
||||
// Aggregate(ent.Count()).
|
||||
// Scan(ctx, &v)
|
||||
func (ptq *PasswordTokenQuery) GroupBy(field string, fields ...string) *PasswordTokenGroupBy {
|
||||
|
|
@ -322,11 +321,11 @@ func (ptq *PasswordTokenQuery) GroupBy(field string, fields ...string) *Password
|
|||
// Example:
|
||||
//
|
||||
// var v []struct {
|
||||
// Hash string `json:"hash,omitempty"`
|
||||
// Token string `json:"token,omitempty"`
|
||||
// }
|
||||
//
|
||||
// client.PasswordToken.Query().
|
||||
// Select(passwordtoken.FieldHash).
|
||||
// Select(passwordtoken.FieldToken).
|
||||
// Scan(ctx, &v)
|
||||
func (ptq *PasswordTokenQuery) Select(fields ...string) *PasswordTokenSelect {
|
||||
ptq.ctx.Fields = append(ptq.ctx.Fields, fields...)
|
||||
|
|
@ -370,18 +369,11 @@ func (ptq *PasswordTokenQuery) prepareQuery(ctx context.Context) error {
|
|||
func (ptq *PasswordTokenQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*PasswordToken, error) {
|
||||
var (
|
||||
nodes = []*PasswordToken{}
|
||||
withFKs = ptq.withFKs
|
||||
_spec = ptq.querySpec()
|
||||
loadedTypes = [1]bool{
|
||||
ptq.withUser != nil,
|
||||
}
|
||||
)
|
||||
if ptq.withUser != nil {
|
||||
withFKs = true
|
||||
}
|
||||
if withFKs {
|
||||
_spec.Node.Columns = append(_spec.Node.Columns, passwordtoken.ForeignKeys...)
|
||||
}
|
||||
_spec.ScanValues = func(columns []string) ([]any, error) {
|
||||
return (*PasswordToken).scanValues(nil, columns)
|
||||
}
|
||||
|
|
@ -413,10 +405,7 @@ func (ptq *PasswordTokenQuery) loadUser(ctx context.Context, query *UserQuery, n
|
|||
ids := make([]int, 0, len(nodes))
|
||||
nodeids := make(map[int][]*PasswordToken)
|
||||
for i := range nodes {
|
||||
if nodes[i].password_token_user == nil {
|
||||
continue
|
||||
}
|
||||
fk := *nodes[i].password_token_user
|
||||
fk := nodes[i].UserID
|
||||
if _, ok := nodeids[fk]; !ok {
|
||||
ids = append(ids, fk)
|
||||
}
|
||||
|
|
@ -433,7 +422,7 @@ func (ptq *PasswordTokenQuery) loadUser(ctx context.Context, query *UserQuery, n
|
|||
for _, n := range neighbors {
|
||||
nodes, ok := nodeids[n.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf(`unexpected foreign-key "password_token_user" returned %v`, n.ID)
|
||||
return fmt.Errorf(`unexpected foreign-key "user_id" returned %v`, n.ID)
|
||||
}
|
||||
for i := range nodes {
|
||||
assign(nodes[i], n)
|
||||
|
|
@ -467,6 +456,9 @@ func (ptq *PasswordTokenQuery) querySpec() *sqlgraph.QuerySpec {
|
|||
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
|
||||
}
|
||||
}
|
||||
if ptq.withUser != nil {
|
||||
_spec.Node.AddColumnOnce(passwordtoken.FieldUserID)
|
||||
}
|
||||
}
|
||||
if ps := ptq.predicates; len(ps) > 0 {
|
||||
_spec.Predicate = func(selector *sql.Selector) {
|
||||
|
|
|
|||
|
|
@ -29,16 +29,30 @@ func (ptu *PasswordTokenUpdate) Where(ps ...predicate.PasswordToken) *PasswordTo
|
|||
return ptu
|
||||
}
|
||||
|
||||
// SetHash sets the "hash" field.
|
||||
func (ptu *PasswordTokenUpdate) SetHash(s string) *PasswordTokenUpdate {
|
||||
ptu.mutation.SetHash(s)
|
||||
// SetToken sets the "token" field.
|
||||
func (ptu *PasswordTokenUpdate) SetToken(s string) *PasswordTokenUpdate {
|
||||
ptu.mutation.SetToken(s)
|
||||
return ptu
|
||||
}
|
||||
|
||||
// SetNillableHash sets the "hash" field if the given value is not nil.
|
||||
func (ptu *PasswordTokenUpdate) SetNillableHash(s *string) *PasswordTokenUpdate {
|
||||
// SetNillableToken sets the "token" field if the given value is not nil.
|
||||
func (ptu *PasswordTokenUpdate) SetNillableToken(s *string) *PasswordTokenUpdate {
|
||||
if s != nil {
|
||||
ptu.SetHash(*s)
|
||||
ptu.SetToken(*s)
|
||||
}
|
||||
return ptu
|
||||
}
|
||||
|
||||
// SetUserID sets the "user_id" field.
|
||||
func (ptu *PasswordTokenUpdate) SetUserID(i int) *PasswordTokenUpdate {
|
||||
ptu.mutation.SetUserID(i)
|
||||
return ptu
|
||||
}
|
||||
|
||||
// SetNillableUserID sets the "user_id" field if the given value is not nil.
|
||||
func (ptu *PasswordTokenUpdate) SetNillableUserID(i *int) *PasswordTokenUpdate {
|
||||
if i != nil {
|
||||
ptu.SetUserID(*i)
|
||||
}
|
||||
return ptu
|
||||
}
|
||||
|
|
@ -57,12 +71,6 @@ func (ptu *PasswordTokenUpdate) SetNillableCreatedAt(t *time.Time) *PasswordToke
|
|||
return ptu
|
||||
}
|
||||
|
||||
// SetUserID sets the "user" edge to the User entity by ID.
|
||||
func (ptu *PasswordTokenUpdate) SetUserID(id int) *PasswordTokenUpdate {
|
||||
ptu.mutation.SetUserID(id)
|
||||
return ptu
|
||||
}
|
||||
|
||||
// SetUser sets the "user" edge to the User entity.
|
||||
func (ptu *PasswordTokenUpdate) SetUser(u *User) *PasswordTokenUpdate {
|
||||
return ptu.SetUserID(u.ID)
|
||||
|
|
@ -108,9 +116,9 @@ func (ptu *PasswordTokenUpdate) ExecX(ctx context.Context) {
|
|||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (ptu *PasswordTokenUpdate) check() error {
|
||||
if v, ok := ptu.mutation.Hash(); ok {
|
||||
if err := passwordtoken.HashValidator(v); err != nil {
|
||||
return &ValidationError{Name: "hash", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.hash": %w`, err)}
|
||||
if v, ok := ptu.mutation.Token(); ok {
|
||||
if err := passwordtoken.TokenValidator(v); err != nil {
|
||||
return &ValidationError{Name: "token", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.token": %w`, err)}
|
||||
}
|
||||
}
|
||||
if ptu.mutation.UserCleared() && len(ptu.mutation.UserIDs()) > 0 {
|
||||
|
|
@ -131,8 +139,8 @@ func (ptu *PasswordTokenUpdate) sqlSave(ctx context.Context) (n int, err error)
|
|||
}
|
||||
}
|
||||
}
|
||||
if value, ok := ptu.mutation.Hash(); ok {
|
||||
_spec.SetField(passwordtoken.FieldHash, field.TypeString, value)
|
||||
if value, ok := ptu.mutation.Token(); ok {
|
||||
_spec.SetField(passwordtoken.FieldToken, field.TypeString, value)
|
||||
}
|
||||
if value, ok := ptu.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(passwordtoken.FieldCreatedAt, field.TypeTime, value)
|
||||
|
|
@ -186,16 +194,30 @@ type PasswordTokenUpdateOne struct {
|
|||
mutation *PasswordTokenMutation
|
||||
}
|
||||
|
||||
// SetHash sets the "hash" field.
|
||||
func (ptuo *PasswordTokenUpdateOne) SetHash(s string) *PasswordTokenUpdateOne {
|
||||
ptuo.mutation.SetHash(s)
|
||||
// SetToken sets the "token" field.
|
||||
func (ptuo *PasswordTokenUpdateOne) SetToken(s string) *PasswordTokenUpdateOne {
|
||||
ptuo.mutation.SetToken(s)
|
||||
return ptuo
|
||||
}
|
||||
|
||||
// SetNillableHash sets the "hash" field if the given value is not nil.
|
||||
func (ptuo *PasswordTokenUpdateOne) SetNillableHash(s *string) *PasswordTokenUpdateOne {
|
||||
// SetNillableToken sets the "token" field if the given value is not nil.
|
||||
func (ptuo *PasswordTokenUpdateOne) SetNillableToken(s *string) *PasswordTokenUpdateOne {
|
||||
if s != nil {
|
||||
ptuo.SetHash(*s)
|
||||
ptuo.SetToken(*s)
|
||||
}
|
||||
return ptuo
|
||||
}
|
||||
|
||||
// SetUserID sets the "user_id" field.
|
||||
func (ptuo *PasswordTokenUpdateOne) SetUserID(i int) *PasswordTokenUpdateOne {
|
||||
ptuo.mutation.SetUserID(i)
|
||||
return ptuo
|
||||
}
|
||||
|
||||
// SetNillableUserID sets the "user_id" field if the given value is not nil.
|
||||
func (ptuo *PasswordTokenUpdateOne) SetNillableUserID(i *int) *PasswordTokenUpdateOne {
|
||||
if i != nil {
|
||||
ptuo.SetUserID(*i)
|
||||
}
|
||||
return ptuo
|
||||
}
|
||||
|
|
@ -214,12 +236,6 @@ func (ptuo *PasswordTokenUpdateOne) SetNillableCreatedAt(t *time.Time) *Password
|
|||
return ptuo
|
||||
}
|
||||
|
||||
// SetUserID sets the "user" edge to the User entity by ID.
|
||||
func (ptuo *PasswordTokenUpdateOne) SetUserID(id int) *PasswordTokenUpdateOne {
|
||||
ptuo.mutation.SetUserID(id)
|
||||
return ptuo
|
||||
}
|
||||
|
||||
// SetUser sets the "user" edge to the User entity.
|
||||
func (ptuo *PasswordTokenUpdateOne) SetUser(u *User) *PasswordTokenUpdateOne {
|
||||
return ptuo.SetUserID(u.ID)
|
||||
|
|
@ -278,9 +294,9 @@ func (ptuo *PasswordTokenUpdateOne) ExecX(ctx context.Context) {
|
|||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (ptuo *PasswordTokenUpdateOne) check() error {
|
||||
if v, ok := ptuo.mutation.Hash(); ok {
|
||||
if err := passwordtoken.HashValidator(v); err != nil {
|
||||
return &ValidationError{Name: "hash", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.hash": %w`, err)}
|
||||
if v, ok := ptuo.mutation.Token(); ok {
|
||||
if err := passwordtoken.TokenValidator(v); err != nil {
|
||||
return &ValidationError{Name: "token", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.token": %w`, err)}
|
||||
}
|
||||
}
|
||||
if ptuo.mutation.UserCleared() && len(ptuo.mutation.UserIDs()) > 0 {
|
||||
|
|
@ -318,8 +334,8 @@ func (ptuo *PasswordTokenUpdateOne) sqlSave(ctx context.Context) (_node *Passwor
|
|||
}
|
||||
}
|
||||
}
|
||||
if value, ok := ptuo.mutation.Hash(); ok {
|
||||
_spec.SetField(passwordtoken.FieldHash, field.TypeString, value)
|
||||
if value, ok := ptuo.mutation.Token(); ok {
|
||||
_spec.SetField(passwordtoken.FieldToken, field.TypeString, value)
|
||||
}
|
||||
if value, ok := ptuo.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(passwordtoken.FieldCreatedAt, field.TypeTime, value)
|
||||
|
|
|
|||
|
|
@ -14,14 +14,16 @@ import (
|
|||
// (default values, validators, hooks and policies) and stitches it
|
||||
// to their package variables.
|
||||
func init() {
|
||||
passwordtokenHooks := schema.PasswordToken{}.Hooks()
|
||||
passwordtoken.Hooks[0] = passwordtokenHooks[0]
|
||||
passwordtokenFields := schema.PasswordToken{}.Fields()
|
||||
_ = passwordtokenFields
|
||||
// passwordtokenDescHash is the schema descriptor for hash field.
|
||||
passwordtokenDescHash := passwordtokenFields[0].Descriptor()
|
||||
// passwordtoken.HashValidator is a validator for the "hash" field. It is called by the builders before save.
|
||||
passwordtoken.HashValidator = passwordtokenDescHash.Validators[0].(func(string) error)
|
||||
// passwordtokenDescToken is the schema descriptor for token field.
|
||||
passwordtokenDescToken := passwordtokenFields[0].Descriptor()
|
||||
// passwordtoken.TokenValidator is a validator for the "token" field. It is called by the builders before save.
|
||||
passwordtoken.TokenValidator = passwordtokenDescToken.Validators[0].(func(string) error)
|
||||
// passwordtokenDescCreatedAt is the schema descriptor for created_at field.
|
||||
passwordtokenDescCreatedAt := passwordtokenFields[1].Descriptor()
|
||||
passwordtokenDescCreatedAt := passwordtokenFields[2].Descriptor()
|
||||
// passwordtoken.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
passwordtoken.DefaultCreatedAt = passwordtokenDescCreatedAt.Default.(func() time.Time)
|
||||
userHooks := schema.User{}.Hooks()
|
||||
|
|
@ -35,7 +37,21 @@ func init() {
|
|||
// userDescEmail is the schema descriptor for email field.
|
||||
userDescEmail := userFields[1].Descriptor()
|
||||
// user.EmailValidator is a validator for the "email" field. It is called by the builders before save.
|
||||
user.EmailValidator = userDescEmail.Validators[0].(func(string) error)
|
||||
user.EmailValidator = func() func(string) error {
|
||||
validators := userDescEmail.Validators
|
||||
fns := [...]func(string) error{
|
||||
validators[0].(func(string) error),
|
||||
validators[1].(func(string) error),
|
||||
}
|
||||
return func(email string) error {
|
||||
for _, fn := range fns {
|
||||
if err := fn(email); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
// userDescPassword is the schema descriptor for password field.
|
||||
userDescPassword := userFields[2].Descriptor()
|
||||
// user.PasswordValidator is a validator for the "password" field. It is called by the builders before save.
|
||||
|
|
@ -44,13 +60,17 @@ func init() {
|
|||
userDescVerified := userFields[3].Descriptor()
|
||||
// user.DefaultVerified holds the default value on creation for the verified field.
|
||||
user.DefaultVerified = userDescVerified.Default.(bool)
|
||||
// userDescAdmin is the schema descriptor for admin field.
|
||||
userDescAdmin := userFields[4].Descriptor()
|
||||
// user.DefaultAdmin holds the default value on creation for the admin field.
|
||||
user.DefaultAdmin = userDescAdmin.Default.(bool)
|
||||
// userDescCreatedAt is the schema descriptor for created_at field.
|
||||
userDescCreatedAt := userFields[4].Descriptor()
|
||||
userDescCreatedAt := userFields[5].Descriptor()
|
||||
// user.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
user.DefaultCreatedAt = userDescCreatedAt.Default.(func() time.Time)
|
||||
}
|
||||
|
||||
const (
|
||||
Version = "v0.14.2" // Version of ent codegen.
|
||||
Sum = "h1:ywld/j2Rx4EmnIKs8eZ29cbFA1zpB+DA9TLL5l3rlq0=" // Sum of ent codegen.
|
||||
Version = "v0.14.4" // Version of ent codegen.
|
||||
Sum = "h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=" // Sum of ent codegen.
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/schema/edge"
|
||||
"entgo.io/ent/schema/field"
|
||||
ge "github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/ent/hook"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// PasswordToken holds the schema definition for the PasswordToken entity.
|
||||
|
|
@ -16,9 +20,10 @@ type PasswordToken struct {
|
|||
// Fields of the PasswordToken.
|
||||
func (PasswordToken) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.String("hash").
|
||||
field.String("token").
|
||||
Sensitive().
|
||||
NotEmpty(),
|
||||
field.Int("user_id"),
|
||||
field.Time("created_at").
|
||||
Default(time.Now),
|
||||
}
|
||||
|
|
@ -28,7 +33,30 @@ func (PasswordToken) Fields() []ent.Field {
|
|||
func (PasswordToken) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.To("user", User.Type).
|
||||
Field("user_id").
|
||||
Required().
|
||||
Unique(),
|
||||
}
|
||||
}
|
||||
|
||||
// Hooks of the PasswordToken.
|
||||
func (PasswordToken) Hooks() []ent.Hook {
|
||||
return []ent.Hook{
|
||||
hook.On(
|
||||
func(next ent.Mutator) ent.Mutator {
|
||||
return hook.PasswordTokenFunc(func(ctx context.Context, m *ge.PasswordTokenMutation) (ent.Value, error) {
|
||||
if v, exists := m.Token(); exists {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m.SetToken(string(hash))
|
||||
}
|
||||
return next.Mutate(ctx, m)
|
||||
})
|
||||
},
|
||||
// Limit the hook only for these operations.
|
||||
ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package schema
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ge "github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/ent/hook"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/schema/edge"
|
||||
|
|
@ -25,12 +27,18 @@ func (User) Fields() []ent.Field {
|
|||
NotEmpty(),
|
||||
field.String("email").
|
||||
NotEmpty().
|
||||
Unique(),
|
||||
Unique().
|
||||
Validate(func(s string) error {
|
||||
_, err := mail.ParseAddress(s)
|
||||
return err
|
||||
}),
|
||||
field.String("password").
|
||||
Sensitive().
|
||||
NotEmpty(),
|
||||
field.Bool("verified").
|
||||
Default(false),
|
||||
field.Bool("admin").
|
||||
Default(false),
|
||||
field.Time("created_at").
|
||||
Default(time.Now).
|
||||
Immutable(),
|
||||
|
|
@ -54,6 +62,14 @@ func (User) Hooks() []ent.Hook {
|
|||
if v, exists := m.Email(); exists {
|
||||
m.SetEmail(strings.ToLower(v))
|
||||
}
|
||||
|
||||
if v, exists := m.Password(); exists {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m.SetPassword(string(hash))
|
||||
}
|
||||
return next.Mutate(ctx, m)
|
||||
})
|
||||
},
|
||||
|
|
|
|||
13
ent/user.go
13
ent/user.go
|
|
@ -25,6 +25,8 @@ type User struct {
|
|||
Password string `json:"-"`
|
||||
// Verified holds the value of the "verified" field.
|
||||
Verified bool `json:"verified,omitempty"`
|
||||
// Admin holds the value of the "admin" field.
|
||||
Admin bool `json:"admin,omitempty"`
|
||||
// CreatedAt holds the value of the "created_at" field.
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
|
|
@ -56,7 +58,7 @@ func (*User) scanValues(columns []string) ([]any, error) {
|
|||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case user.FieldVerified:
|
||||
case user.FieldVerified, user.FieldAdmin:
|
||||
values[i] = new(sql.NullBool)
|
||||
case user.FieldID:
|
||||
values[i] = new(sql.NullInt64)
|
||||
|
|
@ -109,6 +111,12 @@ func (u *User) assignValues(columns []string, values []any) error {
|
|||
} else if value.Valid {
|
||||
u.Verified = value.Bool
|
||||
}
|
||||
case user.FieldAdmin:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field admin", values[i])
|
||||
} else if value.Valid {
|
||||
u.Admin = value.Bool
|
||||
}
|
||||
case user.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
||||
|
|
@ -167,6 +175,9 @@ func (u *User) String() string {
|
|||
builder.WriteString("verified=")
|
||||
builder.WriteString(fmt.Sprintf("%v", u.Verified))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("admin=")
|
||||
builder.WriteString(fmt.Sprintf("%v", u.Admin))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(u.CreatedAt.Format(time.ANSIC))
|
||||
builder.WriteByte(')')
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ const (
|
|||
FieldPassword = "password"
|
||||
// FieldVerified holds the string denoting the verified field in the database.
|
||||
FieldVerified = "verified"
|
||||
// FieldAdmin holds the string denoting the admin field in the database.
|
||||
FieldAdmin = "admin"
|
||||
// FieldCreatedAt holds the string denoting the created_at field in the database.
|
||||
FieldCreatedAt = "created_at"
|
||||
// EdgeOwner holds the string denoting the owner edge name in mutations.
|
||||
|
|
@ -35,7 +37,7 @@ const (
|
|||
// It exists in this package in order to avoid circular dependency with the "passwordtoken" package.
|
||||
OwnerInverseTable = "password_tokens"
|
||||
// OwnerColumn is the table column denoting the owner relation/edge.
|
||||
OwnerColumn = "password_token_user"
|
||||
OwnerColumn = "user_id"
|
||||
)
|
||||
|
||||
// Columns holds all SQL columns for user fields.
|
||||
|
|
@ -45,6 +47,7 @@ var Columns = []string{
|
|||
FieldEmail,
|
||||
FieldPassword,
|
||||
FieldVerified,
|
||||
FieldAdmin,
|
||||
FieldCreatedAt,
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +76,8 @@ var (
|
|||
PasswordValidator func(string) error
|
||||
// DefaultVerified holds the default value on creation for the "verified" field.
|
||||
DefaultVerified bool
|
||||
// DefaultAdmin holds the default value on creation for the "admin" field.
|
||||
DefaultAdmin bool
|
||||
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
||||
DefaultCreatedAt func() time.Time
|
||||
)
|
||||
|
|
@ -105,6 +110,11 @@ func ByVerified(opts ...sql.OrderTermOption) OrderOption {
|
|||
return sql.OrderByField(FieldVerified, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAdmin orders the results by the admin field.
|
||||
func ByAdmin(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAdmin, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCreatedAt orders the results by the created_at field.
|
||||
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
|
||||
|
|
|
|||
|
|
@ -75,6 +75,11 @@ func Verified(v bool) predicate.User {
|
|||
return predicate.User(sql.FieldEQ(FieldVerified, v))
|
||||
}
|
||||
|
||||
// Admin applies equality check predicate on the "admin" field. It's identical to AdminEQ.
|
||||
func Admin(v bool) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldAdmin, v))
|
||||
}
|
||||
|
||||
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
|
||||
func CreatedAt(v time.Time) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldCreatedAt, v))
|
||||
|
|
@ -285,6 +290,16 @@ func VerifiedNEQ(v bool) predicate.User {
|
|||
return predicate.User(sql.FieldNEQ(FieldVerified, v))
|
||||
}
|
||||
|
||||
// AdminEQ applies the EQ predicate on the "admin" field.
|
||||
func AdminEQ(v bool) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldAdmin, v))
|
||||
}
|
||||
|
||||
// AdminNEQ applies the NEQ predicate on the "admin" field.
|
||||
func AdminNEQ(v bool) predicate.User {
|
||||
return predicate.User(sql.FieldNEQ(FieldAdmin, v))
|
||||
}
|
||||
|
||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||
func CreatedAtEQ(v time.Time) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldCreatedAt, v))
|
||||
|
|
|
|||
|
|
@ -53,6 +53,20 @@ func (uc *UserCreate) SetNillableVerified(b *bool) *UserCreate {
|
|||
return uc
|
||||
}
|
||||
|
||||
// SetAdmin sets the "admin" field.
|
||||
func (uc *UserCreate) SetAdmin(b bool) *UserCreate {
|
||||
uc.mutation.SetAdmin(b)
|
||||
return uc
|
||||
}
|
||||
|
||||
// SetNillableAdmin sets the "admin" field if the given value is not nil.
|
||||
func (uc *UserCreate) SetNillableAdmin(b *bool) *UserCreate {
|
||||
if b != nil {
|
||||
uc.SetAdmin(*b)
|
||||
}
|
||||
return uc
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the "created_at" field.
|
||||
func (uc *UserCreate) SetCreatedAt(t time.Time) *UserCreate {
|
||||
uc.mutation.SetCreatedAt(t)
|
||||
|
|
@ -123,6 +137,10 @@ func (uc *UserCreate) defaults() error {
|
|||
v := user.DefaultVerified
|
||||
uc.mutation.SetVerified(v)
|
||||
}
|
||||
if _, ok := uc.mutation.Admin(); !ok {
|
||||
v := user.DefaultAdmin
|
||||
uc.mutation.SetAdmin(v)
|
||||
}
|
||||
if _, ok := uc.mutation.CreatedAt(); !ok {
|
||||
if user.DefaultCreatedAt == nil {
|
||||
return fmt.Errorf("ent: uninitialized user.DefaultCreatedAt (forgotten import ent/runtime?)")
|
||||
|
|
@ -162,6 +180,9 @@ func (uc *UserCreate) check() error {
|
|||
if _, ok := uc.mutation.Verified(); !ok {
|
||||
return &ValidationError{Name: "verified", err: errors.New(`ent: missing required field "User.verified"`)}
|
||||
}
|
||||
if _, ok := uc.mutation.Admin(); !ok {
|
||||
return &ValidationError{Name: "admin", err: errors.New(`ent: missing required field "User.admin"`)}
|
||||
}
|
||||
if _, ok := uc.mutation.CreatedAt(); !ok {
|
||||
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "User.created_at"`)}
|
||||
}
|
||||
|
|
@ -207,6 +228,10 @@ func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
|
|||
_spec.SetField(user.FieldVerified, field.TypeBool, value)
|
||||
_node.Verified = value
|
||||
}
|
||||
if value, ok := uc.mutation.Admin(); ok {
|
||||
_spec.SetField(user.FieldAdmin, field.TypeBool, value)
|
||||
_node.Admin = value
|
||||
}
|
||||
if value, ok := uc.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(user.FieldCreatedAt, field.TypeTime, value)
|
||||
_node.CreatedAt = value
|
||||
|
|
|
|||
|
|
@ -413,7 +413,9 @@ func (uq *UserQuery) loadOwner(ctx context.Context, query *PasswordTokenQuery, n
|
|||
init(nodes[i])
|
||||
}
|
||||
}
|
||||
query.withFKs = true
|
||||
if len(query.ctx.Fields) > 0 {
|
||||
query.ctx.AppendFieldOnce(passwordtoken.FieldUserID)
|
||||
}
|
||||
query.Where(predicate.PasswordToken(func(s *sql.Selector) {
|
||||
s.Where(sql.InValues(s.C(user.OwnerColumn), fks...))
|
||||
}))
|
||||
|
|
@ -422,13 +424,10 @@ func (uq *UserQuery) loadOwner(ctx context.Context, query *PasswordTokenQuery, n
|
|||
return err
|
||||
}
|
||||
for _, n := range neighbors {
|
||||
fk := n.password_token_user
|
||||
if fk == nil {
|
||||
return fmt.Errorf(`foreign-key "password_token_user" is nil for node %v`, n.ID)
|
||||
}
|
||||
node, ok := nodeids[*fk]
|
||||
fk := n.UserID
|
||||
node, ok := nodeids[fk]
|
||||
if !ok {
|
||||
return fmt.Errorf(`unexpected referenced foreign-key "password_token_user" returned %v for node %v`, *fk, n.ID)
|
||||
return fmt.Errorf(`unexpected referenced foreign-key "user_id" returned %v for node %v`, fk, n.ID)
|
||||
}
|
||||
assign(node, n)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,20 @@ func (uu *UserUpdate) SetNillableVerified(b *bool) *UserUpdate {
|
|||
return uu
|
||||
}
|
||||
|
||||
// SetAdmin sets the "admin" field.
|
||||
func (uu *UserUpdate) SetAdmin(b bool) *UserUpdate {
|
||||
uu.mutation.SetAdmin(b)
|
||||
return uu
|
||||
}
|
||||
|
||||
// SetNillableAdmin sets the "admin" field if the given value is not nil.
|
||||
func (uu *UserUpdate) SetNillableAdmin(b *bool) *UserUpdate {
|
||||
if b != nil {
|
||||
uu.SetAdmin(*b)
|
||||
}
|
||||
return uu
|
||||
}
|
||||
|
||||
// AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs.
|
||||
func (uu *UserUpdate) AddOwnerIDs(ids ...int) *UserUpdate {
|
||||
uu.mutation.AddOwnerIDs(ids...)
|
||||
|
|
@ -196,6 +210,9 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
|||
if value, ok := uu.mutation.Verified(); ok {
|
||||
_spec.SetField(user.FieldVerified, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := uu.mutation.Admin(); ok {
|
||||
_spec.SetField(user.FieldAdmin, field.TypeBool, value)
|
||||
}
|
||||
if uu.mutation.OwnerCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
|
@ -317,6 +334,20 @@ func (uuo *UserUpdateOne) SetNillableVerified(b *bool) *UserUpdateOne {
|
|||
return uuo
|
||||
}
|
||||
|
||||
// SetAdmin sets the "admin" field.
|
||||
func (uuo *UserUpdateOne) SetAdmin(b bool) *UserUpdateOne {
|
||||
uuo.mutation.SetAdmin(b)
|
||||
return uuo
|
||||
}
|
||||
|
||||
// SetNillableAdmin sets the "admin" field if the given value is not nil.
|
||||
func (uuo *UserUpdateOne) SetNillableAdmin(b *bool) *UserUpdateOne {
|
||||
if b != nil {
|
||||
uuo.SetAdmin(*b)
|
||||
}
|
||||
return uuo
|
||||
}
|
||||
|
||||
// AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs.
|
||||
func (uuo *UserUpdateOne) AddOwnerIDs(ids ...int) *UserUpdateOne {
|
||||
uuo.mutation.AddOwnerIDs(ids...)
|
||||
|
|
@ -459,6 +490,9 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
|
|||
if value, ok := uuo.mutation.Verified(); ok {
|
||||
_spec.SetField(user.FieldVerified, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := uuo.mutation.Admin(); ok {
|
||||
_spec.SetField(user.FieldAdmin, field.TypeBool, value)
|
||||
}
|
||||
if uuo.mutation.OwnerCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
|
|
|||
12
go.mod
12
go.mod
|
|
@ -1,18 +1,18 @@
|
|||
module github.com/mikestefanello/pagoda
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.4
|
||||
github.com/PuerkitoBio/goquery v1.10.2
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/gorilla/context v1.1.2
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/mattn/go-sqlite3 v1.14.27
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
github.com/maypok86/otter v1.2.4
|
||||
github.com/mikestefanello/backlite v0.4.0
|
||||
github.com/mikestefanello/backlite v0.5.0
|
||||
github.com/spf13/afero v1.14.0
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
|
|
@ -58,7 +58,7 @@ require (
|
|||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
|
|
|
|||
20
go.sum
20
go.sum
|
|
@ -4,8 +4,8 @@ entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=
|
|||
entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
|
||||
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
|
|
@ -40,8 +40,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
|||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
|
|
@ -74,12 +74,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
|
||||
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
|
||||
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
|
||||
github.com/mikestefanello/backlite v0.4.0 h1:+OTiErTfwydXzeDS8H8iAdbmrQYJ7XuwxFApFORVy+k=
|
||||
github.com/mikestefanello/backlite v0.4.0/go.mod h1:yvGKIFQxscmVYW8dtvlmT3UzfmM0NX2OigwNGsbR46o=
|
||||
github.com/mikestefanello/backlite v0.5.0 h1:6lKZdEYgutdmwV4Id8/t9E/NY14U3fS4/RhiOkFDD4c=
|
||||
github.com/mikestefanello/backlite v0.5.0/go.mod h1:gx6UKLUQY5OVXQkIm3AzNkyPn9OzoKHKuwM4JGrY4tQ=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
|
|
@ -141,8 +141,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ const (
|
|||
|
||||
// ConfigKey is the key used to store the configuration in context.
|
||||
ConfigKey = "config"
|
||||
|
||||
// AdminEntityKey is the key used to store the entity being operated on in the admin panel.
|
||||
AdminEntityKey = "admin:entity"
|
||||
|
||||
// AdminEntityIDKey is the key used to store the ID of the entity being operated on in the admin panel.
|
||||
AdminEntityIDKey = "admin:entity_id"
|
||||
)
|
||||
|
||||
// IsCanceledError determines if an error is due to a context cancellation.
|
||||
|
|
|
|||
211
pkg/handlers/admin.go
Normal file
211
pkg/handlers/admin.go
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/entc/gen"
|
||||
"entgo.io/ent/entc/load"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/backlite/ui"
|
||||
"github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/ent/admin"
|
||||
"github.com/mikestefanello/pagoda/pkg/context"
|
||||
"github.com/mikestefanello/pagoda/pkg/middleware"
|
||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||
"github.com/mikestefanello/pagoda/pkg/pager"
|
||||
"github.com/mikestefanello/pagoda/pkg/redirect"
|
||||
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||
"github.com/mikestefanello/pagoda/pkg/services"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/pages"
|
||||
)
|
||||
|
||||
type Admin struct {
|
||||
orm *ent.Client
|
||||
graph *gen.Graph
|
||||
admin *admin.Handler
|
||||
backlite *ui.Handler
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(new(Admin))
|
||||
}
|
||||
|
||||
func (h *Admin) Init(c *services.Container) error {
|
||||
var err error
|
||||
h.graph = c.Graph
|
||||
h.orm = c.ORM
|
||||
h.admin = admin.NewHandler(h.orm, admin.HandlerConfig{
|
||||
ItemsPerPage: 25,
|
||||
PageQueryKey: pager.QueryKey,
|
||||
TimeFormat: time.DateTime,
|
||||
})
|
||||
h.backlite, err = ui.NewHandler(ui.Config{
|
||||
DB: c.Database,
|
||||
BasePath: "/admin/tasks",
|
||||
ItemsPerPage: 25,
|
||||
ReleaseAfter: c.Config.Tasks.ReleaseAfter,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Admin) Routes(g *echo.Group) {
|
||||
ag := g.Group("/admin", middleware.RequireAdmin)
|
||||
|
||||
entities := ag.Group("/entity")
|
||||
for _, n := range h.graph.Nodes {
|
||||
ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.Name)))
|
||||
ng.GET("", h.EntityList(n)).
|
||||
Name = routenames.AdminEntityList(n.Name)
|
||||
ng.GET("/add", h.EntityAdd(n)).
|
||||
Name = routenames.AdminEntityAdd(n.Name)
|
||||
ng.POST("/add", h.EntityAddSubmit(n)).
|
||||
Name = routenames.AdminEntityAddSubmit(n.Name)
|
||||
ng.GET("/:id/edit", h.EntityEdit(n), h.middlewareEntityLoad(n)).
|
||||
Name = routenames.AdminEntityEdit(n.Name)
|
||||
ng.POST("/:id/edit", h.EntityEditSubmit(n), h.middlewareEntityLoad(n)).
|
||||
Name = routenames.AdminEntityEditSubmit(n.Name)
|
||||
ng.GET("/:id/delete", h.EntityDelete(n), h.middlewareEntityLoad(n)).
|
||||
Name = routenames.AdminEntityDelete(n.Name)
|
||||
ng.POST("/:id/delete", h.EntityDeleteSubmit(n), h.middlewareEntityLoad(n)).
|
||||
Name = routenames.AdminEntityDeleteSubmit(n.Name)
|
||||
}
|
||||
|
||||
tasks := ag.Group("/tasks")
|
||||
tasks.GET("", h.Backlite(h.backlite.Running)).Name = routenames.AdminTasks
|
||||
tasks.GET("/succeeded", h.Backlite(h.backlite.Succeeded))
|
||||
tasks.GET("/failed", h.Backlite(h.backlite.Failed))
|
||||
tasks.GET("/upcoming", h.Backlite(h.backlite.Upcoming))
|
||||
tasks.GET("/task/:id", h.Backlite(h.backlite.Task))
|
||||
tasks.GET("/completed/:id", h.Backlite(h.backlite.TaskCompleted))
|
||||
}
|
||||
|
||||
// middlewareEntityLoad is middleware to extract the entity ID and attempt to load the given entity.
|
||||
func (h *Admin) middlewareEntityLoad(n *gen.Type) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
id, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid entity ID")
|
||||
}
|
||||
|
||||
entity, err := h.admin.Get(ctx, n.Name, id)
|
||||
switch {
|
||||
case err == nil:
|
||||
ctx.Set(context.AdminEntityIDKey, id)
|
||||
ctx.Set(context.AdminEntityKey, map[string][]string(entity))
|
||||
return next(ctx)
|
||||
case ent.IsNotFound(err):
|
||||
return echo.NewHTTPError(http.StatusNotFound, "entity not found")
|
||||
default:
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Admin) EntityList(n *gen.Type) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
list, err := h.admin.List(ctx, n.Name)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return pages.AdminEntityList(ctx, n.Name, list)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Admin) EntityAdd(n *gen.Type) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
return pages.AdminEntityInput(ctx, h.getEntitySchema(n), nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Admin) EntityAddSubmit(n *gen.Type) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
err := h.admin.Create(ctx, n.Name)
|
||||
if err != nil {
|
||||
msg.Danger(ctx, err.Error())
|
||||
return h.EntityAdd(n)(ctx)
|
||||
}
|
||||
|
||||
msg.Success(ctx, fmt.Sprintf("Successfully added %s.", n.Name))
|
||||
|
||||
return redirect.
|
||||
New(ctx).
|
||||
Route(routenames.AdminEntityList(n.Name)).
|
||||
StatusCode(http.StatusFound).
|
||||
Go()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Admin) EntityEdit(n *gen.Type) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
v := ctx.Get(context.AdminEntityKey).(map[string][]string)
|
||||
return pages.AdminEntityInput(ctx, h.getEntitySchema(n), v)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Admin) EntityEditSubmit(n *gen.Type) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
id := ctx.Get(context.AdminEntityIDKey).(int)
|
||||
err := h.admin.Update(ctx, n.Name, id)
|
||||
if err != nil {
|
||||
msg.Danger(ctx, err.Error())
|
||||
return h.EntityEdit(n)(ctx)
|
||||
}
|
||||
|
||||
msg.Success(ctx, fmt.Sprintf("Updated %s.", n.Name))
|
||||
|
||||
return redirect.
|
||||
New(ctx).
|
||||
Route(routenames.AdminEntityList(n.Name)).
|
||||
StatusCode(http.StatusFound).
|
||||
Go()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Admin) EntityDelete(n *gen.Type) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
return pages.AdminEntityDelete(ctx, n.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Admin) EntityDeleteSubmit(n *gen.Type) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
id := ctx.Get(context.AdminEntityIDKey).(int)
|
||||
if err := h.admin.Delete(ctx, n.Name, id); err != nil {
|
||||
msg.Danger(ctx, err.Error())
|
||||
return h.EntityDelete(n)(ctx)
|
||||
}
|
||||
|
||||
msg.Success(ctx, fmt.Sprintf("Successfully deleted %s (ID %d).", n.Name, id))
|
||||
|
||||
return redirect.
|
||||
New(ctx).
|
||||
Route(routenames.AdminEntityList(n.Name)).
|
||||
StatusCode(http.StatusFound).
|
||||
Go()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Admin) getEntitySchema(n *gen.Type) *load.Schema {
|
||||
for _, s := range h.graph.Schemas {
|
||||
if s.Name == n.Name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Admin) Backlite(handler func(http.ResponseWriter, *http.Request) error) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if id := c.Param("id"); id != "" {
|
||||
c.Request().SetPathValue("task", id)
|
||||
}
|
||||
return handler(c.Response().Writer, c.Request())
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/config"
|
||||
"github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/ent/user"
|
||||
"github.com/mikestefanello/pagoda/pkg/context"
|
||||
|
|
@ -22,6 +23,7 @@ import (
|
|||
)
|
||||
|
||||
type Auth struct {
|
||||
config *config.Config
|
||||
auth *services.AuthClient
|
||||
mail *services.MailClient
|
||||
orm *ent.Client
|
||||
|
|
@ -32,6 +34,7 @@ func init() {
|
|||
}
|
||||
|
||||
func (h *Auth) Init(c *services.Container) error {
|
||||
h.config = c.Config
|
||||
h.orm = c.ORM
|
||||
h.auth = c.Auth
|
||||
h.mail = c.Mail
|
||||
|
|
@ -39,10 +42,10 @@ func (h *Auth) Init(c *services.Container) error {
|
|||
}
|
||||
|
||||
func (h *Auth) Routes(g *echo.Group) {
|
||||
g.GET("/logout", h.Logout, middleware.RequireAuthentication()).Name = routenames.Logout
|
||||
g.GET("/logout", h.Logout, middleware.RequireAuthentication).Name = routenames.Logout
|
||||
g.GET("/email/verify/:token", h.VerifyEmail).Name = routenames.VerifyEmail
|
||||
|
||||
noAuth := g.Group("/user", middleware.RequireNoAuthentication())
|
||||
noAuth := g.Group("/user", middleware.RequireNoAuthentication)
|
||||
noAuth.GET("/login", h.LoginPage).Name = routenames.Login
|
||||
noAuth.POST("/login", h.LoginSubmit).Name = routenames.LoginSubmit
|
||||
noAuth.GET("/register", h.RegisterPage).Name = routenames.Register
|
||||
|
|
@ -111,7 +114,7 @@ func (h *Auth) ForgotPasswordSubmit(ctx echo.Context) error {
|
|||
Compose().
|
||||
To(u.Email).
|
||||
Subject("Reset your password").
|
||||
Body(fmt.Sprintf("Go here to reset your password: %s", url)).
|
||||
Body(fmt.Sprintf("Go here to reset your password: %s", h.config.App.Host+url)).
|
||||
Send(ctx)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -206,18 +209,12 @@ func (h *Auth) RegisterSubmit(ctx echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Hash the password.
|
||||
pwHash, err := h.auth.HashPassword(input.Password)
|
||||
if err != nil {
|
||||
return fail(err, "unable to hash password")
|
||||
}
|
||||
|
||||
// Attempt creating the user.
|
||||
u, err := h.orm.User.
|
||||
Create().
|
||||
SetName(input.Name).
|
||||
SetEmail(input.Email).
|
||||
SetPassword(pwHash).
|
||||
SetPassword(input.Password).
|
||||
Save(ctx.Request().Context())
|
||||
|
||||
switch err.(type) {
|
||||
|
|
@ -305,19 +302,13 @@ func (h *Auth) ResetPasswordSubmit(ctx echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Hash the new password.
|
||||
hash, err := h.auth.HashPassword(input.Password)
|
||||
if err != nil {
|
||||
return fail(err, "unable to hash password")
|
||||
}
|
||||
|
||||
// Get the requesting user.
|
||||
usr := ctx.Get(context.UserKey).(*ent.User)
|
||||
|
||||
// Update the user.
|
||||
_, err = usr.
|
||||
Update().
|
||||
SetPassword(hash).
|
||||
SetPassword(input.Password).
|
||||
Save(ctx.Request().Context())
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func (e *Error) Page(err error, ctx echo.Context) {
|
|||
}
|
||||
|
||||
// Set the status code.
|
||||
ctx.Response().Status = code
|
||||
ctx.Response().WriteHeader(code)
|
||||
|
||||
// Render the error page.
|
||||
if err = pages.Error(ctx, code); err != nil {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ func BuildRouter(c *services.Container) error {
|
|||
// Create a cookie store for session data.
|
||||
cookieStore := sessions.NewCookieStore([]byte(c.Config.App.EncryptionKey))
|
||||
cookieStore.Options.HttpOnly = true
|
||||
cookieStore.Options.Secure = true
|
||||
cookieStore.Options.SameSite = http.SameSiteStrictMode
|
||||
|
||||
g.Use(
|
||||
|
|
@ -52,7 +51,6 @@ func BuildRouter(c *services.Container) error {
|
|||
echomw.CSRFWithConfig(echomw.CSRFConfig{
|
||||
TokenLookup: "form:csrf",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSecure: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
ContextKey: context.CSRFKey,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ import (
|
|||
"github.com/mikestefanello/pagoda/pkg/context"
|
||||
"github.com/mikestefanello/pagoda/pkg/log"
|
||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||
"github.com/mikestefanello/pagoda/pkg/services"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// LoadAuthenticatedUser loads the authenticated user, if one, and stores in context
|
||||
// LoadAuthenticatedUser loads the authenticated user, if one, and stores in context.
|
||||
func LoadAuthenticatedUser(authClient *services.AuthClient) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
|
@ -40,7 +41,7 @@ func LoadAuthenticatedUser(authClient *services.AuthClient) echo.MiddlewareFunc
|
|||
// LoadValidPasswordToken loads a valid password token entity that matches the user and token
|
||||
// provided in path parameters
|
||||
// If the token is invalid, the user will be redirected to the forgot password route
|
||||
// This requires that the user owning the token is loaded in to context
|
||||
// This requires that the user owning the token is loaded in to context.
|
||||
func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
|
@ -50,13 +51,13 @@ func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc
|
|||
}
|
||||
usr := c.Get(context.UserKey).(*ent.User)
|
||||
|
||||
// Extract the token ID
|
||||
// Extract the token ID.
|
||||
tokenID, err := strconv.Atoi(c.Param("password_token"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
// Attempt to load a valid password token
|
||||
// Attempt to load a valid password token.
|
||||
token, err := authClient.GetValidPasswordToken(
|
||||
c,
|
||||
usr.ID,
|
||||
|
|
@ -70,8 +71,7 @@ func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc
|
|||
return next(c)
|
||||
case services.InvalidPasswordTokenError:
|
||||
msg.Warning(c, "The link is either invalid or has expired. Please request a new one.")
|
||||
// TODO use the const for route name
|
||||
return c.Redirect(http.StatusFound, c.Echo().Reverse("forgot_password"))
|
||||
return c.Redirect(http.StatusFound, c.Echo().Reverse(routenames.ForgotPassword))
|
||||
default:
|
||||
return echo.NewHTTPError(
|
||||
http.StatusInternalServerError,
|
||||
|
|
@ -82,9 +82,8 @@ func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc
|
|||
}
|
||||
}
|
||||
|
||||
// RequireAuthentication requires that the user be authenticated in order to proceed
|
||||
func RequireAuthentication() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
// RequireAuthentication requires that the user be authenticated in order to proceed.
|
||||
func RequireAuthentication(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if u := c.Get(context.AuthenticatedUserKey); u == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||
|
|
@ -93,11 +92,9 @@ func RequireAuthentication() echo.MiddlewareFunc {
|
|||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequireNoAuthentication requires that the user not be authenticated in order to proceed
|
||||
func RequireNoAuthentication() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
// RequireNoAuthentication requires that the user not be authenticated in order to proceed.
|
||||
func RequireNoAuthentication(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if u := c.Get(context.AuthenticatedUserKey); u != nil {
|
||||
return echo.NewHTTPError(http.StatusForbidden)
|
||||
|
|
@ -106,4 +103,18 @@ func RequireNoAuthentication() echo.MiddlewareFunc {
|
|||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdmin requires that the authenticated user be an admin in order to proceed.
|
||||
func RequireAdmin(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if u := c.Get(context.AuthenticatedUserKey); u != nil {
|
||||
if user, ok := u.(*ent.User); ok {
|
||||
if user.Admin {
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
goctx "context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
|
@ -40,7 +41,7 @@ func TestRequireAuthentication(t *testing.T) {
|
|||
tests.InitSession(ctx)
|
||||
|
||||
// Not logged in
|
||||
err := tests.ExecuteMiddleware(ctx, RequireAuthentication())
|
||||
err := tests.ExecuteMiddleware(ctx, RequireAuthentication)
|
||||
tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized)
|
||||
|
||||
// Login
|
||||
|
|
@ -49,7 +50,7 @@ func TestRequireAuthentication(t *testing.T) {
|
|||
_ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
|
||||
|
||||
// Logged in
|
||||
err = tests.ExecuteMiddleware(ctx, RequireAuthentication())
|
||||
err = tests.ExecuteMiddleware(ctx, RequireAuthentication)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ func TestRequireNoAuthentication(t *testing.T) {
|
|||
tests.InitSession(ctx)
|
||||
|
||||
// Not logged in
|
||||
err := tests.ExecuteMiddleware(ctx, RequireNoAuthentication())
|
||||
err := tests.ExecuteMiddleware(ctx, RequireNoAuthentication)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Login
|
||||
|
|
@ -67,7 +68,7 @@ func TestRequireNoAuthentication(t *testing.T) {
|
|||
_ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
|
||||
|
||||
// Logged in
|
||||
err = tests.ExecuteMiddleware(ctx, RequireNoAuthentication())
|
||||
err = tests.ExecuteMiddleware(ctx, RequireNoAuthentication)
|
||||
tests.AssertHTTPErrorCode(t, err, http.StatusForbidden)
|
||||
}
|
||||
|
||||
|
|
@ -109,3 +110,36 @@ func TestLoadValidPasswordToken(t *testing.T) {
|
|||
require.True(t, ok)
|
||||
assert.Equal(t, pt.ID, ctxPt.ID)
|
||||
}
|
||||
|
||||
func TestRequireAdmin(t *testing.T) {
|
||||
ctx, _ := tests.NewContext(c.Web, "/")
|
||||
tests.InitSession(ctx)
|
||||
|
||||
// Not logged in
|
||||
err := tests.ExecuteMiddleware(ctx, RequireAdmin)
|
||||
tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized)
|
||||
|
||||
// Login as a non-admin
|
||||
err = c.Auth.Login(ctx, usr.ID)
|
||||
require.NoError(t, err)
|
||||
_ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
|
||||
|
||||
// Logged in as a non-admin
|
||||
err = tests.ExecuteMiddleware(ctx, RequireAdmin)
|
||||
tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized)
|
||||
|
||||
// Create an admin and login
|
||||
adm, err := tests.CreateUser(c.ORM)
|
||||
require.NoError(t, err)
|
||||
err = c.ORM.User.Update().
|
||||
SetAdmin(true).
|
||||
Exec(goctx.Background())
|
||||
require.NoError(t, err)
|
||||
err = c.Auth.Login(ctx, adm.ID)
|
||||
require.NoError(t, err)
|
||||
_ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
|
||||
|
||||
// Logged in as an admin
|
||||
err = tests.ExecuteMiddleware(ctx, RequireAdmin)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func (r *Redirect) Params(params ...any) *Redirect {
|
|||
return r
|
||||
}
|
||||
|
||||
// StatusCode sets the HTTP status code which defaults to http.StatusFound.
|
||||
// StatusCode sets the HTTP status code which defaults to http.StatusTemporaryRedirect.
|
||||
// Does not apply to HTMX redirects.
|
||||
func (r *Redirect) StatusCode(code int) *Redirect {
|
||||
r.status = code
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package routenames
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
Home = "home"
|
||||
About = "about"
|
||||
|
|
@ -22,4 +26,33 @@ const (
|
|||
CacheSubmit = "cache.submit"
|
||||
Files = "files"
|
||||
FilesSubmit = "files.submit"
|
||||
AdminTasks = "admin:tasks"
|
||||
)
|
||||
|
||||
func AdminEntityList(entityTypeName string) string {
|
||||
return fmt.Sprintf("admin:%s_list", entityTypeName)
|
||||
}
|
||||
|
||||
func AdminEntityAdd(entityTypeName string) string {
|
||||
return fmt.Sprintf("admin:%s_add", entityTypeName)
|
||||
}
|
||||
|
||||
func AdminEntityEdit(entityTypeName string) string {
|
||||
return fmt.Sprintf("admin:%s_edit", entityTypeName)
|
||||
}
|
||||
|
||||
func AdminEntityDelete(entityTypeName string) string {
|
||||
return fmt.Sprintf("admin:%s_delete", entityTypeName)
|
||||
}
|
||||
|
||||
func AdminEntityAddSubmit(entityTypeName string) string {
|
||||
return fmt.Sprintf("admin:%s_add.submit", entityTypeName)
|
||||
}
|
||||
|
||||
func AdminEntityEditSubmit(entityTypeName string) string {
|
||||
return fmt.Sprintf("admin:%s_edit.submit", entityTypeName)
|
||||
}
|
||||
|
||||
func AdminEntityDeleteSubmit(entityTypeName string) string {
|
||||
return fmt.Sprintf("admin:%s_delete.submit", entityTypeName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/mikestefanello/pagoda/config"
|
||||
"github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/ent/passwordtoken"
|
||||
|
|
@ -106,15 +106,6 @@ func (c *AuthClient) GetAuthenticatedUser(ctx echo.Context) (*ent.User, error) {
|
|||
return nil, NotAuthenticatedError{}
|
||||
}
|
||||
|
||||
// HashPassword returns a hash of a given password
|
||||
func (c *AuthClient) HashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// CheckPassword check if a given password matches a given hash
|
||||
func (c *AuthClient) CheckPassword(password, hash string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
|
|
@ -123,7 +114,7 @@ func (c *AuthClient) CheckPassword(password, hash string) error {
|
|||
// GeneratePasswordResetToken generates a password reset token for a given user.
|
||||
// For security purposes, the token itself is not stored in the database but rather
|
||||
// a hash of the token, exactly how passwords are handled. This method returns both
|
||||
// the generated token as well as the token entity which only contains the hash.
|
||||
// the generated token and the token entity which only contains the hash.
|
||||
func (c *AuthClient) GeneratePasswordResetToken(ctx echo.Context, userID int) (string, *ent.PasswordToken, error) {
|
||||
// Generate the token, which is what will go in the URL, but not the database
|
||||
token, err := c.RandomToken(c.config.App.PasswordToken.Length)
|
||||
|
|
@ -131,16 +122,10 @@ func (c *AuthClient) GeneratePasswordResetToken(ctx echo.Context, userID int) (s
|
|||
return "", nil, err
|
||||
}
|
||||
|
||||
// Hash the token, which is what will be stored in the database
|
||||
hash, err := c.HashPassword(token)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Create and save the password reset token
|
||||
pt, err := c.orm.PasswordToken.
|
||||
Create().
|
||||
SetHash(hash).
|
||||
SetToken(token).
|
||||
SetUserID(userID).
|
||||
Save(ctx.Request().Context())
|
||||
|
||||
|
|
@ -166,7 +151,7 @@ func (c *AuthClient) GetValidPasswordToken(ctx echo.Context, userID, tokenID int
|
|||
case *ent.NotFoundError:
|
||||
case nil:
|
||||
// Check the token for a hash match
|
||||
if err := c.CheckPassword(token, pt.Hash); err == nil {
|
||||
if err := c.CheckPassword(token, pt.Token); err == nil {
|
||||
return pt, nil
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/mikestefanello/pagoda/ent/passwordtoken"
|
||||
"github.com/mikestefanello/pagoda/ent/user"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
|
@ -41,12 +42,12 @@ func TestAuthClient_Auth(t *testing.T) {
|
|||
assertNoAuth()
|
||||
}
|
||||
|
||||
func TestAuthClient_PasswordHashing(t *testing.T) {
|
||||
func TestAuthClient_CheckPassword(t *testing.T) {
|
||||
pw := "testcheckpassword"
|
||||
hash, err := c.Auth.HashPassword(pw)
|
||||
assert.NoError(t, err)
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, hash, pw)
|
||||
err = c.Auth.CheckPassword(pw, hash)
|
||||
err = c.Auth.CheckPassword(pw, string(hash))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ func TestAuthClient_GeneratePasswordResetToken(t *testing.T) {
|
|||
token, pt, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, token, c.Config.App.PasswordToken.Length)
|
||||
assert.NoError(t, c.Auth.CheckPassword(token, pt.Hash))
|
||||
assert.NoError(t, c.Auth.CheckPassword(token, pt.Token))
|
||||
}
|
||||
|
||||
func TestAuthClient_GetValidPasswordToken(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -5,18 +5,23 @@ import (
|
|||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/mikestefanello/backlite"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
entsql "entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/entc"
|
||||
"entgo.io/ent/entc/gen"
|
||||
"github.com/labstack/echo/v4"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/mikestefanello/backlite"
|
||||
"github.com/mikestefanello/pagoda/config"
|
||||
"github.com/mikestefanello/pagoda/ent"
|
||||
"github.com/mikestefanello/pagoda/pkg/log"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
// Required by ent.
|
||||
_ "github.com/mikestefanello/pagoda/ent/runtime"
|
||||
|
|
@ -46,6 +51,9 @@ type Container struct {
|
|||
// ORM stores a client to the ORM.
|
||||
ORM *ent.Client
|
||||
|
||||
// Graph is the entity graph defined by your Ent schema.
|
||||
Graph *gen.Graph
|
||||
|
||||
// Mail stores an email sending client.
|
||||
Mail *MailClient
|
||||
|
||||
|
|
@ -184,6 +192,16 @@ func (c *Container) initORM() {
|
|||
if err := c.ORM.Schema.Create(context.Background()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Load the graph.
|
||||
_, b, _, _ := runtime.Caller(0)
|
||||
d := path.Join(path.Dir(b))
|
||||
p := filepath.Join(filepath.Dir(d), "../ent/schema")
|
||||
g, err := entc.LoadGraph(p, &gen.Config{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Graph = g
|
||||
}
|
||||
|
||||
// initAuth initializes the authentication client.
|
||||
|
|
@ -224,18 +242,22 @@ func (c *Container) initTasks() {
|
|||
|
||||
// openDB opens a database connection.
|
||||
func openDB(driver, connection string) (*sql.DB, error) {
|
||||
if driver == "sqlite3" {
|
||||
// Helper to automatically create the directories that the specified sqlite file
|
||||
// should reside in, if one.
|
||||
if driver == "sqlite3" {
|
||||
d := strings.Split(connection, "/")
|
||||
|
||||
if len(d) > 1 {
|
||||
path := strings.Join(d[:len(d)-1], "/")
|
||||
dirpath := strings.Join(d[:len(d)-1], "/")
|
||||
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
if err := os.MkdirAll(dirpath, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a random value is required, which is often used for in-memory test databases.
|
||||
if strings.Contains(connection, "$RAND") {
|
||||
connection = strings.Replace(connection, "$RAND", fmt.Sprint(rand.Int()), 1)
|
||||
}
|
||||
}
|
||||
|
||||
return sql.Open(driver, connection)
|
||||
|
|
|
|||
|
|
@ -19,16 +19,16 @@ type (
|
|||
Help string
|
||||
}
|
||||
|
||||
RadiosParams struct {
|
||||
OptionsParams struct {
|
||||
Form form.Form
|
||||
FormField string
|
||||
Name string
|
||||
Label string
|
||||
Value string
|
||||
Options []Radio
|
||||
Options []Choice
|
||||
}
|
||||
|
||||
Radio struct {
|
||||
Choice struct {
|
||||
Value string
|
||||
Label string
|
||||
}
|
||||
|
|
@ -41,6 +41,14 @@ type (
|
|||
Value string
|
||||
Help string
|
||||
}
|
||||
|
||||
CheckboxParams struct {
|
||||
Form form.Form
|
||||
FormField string
|
||||
Name string
|
||||
Label string
|
||||
Checked bool
|
||||
}
|
||||
)
|
||||
|
||||
func ControlGroup(controls ...Node) Node {
|
||||
|
|
@ -80,7 +88,7 @@ func TextareaField(el TextareaFieldParams) Node {
|
|||
)
|
||||
}
|
||||
|
||||
func Radios(el RadiosParams) Node {
|
||||
func Radios(el OptionsParams) Node {
|
||||
buttons := make(Group, len(el.Options))
|
||||
for i, opt := range el.Options {
|
||||
buttons[i] = Label(
|
||||
|
|
@ -106,6 +114,50 @@ func Radios(el RadiosParams) Node {
|
|||
)
|
||||
}
|
||||
|
||||
func SelectList(el OptionsParams) Node {
|
||||
buttons := make(Group, len(el.Options))
|
||||
for i, opt := range el.Options {
|
||||
buttons[i] = Option(
|
||||
Text(opt.Label),
|
||||
Value(opt.Value),
|
||||
If(opt.Value == el.Value, Attr("selected")),
|
||||
)
|
||||
}
|
||||
|
||||
return Div(
|
||||
Class("control field"),
|
||||
Label(Class("label"), Text(el.Label)),
|
||||
Div(
|
||||
Class("select"),
|
||||
Select(
|
||||
Name(el.Name),
|
||||
buttons,
|
||||
),
|
||||
),
|
||||
formFieldErrors(el.Form, el.FormField),
|
||||
)
|
||||
}
|
||||
|
||||
func Checkbox(el CheckboxParams) Node {
|
||||
return Div(
|
||||
Class("field"),
|
||||
Div(
|
||||
Class("control"),
|
||||
Label(
|
||||
Class("checkbox"),
|
||||
Input(
|
||||
Type("checkbox"),
|
||||
Name(el.Name),
|
||||
If(el.Checked, Checked()),
|
||||
Value("true"),
|
||||
),
|
||||
Text(" "+el.Label),
|
||||
),
|
||||
),
|
||||
formFieldErrors(el.Form, el.FormField),
|
||||
)
|
||||
}
|
||||
|
||||
func InputField(el InputFieldParams) Node {
|
||||
return Div(
|
||||
Class("field"),
|
||||
|
|
@ -153,6 +205,8 @@ func FileField(name, label string) Node {
|
|||
|
||||
func formFieldStatusClass(fm form.Form, formField string) string {
|
||||
switch {
|
||||
case fm == nil:
|
||||
return ""
|
||||
case !fm.IsSubmitted():
|
||||
return ""
|
||||
case fm.FieldHasErrors(formField):
|
||||
|
|
@ -163,6 +217,10 @@ func formFieldStatusClass(fm form.Form, formField string) string {
|
|||
}
|
||||
|
||||
func formFieldErrors(fm form.Form, field string) Node {
|
||||
if fm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := fm.GetFieldErrors(field)
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
125
pkg/ui/forms/admin_entity.go
Normal file
125
pkg/ui/forms/admin_entity.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package forms
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"entgo.io/ent/entc/load"
|
||||
"entgo.io/ent/schema/field"
|
||||
"github.com/mikestefanello/pagoda/ent/admin"
|
||||
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||
. "github.com/mikestefanello/pagoda/pkg/ui/components"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func AdminEntity(r *ui.Request, schema *load.Schema, values url.Values) Node {
|
||||
// TODO inline validation?
|
||||
isNew := values == nil
|
||||
nodes := make(Group, 0)
|
||||
|
||||
getValue := func(name string) string {
|
||||
// Values in the submitted form take precedence.
|
||||
if value := r.Context.FormValue(name); value != "" {
|
||||
return value
|
||||
}
|
||||
|
||||
// Fallback to the entity's values, if being edited.
|
||||
if values != nil && len(values[name]) > 0 {
|
||||
return values[name][0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Attempt to add form elements for all editable entity fields.
|
||||
for _, f := range schema.Fields {
|
||||
// TODO cardinality?
|
||||
if !isNew && f.Immutable {
|
||||
continue
|
||||
}
|
||||
|
||||
switch f.Info.Type {
|
||||
case field.TypeString:
|
||||
p := InputFieldParams{
|
||||
Name: f.Name,
|
||||
InputType: "text",
|
||||
Label: admin.FieldLabel(f.Name),
|
||||
Value: getValue(f.Name),
|
||||
}
|
||||
|
||||
if f.Sensitive {
|
||||
p.InputType = "password"
|
||||
if !isNew {
|
||||
p.Placeholder = "*****"
|
||||
p.Help = "SENSITIVE: This field will only be updated if a value is provided."
|
||||
}
|
||||
}
|
||||
nodes = append(nodes, InputField(p))
|
||||
|
||||
case field.TypeTime:
|
||||
nodes = append(nodes, InputField(InputFieldParams{
|
||||
Name: f.Name,
|
||||
InputType: "datetime-local",
|
||||
Label: admin.FieldLabel(f.Name),
|
||||
Value: getValue(f.Name),
|
||||
}))
|
||||
|
||||
case field.TypeInt, field.TypeInt8, field.TypeInt16, field.TypeInt32, field.TypeInt64,
|
||||
field.TypeUint, field.TypeUint8, field.TypeUint16, field.TypeUint32, field.TypeUint64,
|
||||
field.TypeFloat32, field.TypeFloat64:
|
||||
nodes = append(nodes, InputField(InputFieldParams{
|
||||
Name: f.Name,
|
||||
InputType: "number",
|
||||
Label: admin.FieldLabel(f.Name),
|
||||
Value: getValue(f.Name),
|
||||
}))
|
||||
|
||||
case field.TypeBool:
|
||||
nodes = append(nodes, Checkbox(CheckboxParams{
|
||||
Name: f.Name,
|
||||
Label: admin.FieldLabel(f.Name),
|
||||
Checked: getValue(f.Name) == "true",
|
||||
}))
|
||||
|
||||
case field.TypeEnum:
|
||||
options := make([]Choice, 0, len(f.Enums)+1)
|
||||
if f.Optional {
|
||||
options = append(options, Choice{
|
||||
Label: "-",
|
||||
Value: "",
|
||||
})
|
||||
}
|
||||
for _, enum := range f.Enums {
|
||||
options = append(options, Choice{
|
||||
Label: enum.V,
|
||||
Value: enum.V,
|
||||
})
|
||||
}
|
||||
nodes = append(nodes, SelectList(OptionsParams{
|
||||
Name: f.Name,
|
||||
Label: admin.FieldLabel(f.Name),
|
||||
Value: getValue(f.Name),
|
||||
Options: options,
|
||||
}))
|
||||
|
||||
default:
|
||||
nodes = append(nodes, P(Textf("%s not supported", f.Name)))
|
||||
}
|
||||
}
|
||||
|
||||
return Form(
|
||||
Method(http.MethodPost),
|
||||
nodes,
|
||||
ControlGroup(
|
||||
FormButton("is-primary", "Submit"),
|
||||
ButtonLink(
|
||||
r.Path(routenames.AdminEntityList(schema.Name)),
|
||||
"is-secondary",
|
||||
"Cancel",
|
||||
),
|
||||
),
|
||||
CSRF(r),
|
||||
)
|
||||
}
|
||||
30
pkg/ui/forms/admin_entity_delete.go
Normal file
30
pkg/ui/forms/admin_entity_delete.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package forms
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||
. "github.com/mikestefanello/pagoda/pkg/ui/components"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func AdminEntityDelete(r *ui.Request, entityTypeName string) Node {
|
||||
return Form(
|
||||
Method(http.MethodPost),
|
||||
P(
|
||||
Class("subtitle"),
|
||||
Textf("Are you sure you want to delete this %s?", entityTypeName),
|
||||
),
|
||||
ControlGroup(
|
||||
FormButton("is-link", "Delete"),
|
||||
ButtonLink(
|
||||
r.Path(routenames.AdminEntityList(entityTypeName)),
|
||||
"is-secondary",
|
||||
"Cancel",
|
||||
),
|
||||
),
|
||||
CSRF(r),
|
||||
)
|
||||
}
|
||||
|
|
@ -31,13 +31,13 @@ func (f *Contact) Render(r *ui.Request) Node {
|
|||
Label: "Email address",
|
||||
Value: f.Email,
|
||||
}),
|
||||
Radios(RadiosParams{
|
||||
Radios(OptionsParams{
|
||||
Form: f,
|
||||
FormField: "Department",
|
||||
Name: "department",
|
||||
Label: "Department",
|
||||
Value: f.Department,
|
||||
Options: []Radio{
|
||||
Options: []Choice{
|
||||
{Value: "sales", Label: "Sales"},
|
||||
{Value: "marketing", Label: "Marketing"},
|
||||
{Value: "hr", Label: "HR"},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ func Auth(r *ui.Request, content Node) Node {
|
|||
return Doctype(
|
||||
HTML(
|
||||
Lang("en"),
|
||||
Data("theme", "light"),
|
||||
Head(
|
||||
Metatags(r),
|
||||
CSS(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package layouts
|
||||
|
||||
import (
|
||||
"github.com/mikestefanello/pagoda/ent/admin"
|
||||
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/cache"
|
||||
|
|
@ -13,6 +14,7 @@ func Primary(r *ui.Request, content Node) Node {
|
|||
return Doctype(
|
||||
HTML(
|
||||
Lang("en"),
|
||||
Data("theme", "light"),
|
||||
Head(
|
||||
Metatags(r),
|
||||
CSS(),
|
||||
|
|
@ -30,12 +32,15 @@ func Primary(r *ui.Request, content Node) Node {
|
|||
),
|
||||
Div(
|
||||
Class("column is-10"),
|
||||
Div(
|
||||
Class("box"),
|
||||
If(len(r.Title) > 0, H1(Class("title"), Text(r.Title))),
|
||||
FlashMessages(r),
|
||||
content,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
HtmxListeners(r),
|
||||
),
|
||||
),
|
||||
|
|
@ -128,6 +133,39 @@ func search(r *ui.Request) Node {
|
|||
}
|
||||
|
||||
func sidebarMenu(r *ui.Request) Node {
|
||||
adminSubMenu := func() Node {
|
||||
entityTypeNames := admin.GetEntityTypeNames()
|
||||
entityTypeLinks := make(Group, len(entityTypeNames))
|
||||
for _, n := range entityTypeNames {
|
||||
entityTypeLinks = append(entityTypeLinks, MenuLink(r, n, routenames.AdminEntityList(n)))
|
||||
}
|
||||
|
||||
return Group{
|
||||
P(
|
||||
Class("menu-label"),
|
||||
Text("Entities"),
|
||||
),
|
||||
Ul(
|
||||
Class("menu-list"),
|
||||
entityTypeLinks,
|
||||
),
|
||||
P(
|
||||
Class("menu-label"),
|
||||
Text("Monitoring"),
|
||||
),
|
||||
Ul(
|
||||
Class("menu-list"),
|
||||
Li(
|
||||
A(
|
||||
Href(r.Path(routenames.AdminTasks)),
|
||||
Text("Tasks"),
|
||||
Target("_blank"),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return Aside(
|
||||
Class("menu"),
|
||||
HxBoost(),
|
||||
|
|
@ -155,5 +193,6 @@ func sidebarMenu(r *ui.Request) Node {
|
|||
If(!r.IsAuth, MenuLink(r, "Register", routenames.Register)),
|
||||
If(!r.IsAuth, MenuLink(r, "Forgot password", routenames.ForgotPasswordSubmit)),
|
||||
),
|
||||
Iff(r.IsAdmin, adminSubMenu),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
136
pkg/ui/pages/admin_entity.go
Normal file
136
pkg/ui/pages/admin_entity.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"entgo.io/ent/entc/load"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mikestefanello/pagoda/ent/admin"
|
||||
"github.com/mikestefanello/pagoda/pkg/pager"
|
||||
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||
. "github.com/mikestefanello/pagoda/pkg/ui/components"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/forms"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/components"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func AdminEntityDelete(ctx echo.Context, entityTypeName string) error {
|
||||
r := ui.NewRequest(ctx)
|
||||
r.Title = fmt.Sprintf("Delete %s", entityTypeName)
|
||||
|
||||
return r.Render(
|
||||
layouts.Primary,
|
||||
forms.AdminEntityDelete(r, entityTypeName),
|
||||
)
|
||||
}
|
||||
|
||||
func AdminEntityInput(ctx echo.Context, schema *load.Schema, values url.Values) error {
|
||||
r := ui.NewRequest(ctx)
|
||||
if values == nil {
|
||||
r.Title = fmt.Sprintf("Add %s", schema.Name)
|
||||
} else {
|
||||
r.Title = fmt.Sprintf("Edit %s", schema.Name)
|
||||
}
|
||||
|
||||
return r.Render(
|
||||
layouts.Primary,
|
||||
forms.AdminEntity(r, schema, values),
|
||||
)
|
||||
}
|
||||
|
||||
func AdminEntityList(
|
||||
ctx echo.Context,
|
||||
entityTypeName string,
|
||||
entityList *admin.EntityList,
|
||||
) error {
|
||||
r := ui.NewRequest(ctx)
|
||||
r.Title = entityTypeName
|
||||
|
||||
genHeader := func() Node {
|
||||
g := make(Group, 0, len(entityList.Columns)+3)
|
||||
g = append(g, Th(Text("ID")))
|
||||
for _, h := range entityList.Columns {
|
||||
g = append(g, Th(Text(h)))
|
||||
}
|
||||
g = append(g, Th(), Th())
|
||||
return g
|
||||
}
|
||||
|
||||
genRow := func(row admin.EntityValues) Node {
|
||||
g := make(Group, 0, len(row.Values)+3)
|
||||
g = append(g, Th(Text(fmt.Sprint(row.ID))))
|
||||
for _, h := range row.Values {
|
||||
g = append(g, Td(Text(h)))
|
||||
}
|
||||
g = append(g,
|
||||
Td(
|
||||
ButtonLink(
|
||||
r.Path(routenames.AdminEntityEdit(entityTypeName), row.ID),
|
||||
"is-link",
|
||||
"Edit",
|
||||
),
|
||||
),
|
||||
Td(
|
||||
ButtonLink(r.Path(routenames.AdminEntityDelete(entityTypeName), row.ID),
|
||||
"is-danger",
|
||||
"Delete",
|
||||
),
|
||||
),
|
||||
)
|
||||
return g
|
||||
}
|
||||
|
||||
genRows := func() Node {
|
||||
g := make(Group, 0, len(entityList.Entities))
|
||||
for _, row := range entityList.Entities {
|
||||
g = append(g, Tr(genRow(row)))
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
pagedHref := func(page int) string {
|
||||
return fmt.Sprintf("%s?%s=%d",
|
||||
r.Path(routenames.AdminEntityList(entityTypeName)),
|
||||
pager.QueryKey,
|
||||
page,
|
||||
)
|
||||
}
|
||||
|
||||
return r.Render(layouts.Primary, Group{
|
||||
ButtonLink(
|
||||
r.Path(routenames.AdminEntityAdd(entityTypeName)),
|
||||
"is-primary",
|
||||
fmt.Sprintf("Add %s", entityTypeName),
|
||||
),
|
||||
Table(
|
||||
Class("table"),
|
||||
THead(
|
||||
Tr(genHeader()),
|
||||
),
|
||||
TBody(genRows()),
|
||||
),
|
||||
Nav(
|
||||
Class("pagination"),
|
||||
A(
|
||||
Classes{
|
||||
"pagination-previous": true,
|
||||
"is-disabled": entityList.Page == 1,
|
||||
},
|
||||
If(entityList.Page != 1, Href(pagedHref(entityList.Page-1))),
|
||||
Text("Previous page"),
|
||||
),
|
||||
A(
|
||||
Classes{
|
||||
"pagination-previous": true,
|
||||
"is-disabled": !entityList.HasNextPage,
|
||||
},
|
||||
If(entityList.HasNextPage, Href(pagedHref(entityList.Page+1))),
|
||||
Text("Next page"),
|
||||
),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
||||
"github.com/mikestefanello/pagoda/pkg/ui/models"
|
||||
. "maragu.dev/gomponents"
|
||||
. "maragu.dev/gomponents/components"
|
||||
. "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ func Home(ctx echo.Context, posts *models.Posts) error {
|
|||
headerMsg := func() Node {
|
||||
return Group{
|
||||
Section(
|
||||
Class("hero is-info welcome is-small mb-5"),
|
||||
Class("hero is-info welcome is-small mb-3"),
|
||||
Div(
|
||||
Class("hero-body"),
|
||||
Div(
|
||||
|
|
@ -58,6 +59,28 @@ func Home(ctx echo.Context, posts *models.Posts) error {
|
|||
),
|
||||
),
|
||||
),
|
||||
Section(
|
||||
Class("hero is-light is-small mb-5"),
|
||||
Div(
|
||||
Class("hero-body"),
|
||||
Div(
|
||||
Class("container"),
|
||||
B(Text("Admin status: ")),
|
||||
Span(
|
||||
Classes{
|
||||
"tag": true,
|
||||
"is-success": r.IsAdmin,
|
||||
"is-danger": !r.IsAdmin,
|
||||
},
|
||||
Text(fmt.Sprint(r.IsAdmin)),
|
||||
),
|
||||
If(!r.IsAdmin, Span(
|
||||
Class("is-size-7 ml-3"),
|
||||
Raw(`(<a href="https://github.com/mikestefanello/pagoda#create-an-admin-account">click here</a> for instructions to make an admin account)`),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
H2(Class("title"), Text("Recent posts")),
|
||||
H3(Class("subtitle"), Text("Below is an example of both paging and AJAX fetching using HTMX")),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,19 @@ func AddTask(ctx echo.Context, form *forms.Task) error {
|
|||
"",
|
||||
Group{
|
||||
P(Raw("Submitting this form will create an <i>ExampleTask</i> in the task queue. After the specified delay, the message will be logged by the queue processor.")),
|
||||
P(Text("See pkg/tasks and the README for more information.")),
|
||||
P(Raw("See <i>pkg/tasks</i> and the README for more information.")),
|
||||
})
|
||||
}),
|
||||
form.Render(r),
|
||||
Iff(r.Htmx.Target != "task", func() Node {
|
||||
return components.Message(
|
||||
"is-warning",
|
||||
"",
|
||||
Group{
|
||||
If(!r.IsAdmin, P(Text("Log in as an admin in order to access the task and queue monitoring UI."))),
|
||||
If(r.IsAdmin, P(Text("View all queued tasks by clicking on the Tasks link in the sidebar."))),
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
return r.Render(layouts.Primary, g)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ type (
|
|||
// IsAuth stores whether the user is authenticated.
|
||||
IsAuth bool
|
||||
|
||||
// IsAdmin stores whether the user is an admin.
|
||||
IsAdmin bool
|
||||
|
||||
// AuthUser stores the authenticated user.
|
||||
AuthUser *ent.User
|
||||
|
||||
|
|
@ -77,6 +80,7 @@ func NewRequest(ctx echo.Context) *Request {
|
|||
if u := ctx.Get(context.AuthenticatedUserKey); u != nil {
|
||||
p.IsAuth = true
|
||||
p.AuthUser = u.(*ent.User)
|
||||
p.IsAdmin = p.AuthUser.Admin
|
||||
}
|
||||
|
||||
if cfg := ctx.Get(context.ConfigKey); cfg != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue