forked from mirrors/homebox
Initial commit
This commit is contained in:
commit
29f583e936
135 changed files with 18463 additions and 0 deletions
81
backend/internal/config/conf.go
Normal file
81
backend/internal/config/conf.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ardanlabs/conf/v2"
|
||||
"github.com/ardanlabs/conf/v2/yaml"
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
ModeDevelopment = "development"
|
||||
ModeProduction = "production"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mode string `yaml:"mode" conf:"default:development"` // development or production
|
||||
Web WebConfig `yaml:"web"`
|
||||
Database Database `yaml:"database"`
|
||||
Log LoggerConf `yaml:"logger"`
|
||||
Mailer MailerConf `yaml:"mailer"`
|
||||
Seed Seed `yaml:"seed"`
|
||||
Swagger SwaggerConf `yaml:"swagger"`
|
||||
}
|
||||
|
||||
type SwaggerConf struct {
|
||||
Host string `yaml:"host" conf:"default:localhost:7745"`
|
||||
Scheme string `yaml:"scheme" conf:"default:http"`
|
||||
}
|
||||
|
||||
type WebConfig struct {
|
||||
Port string `yaml:"port" conf:"default:7745"`
|
||||
Host string `yaml:"host" conf:"default:127.0.0.1"`
|
||||
}
|
||||
|
||||
// NewConfig parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
|
||||
// file is not read. If the file is not empty, the file is read and the Config struct is returned.
|
||||
func NewConfig(file string) (*Config, error) {
|
||||
var cfg Config
|
||||
|
||||
const prefix = "API"
|
||||
|
||||
help, err := func() (string, error) {
|
||||
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
|
||||
return conf.Parse(prefix, &cfg)
|
||||
} else {
|
||||
yamlData, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return conf.Parse(prefix, &cfg, yaml.WithData(yamlData))
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, conf.ErrHelpWanted) {
|
||||
fmt.Println(help)
|
||||
os.Exit(0)
|
||||
}
|
||||
return &cfg, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Print prints the configuration to stdout as a json indented string
|
||||
// This is useful for debugging. If the marshaller errors out, it will panic.
|
||||
func (c *Config) Print() {
|
||||
res, err := json.MarshalIndent(c, "", " ")
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(string(res))
|
||||
|
||||
}
|
27
backend/internal/config/conf_database.go
Normal file
27
backend/internal/config/conf_database.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
DriverSqlite3 = "sqlite3"
|
||||
DriverPostgres = "postgres"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Driver string `yaml:"driver" conf:"default:sqlite3"`
|
||||
SqliteUrl string `yaml:"sqlite-url" conf:"default:file:ent?mode=memory&cache=shared&_fk=1"`
|
||||
PostgresUrl string `yaml:"postgres-url" conf:""`
|
||||
}
|
||||
|
||||
func (d *Database) GetDriver() string {
|
||||
return d.Driver
|
||||
}
|
||||
|
||||
func (d *Database) GetUrl() string {
|
||||
switch d.Driver {
|
||||
case DriverSqlite3:
|
||||
return d.SqliteUrl
|
||||
case DriverPostgres:
|
||||
return d.PostgresUrl
|
||||
default:
|
||||
panic("unknown database driver")
|
||||
}
|
||||
}
|
36
backend/internal/config/conf_database_test.go
Normal file
36
backend/internal/config/conf_database_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_DatabaseConfig_Sqlite(t *testing.T) {
|
||||
dbConf := &Database{
|
||||
Driver: DriverSqlite3,
|
||||
SqliteUrl: "file:ent?mode=memory&cache=shared&_fk=1",
|
||||
}
|
||||
|
||||
assert.Equal(t, "sqlite3", dbConf.GetDriver())
|
||||
assert.Equal(t, "file:ent?mode=memory&cache=shared&_fk=1", dbConf.GetUrl())
|
||||
}
|
||||
|
||||
func Test_DatabaseConfig_Postgres(t *testing.T) {
|
||||
dbConf := &Database{
|
||||
Driver: DriverPostgres,
|
||||
PostgresUrl: "postgres://user:pass@host:port/dbname?sslmode=disable",
|
||||
}
|
||||
|
||||
assert.Equal(t, "postgres", dbConf.GetDriver())
|
||||
assert.Equal(t, "postgres://user:pass@host:port/dbname?sslmode=disable", dbConf.GetUrl())
|
||||
}
|
||||
|
||||
func Test_DatabaseConfig_Unknown(t *testing.T) {
|
||||
dbConf := &Database{
|
||||
Driver: "null",
|
||||
}
|
||||
|
||||
assert.Panics(t, func() { dbConf.GetUrl() })
|
||||
|
||||
}
|
6
backend/internal/config/conf_logger.go
Normal file
6
backend/internal/config/conf_logger.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package config
|
||||
|
||||
type LoggerConf struct {
|
||||
Level string `conf:"default:debug"`
|
||||
File string `conf:""`
|
||||
}
|
15
backend/internal/config/conf_mailer.go
Normal file
15
backend/internal/config/conf_mailer.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package config
|
||||
|
||||
type MailerConf struct {
|
||||
Host string `conf:""`
|
||||
Port int `conf:""`
|
||||
Username string `conf:""`
|
||||
Password string `conf:""`
|
||||
From string `conf:""`
|
||||
}
|
||||
|
||||
// Ready is a simple check to ensure that the configuration is not empty.
|
||||
// or with it's default state.
|
||||
func (mc *MailerConf) Ready() bool {
|
||||
return mc.Host != "" && mc.Port != 0 && mc.Username != "" && mc.Password != "" && mc.From != ""
|
||||
}
|
40
backend/internal/config/conf_mailer_test.go
Normal file
40
backend/internal/config/conf_mailer_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_MailerReady_Success(t *testing.T) {
|
||||
mc := &MailerConf{
|
||||
Host: "host",
|
||||
Port: 1,
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
From: "from",
|
||||
}
|
||||
|
||||
assert.True(t, mc.Ready())
|
||||
}
|
||||
|
||||
func Test_MailerReady_Failure(t *testing.T) {
|
||||
mc := &MailerConf{}
|
||||
assert.False(t, mc.Ready())
|
||||
|
||||
mc.Host = "host"
|
||||
assert.False(t, mc.Ready())
|
||||
|
||||
mc.Port = 1
|
||||
assert.False(t, mc.Ready())
|
||||
|
||||
mc.Username = "username"
|
||||
assert.False(t, mc.Ready())
|
||||
|
||||
mc.Password = "password"
|
||||
assert.False(t, mc.Ready())
|
||||
|
||||
mc.From = "from"
|
||||
assert.True(t, mc.Ready())
|
||||
|
||||
}
|
13
backend/internal/config/conf_seed.go
Normal file
13
backend/internal/config/conf_seed.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package config
|
||||
|
||||
type SeedUser struct {
|
||||
Name string `yaml:"name"`
|
||||
Email string `yaml:"email"`
|
||||
Password string `yaml:"password"`
|
||||
IsSuperuser bool `yaml:"isSuperuser"`
|
||||
}
|
||||
|
||||
type Seed struct {
|
||||
Enabled bool `yaml:"enabled" conf:"default:false"`
|
||||
Users []SeedUser `yaml:"users"`
|
||||
}
|
27
backend/internal/mapper/users_automapper.go
Normal file
27
backend/internal/mapper/users_automapper.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Code generated by "/pkgs/automapper"; DO NOT EDIT.
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
)
|
||||
|
||||
func UserOutFromModel(from ent.User) types.UserOut {
|
||||
return types.UserOut{
|
||||
ID: from.ID,
|
||||
Name: from.Name,
|
||||
Email: from.Email,
|
||||
Password: from.Password,
|
||||
IsSuperuser: from.IsSuperuser,
|
||||
}
|
||||
}
|
||||
|
||||
func UserOutToModel(from types.UserOut) ent.User {
|
||||
return ent.User{
|
||||
ID: from.ID,
|
||||
Name: from.Name,
|
||||
Email: from.Email,
|
||||
Password: from.Password,
|
||||
IsSuperuser: from.IsSuperuser,
|
||||
}
|
||||
}
|
30
backend/internal/mocks/chimocker/chimocker.go
Normal file
30
backend/internal/mocks/chimocker/chimocker.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package chimocker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Params map[string]string
|
||||
|
||||
// WithUrlParam returns a pointer to a request object with the given URL params
|
||||
// added to a new chi.Context object.
|
||||
func WithUrlParam(r *http.Request, key, value string) *http.Request {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
req := r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, chiCtx))
|
||||
chiCtx.URLParams.Add(key, value)
|
||||
return req
|
||||
}
|
||||
|
||||
// WithUrlParams returns a pointer to a request object with the given URL params
|
||||
// added to a new chi.Context object. for single param assignment see WithUrlParam
|
||||
func WithUrlParams(r *http.Request, params Params) *http.Request {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
req := r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, chiCtx))
|
||||
for key, value := range params {
|
||||
chiCtx.URLParams.Add(key, value)
|
||||
}
|
||||
return req
|
||||
}
|
16
backend/internal/mocks/factories/users.go
Normal file
16
backend/internal/mocks/factories/users.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package factories
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/faker"
|
||||
)
|
||||
|
||||
func UserFactory() types.UserCreate {
|
||||
f := faker.NewFaker()
|
||||
return types.UserCreate{
|
||||
Name: f.RandomString(10),
|
||||
Email: f.RandomEmail(),
|
||||
Password: f.RandomString(10),
|
||||
IsSuperuser: f.RandomBool(),
|
||||
}
|
||||
}
|
11
backend/internal/mocks/mock_logger.go
Normal file
11
backend/internal/mocks/mock_logger.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/logger"
|
||||
)
|
||||
|
||||
func GetStructLogger() *logger.Logger {
|
||||
return logger.New(os.Stdout, logger.LevelDebug)
|
||||
}
|
10
backend/internal/mocks/mocker_services.go
Normal file
10
backend/internal/mocks/mocker_services.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/services"
|
||||
)
|
||||
|
||||
func GetMockServices(repos *repo.AllRepos) *services.AllServices {
|
||||
return services.NewServices(repos)
|
||||
}
|
22
backend/internal/mocks/mocks_ent_repo.go
Normal file
22
backend/internal/mocks/mocks_ent_repo.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func GetEntRepos() (*repo.AllRepos, func() error) {
|
||||
c, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := c.Schema.Create(context.Background()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return repo.EntAllRepos(c), c.Close
|
||||
}
|
38
backend/internal/repo/main_test.go
Normal file
38
backend/internal/repo/main_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var testEntClient *ent.Client
|
||||
var testRepos *AllRepos
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
rand.Seed(int64(time.Now().Unix()))
|
||||
|
||||
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening connection to sqlite: %v", err)
|
||||
}
|
||||
|
||||
if err := client.Schema.Create(context.Background()); err != nil {
|
||||
log.Fatalf("failed creating schema resources: %v", err)
|
||||
}
|
||||
|
||||
testEntClient = client
|
||||
testRepos = EntAllRepos(testEntClient)
|
||||
|
||||
defer client.Close()
|
||||
|
||||
m.Run()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
16
backend/internal/repo/repos_all.go
Normal file
16
backend/internal/repo/repos_all.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package repo
|
||||
|
||||
import "github.com/hay-kot/git-web-template/backend/ent"
|
||||
|
||||
// AllRepos is a container for all the repository interfaces
|
||||
type AllRepos struct {
|
||||
Users UserRepository
|
||||
AuthTokens TokenRepository
|
||||
}
|
||||
|
||||
func EntAllRepos(db *ent.Client) *AllRepos {
|
||||
return &AllRepos{
|
||||
Users: &EntUserRepository{db},
|
||||
AuthTokens: &EntTokenRepository{db},
|
||||
}
|
||||
}
|
74
backend/internal/repo/token_ent.go
Normal file
74
backend/internal/repo/token_ent.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
"github.com/hay-kot/git-web-template/backend/ent/authtokens"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/mapper"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
)
|
||||
|
||||
type EntTokenRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
// GetUserFromToken get's a user from a token
|
||||
func (r *EntTokenRepository) GetUserFromToken(ctx context.Context, token []byte) (types.UserOut, error) {
|
||||
dbToken, err := r.db.AuthTokens.Query().
|
||||
Where(authtokens.Token(token)).
|
||||
Where(authtokens.ExpiresAtGTE(time.Now())).
|
||||
WithUser().
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
return types.UserOut{}, err
|
||||
}
|
||||
|
||||
return mapper.UserOutFromModel(*dbToken.Edges.User), nil
|
||||
}
|
||||
|
||||
// Creates a token for a user
|
||||
func (r *EntTokenRepository) CreateToken(ctx context.Context, createToken types.UserAuthTokenCreate) (types.UserAuthToken, error) {
|
||||
tokenOut := types.UserAuthToken{}
|
||||
|
||||
dbToken, err := r.db.AuthTokens.Create().
|
||||
SetToken(createToken.TokenHash).
|
||||
SetUserID(createToken.UserID).
|
||||
SetExpiresAt(createToken.ExpiresAt).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return tokenOut, err
|
||||
}
|
||||
|
||||
tokenOut.TokenHash = dbToken.Token
|
||||
tokenOut.UserID = createToken.UserID
|
||||
tokenOut.CreatedAt = dbToken.CreatedAt
|
||||
tokenOut.ExpiresAt = dbToken.ExpiresAt
|
||||
|
||||
return tokenOut, nil
|
||||
}
|
||||
|
||||
// DeleteToken remove a single token from the database - equivalent to revoke or logout
|
||||
func (r *EntTokenRepository) DeleteToken(ctx context.Context, token []byte) error {
|
||||
_, err := r.db.AuthTokens.Delete().Where(authtokens.Token(token)).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// PurgeExpiredTokens removes all expired tokens from the database
|
||||
func (r *EntTokenRepository) PurgeExpiredTokens(ctx context.Context) (int, error) {
|
||||
tokensDeleted, err := r.db.AuthTokens.Delete().Where(authtokens.ExpiresAtLTE(time.Now())).Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return tokensDeleted, nil
|
||||
}
|
||||
|
||||
func (r *EntTokenRepository) DeleteAll(ctx context.Context) (int, error) {
|
||||
amount, err := r.db.AuthTokens.Delete().Exec(ctx)
|
||||
return amount, err
|
||||
}
|
110
backend/internal/repo/token_ent_test.go
Normal file
110
backend/internal/repo/token_ent_test.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/hasher"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_EntAuthTokenRepo_CreateToken(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := UserFactory()
|
||||
|
||||
userOut, _ := testRepos.Users.Create(ctx, user)
|
||||
|
||||
expiresAt := time.Now().Add(time.Hour)
|
||||
|
||||
generatedToken := hasher.GenerateToken()
|
||||
|
||||
token, err := testRepos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
|
||||
TokenHash: generatedToken.Hash,
|
||||
ExpiresAt: expiresAt,
|
||||
UserID: userOut.ID,
|
||||
})
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Equal(userOut.ID, token.UserID)
|
||||
assert.Equal(expiresAt, token.ExpiresAt)
|
||||
|
||||
// Cleanup
|
||||
err = testRepos.Users.Delete(ctx, userOut.ID)
|
||||
_, err = testRepos.AuthTokens.DeleteAll(ctx)
|
||||
}
|
||||
|
||||
func Test_EntAuthTokenRepo_GetUserByToken(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := UserFactory()
|
||||
userOut, _ := testRepos.Users.Create(ctx, user)
|
||||
|
||||
expiresAt := time.Now().Add(time.Hour)
|
||||
generatedToken := hasher.GenerateToken()
|
||||
|
||||
token, err := testRepos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
|
||||
TokenHash: generatedToken.Hash,
|
||||
ExpiresAt: expiresAt,
|
||||
UserID: userOut.ID,
|
||||
})
|
||||
|
||||
// Get User from token
|
||||
foundUser, err := testRepos.AuthTokens.GetUserFromToken(ctx, token.TokenHash)
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Equal(userOut.ID, foundUser.ID)
|
||||
assert.Equal(userOut.Name, foundUser.Name)
|
||||
assert.Equal(userOut.Email, foundUser.Email)
|
||||
|
||||
// Cleanup
|
||||
err = testRepos.Users.Delete(ctx, userOut.ID)
|
||||
_, err = testRepos.AuthTokens.DeleteAll(ctx)
|
||||
}
|
||||
|
||||
func Test_EntAuthTokenRepo_PurgeExpiredTokens(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := UserFactory()
|
||||
userOut, _ := testRepos.Users.Create(ctx, user)
|
||||
|
||||
createdTokens := []types.UserAuthToken{}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
expiresAt := time.Now()
|
||||
generatedToken := hasher.GenerateToken()
|
||||
|
||||
createdToken, err := testRepos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
|
||||
TokenHash: generatedToken.Hash,
|
||||
ExpiresAt: expiresAt,
|
||||
UserID: userOut.ID,
|
||||
})
|
||||
|
||||
assert.NoError(err)
|
||||
assert.NotNil(createdToken)
|
||||
|
||||
createdTokens = append(createdTokens, createdToken)
|
||||
|
||||
}
|
||||
|
||||
// Purge expired tokens
|
||||
tokensDeleted, err := testRepos.AuthTokens.PurgeExpiredTokens(ctx)
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Equal(5, tokensDeleted)
|
||||
|
||||
// Check if tokens are deleted
|
||||
for _, token := range createdTokens {
|
||||
_, err := testRepos.AuthTokens.GetUserFromToken(ctx, token.TokenHash)
|
||||
assert.Error(err)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
err = testRepos.Users.Delete(ctx, userOut.ID)
|
||||
_, err = testRepos.AuthTokens.DeleteAll(ctx)
|
||||
}
|
20
backend/internal/repo/token_interface.go
Normal file
20
backend/internal/repo/token_interface.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
)
|
||||
|
||||
type TokenRepository interface {
|
||||
// GetUserFromToken get's a user from a token
|
||||
GetUserFromToken(ctx context.Context, token []byte) (types.UserOut, error)
|
||||
// Creates a token for a user
|
||||
CreateToken(ctx context.Context, createToken types.UserAuthTokenCreate) (types.UserAuthToken, error)
|
||||
// DeleteToken remove a single token from the database - equivalent to revoke or logout
|
||||
DeleteToken(ctx context.Context, token []byte) error
|
||||
// PurgeExpiredTokens removes all expired tokens from the database
|
||||
PurgeExpiredTokens(ctx context.Context) (int, error)
|
||||
// DeleteAll removes all tokens from the database
|
||||
DeleteAll(ctx context.Context) (int, error)
|
||||
}
|
141
backend/internal/repo/users_ent.go
Normal file
141
backend/internal/repo/users_ent.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
"github.com/hay-kot/git-web-template/backend/ent/user"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
)
|
||||
|
||||
type EntUserRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) toUserOut(usr *types.UserOut, entUsr *ent.User) {
|
||||
usr.ID = entUsr.ID
|
||||
usr.Password = entUsr.Password
|
||||
usr.Name = entUsr.Name
|
||||
usr.Email = entUsr.Email
|
||||
usr.IsSuperuser = entUsr.IsSuperuser
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) GetOneId(ctx context.Context, id uuid.UUID) (types.UserOut, error) {
|
||||
usr, err := e.db.User.Query().Where(user.ID(id)).Only(ctx)
|
||||
|
||||
usrOut := types.UserOut{}
|
||||
|
||||
if err != nil {
|
||||
return usrOut, err
|
||||
}
|
||||
|
||||
e.toUserOut(&usrOut, usr)
|
||||
|
||||
return usrOut, nil
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) GetOneEmail(ctx context.Context, email string) (types.UserOut, error) {
|
||||
usr, err := e.db.User.Query().Where(user.Email(email)).Only(ctx)
|
||||
|
||||
usrOut := types.UserOut{}
|
||||
|
||||
if err != nil {
|
||||
return usrOut, err
|
||||
}
|
||||
|
||||
e.toUserOut(&usrOut, usr)
|
||||
|
||||
return usrOut, nil
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) GetAll(ctx context.Context) ([]types.UserOut, error) {
|
||||
users, err := e.db.User.Query().All(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var usrs []types.UserOut
|
||||
|
||||
for _, usr := range users {
|
||||
usrOut := types.UserOut{}
|
||||
e.toUserOut(&usrOut, usr)
|
||||
usrs = append(usrs, usrOut)
|
||||
}
|
||||
|
||||
return usrs, nil
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) Create(ctx context.Context, usr types.UserCreate) (types.UserOut, error) {
|
||||
err := usr.Validate()
|
||||
usrOut := types.UserOut{}
|
||||
|
||||
if err != nil {
|
||||
return usrOut, err
|
||||
}
|
||||
|
||||
entUser, err := e.db.User.
|
||||
Create().
|
||||
SetName(usr.Name).
|
||||
SetEmail(usr.Email).
|
||||
SetPassword(usr.Password).
|
||||
SetIsSuperuser(usr.IsSuperuser).
|
||||
Save(ctx)
|
||||
|
||||
e.toUserOut(&usrOut, entUser)
|
||||
|
||||
return usrOut, err
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) Update(ctx context.Context, ID uuid.UUID, data types.UserUpdate) error {
|
||||
bldr := e.db.User.Update().Where(user.ID(ID))
|
||||
|
||||
if data.Name != nil {
|
||||
bldr = bldr.SetName(*data.Name)
|
||||
}
|
||||
|
||||
if data.Email != nil {
|
||||
bldr = bldr.SetEmail(*data.Email)
|
||||
}
|
||||
|
||||
// TODO: FUTURE
|
||||
// if data.Password != nil {
|
||||
// bldr = bldr.SetPassword(*data.Password)
|
||||
// }
|
||||
|
||||
// if data.IsSuperuser != nil {
|
||||
// bldr = bldr.SetIsSuperuser(*data.IsSuperuser)
|
||||
// }
|
||||
|
||||
_, err := bldr.Save(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := e.db.User.Delete().Where(user.ID(id)).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) DeleteAll(ctx context.Context) error {
|
||||
_, err := e.db.User.Delete().Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *EntUserRepository) GetSuperusers(ctx context.Context) ([]types.UserOut, error) {
|
||||
users, err := e.db.User.Query().Where(user.IsSuperuser(true)).All(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var usrs []types.UserOut
|
||||
|
||||
for _, usr := range users {
|
||||
usrOut := types.UserOut{}
|
||||
e.toUserOut(&usrOut, usr)
|
||||
usrs = append(usrs, usrOut)
|
||||
}
|
||||
|
||||
return usrs, nil
|
||||
}
|
148
backend/internal/repo/users_ent_test.go
Normal file
148
backend/internal/repo/users_ent_test.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/faker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func UserFactory() types.UserCreate {
|
||||
f := faker.NewFaker()
|
||||
return types.UserCreate{
|
||||
Name: f.RandomString(10),
|
||||
Email: f.RandomEmail(),
|
||||
Password: f.RandomString(10),
|
||||
IsSuperuser: f.RandomBool(),
|
||||
}
|
||||
}
|
||||
|
||||
func Test_EntUserRepo_GetOneEmail(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
user := UserFactory()
|
||||
ctx := context.Background()
|
||||
|
||||
testRepos.Users.Create(ctx, user)
|
||||
|
||||
foundUser, err := testRepos.Users.GetOneEmail(ctx, user.Email)
|
||||
|
||||
assert.NotNil(foundUser)
|
||||
assert.Nil(err)
|
||||
assert.Equal(user.Email, foundUser.Email)
|
||||
assert.Equal(user.Name, foundUser.Name)
|
||||
|
||||
// Cleanup
|
||||
testRepos.Users.DeleteAll(ctx)
|
||||
}
|
||||
|
||||
func Test_EntUserRepo_GetOneId(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
user := UserFactory()
|
||||
ctx := context.Background()
|
||||
|
||||
userOut, _ := testRepos.Users.Create(ctx, user)
|
||||
foundUser, err := testRepos.Users.GetOneId(ctx, userOut.ID)
|
||||
|
||||
assert.NotNil(foundUser)
|
||||
assert.Nil(err)
|
||||
assert.Equal(user.Email, foundUser.Email)
|
||||
assert.Equal(user.Name, foundUser.Name)
|
||||
|
||||
// Cleanup
|
||||
testRepos.Users.DeleteAll(ctx)
|
||||
}
|
||||
|
||||
func Test_EntUserRepo_GetAll(t *testing.T) {
|
||||
// Setup
|
||||
toCreate := []types.UserCreate{
|
||||
UserFactory(),
|
||||
UserFactory(),
|
||||
UserFactory(),
|
||||
UserFactory(),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
created := []types.UserOut{}
|
||||
|
||||
for _, usr := range toCreate {
|
||||
usrOut, _ := testRepos.Users.Create(ctx, usr)
|
||||
created = append(created, usrOut)
|
||||
}
|
||||
|
||||
// Validate
|
||||
allUsers, err := testRepos.Users.GetAll(ctx)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(created), len(allUsers))
|
||||
|
||||
for _, usr := range created {
|
||||
fmt.Printf("%+v\n", usr)
|
||||
assert.Contains(t, allUsers, usr)
|
||||
}
|
||||
|
||||
for _, usr := range created {
|
||||
testRepos.Users.Delete(ctx, usr.ID)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
testRepos.Users.DeleteAll(ctx)
|
||||
}
|
||||
|
||||
func Test_EntUserRepo_Update(t *testing.T) {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
func Test_EntUserRepo_Delete(t *testing.T) {
|
||||
// Create 10 Users
|
||||
for i := 0; i < 10; i++ {
|
||||
user := UserFactory()
|
||||
ctx := context.Background()
|
||||
_, _ = testRepos.Users.Create(ctx, user)
|
||||
}
|
||||
|
||||
// Delete all
|
||||
ctx := context.Background()
|
||||
allUsers, _ := testRepos.Users.GetAll(ctx)
|
||||
|
||||
assert.Greater(t, len(allUsers), 0)
|
||||
testRepos.Users.DeleteAll(ctx)
|
||||
|
||||
allUsers, _ = testRepos.Users.GetAll(ctx)
|
||||
assert.Equal(t, len(allUsers), 0)
|
||||
|
||||
}
|
||||
|
||||
func Test_EntUserRepo_GetSuperusers(t *testing.T) {
|
||||
// Create 10 Users
|
||||
superuser := 0
|
||||
users := 0
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
user := UserFactory()
|
||||
ctx := context.Background()
|
||||
_, _ = testRepos.Users.Create(ctx, user)
|
||||
|
||||
if user.IsSuperuser {
|
||||
superuser++
|
||||
} else {
|
||||
users++
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all
|
||||
ctx := context.Background()
|
||||
|
||||
superUsers, err := testRepos.Users.GetSuperusers(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, usr := range superUsers {
|
||||
assert.True(t, usr.IsSuperuser)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
testRepos.Users.DeleteAll(ctx)
|
||||
}
|
27
backend/internal/repo/users_interface.go
Normal file
27
backend/internal/repo/users_interface.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
// GetOneId returns a user by id
|
||||
GetOneId(ctx context.Context, ID uuid.UUID) (types.UserOut, error)
|
||||
// GetOneEmail returns a user by email
|
||||
GetOneEmail(ctx context.Context, email string) (types.UserOut, error)
|
||||
// GetAll returns all users
|
||||
GetAll(ctx context.Context) ([]types.UserOut, error)
|
||||
// Get Super Users
|
||||
GetSuperusers(ctx context.Context) ([]types.UserOut, error)
|
||||
// Create creates a new user
|
||||
Create(ctx context.Context, user types.UserCreate) (types.UserOut, error)
|
||||
// Update updates a user
|
||||
Update(ctx context.Context, ID uuid.UUID, user types.UserUpdate) error
|
||||
// Delete deletes a user
|
||||
Delete(ctx context.Context, ID uuid.UUID) error
|
||||
|
||||
DeleteAll(ctx context.Context) error
|
||||
}
|
15
backend/internal/services/all.go
Normal file
15
backend/internal/services/all.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package services
|
||||
|
||||
import "github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
|
||||
type AllServices struct {
|
||||
User *UserService
|
||||
Admin *AdminService
|
||||
}
|
||||
|
||||
func NewServices(repos *repo.AllRepos) *AllServices {
|
||||
return &AllServices{
|
||||
User: &UserService{repos},
|
||||
Admin: &AdminService{repos},
|
||||
}
|
||||
}
|
40
backend/internal/services/contexts.go
Normal file
40
backend/internal/services/contexts.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
)
|
||||
|
||||
type contextKeys struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var (
|
||||
ContextUser = &contextKeys{name: "User"}
|
||||
ContextUserToken = &contextKeys{name: "UserToken"}
|
||||
)
|
||||
|
||||
// SetUserCtx is a helper function that sets the ContextUser and ContextUserToken
|
||||
// values within the context of a web request (or any context).
|
||||
func SetUserCtx(ctx context.Context, user *types.UserOut, token string) context.Context {
|
||||
ctx = context.WithValue(ctx, ContextUser, user)
|
||||
ctx = context.WithValue(ctx, ContextUserToken, token)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// UseUserCtx is a helper function that returns the user from the context.
|
||||
func UseUserCtx(ctx context.Context) *types.UserOut {
|
||||
if val := ctx.Value(ContextUser); val != nil {
|
||||
return val.(*types.UserOut)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UseTokenCtx is a helper function that returns the user token from the context.
|
||||
func UseTokenCtx(ctx context.Context) string {
|
||||
if val := ctx.Value(ContextUserToken); val != nil {
|
||||
return val.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
39
backend/internal/services/contexts_test.go
Normal file
39
backend/internal/services/contexts_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_SetAuthContext(t *testing.T) {
|
||||
user := &types.UserOut{
|
||||
ID: uuid.New(),
|
||||
}
|
||||
|
||||
token := uuid.New().String()
|
||||
|
||||
ctx := SetUserCtx(context.Background(), user, token)
|
||||
|
||||
ctxUser := UseUserCtx(ctx)
|
||||
|
||||
assert.NotNil(t, ctxUser)
|
||||
assert.Equal(t, user.ID, ctxUser.ID)
|
||||
|
||||
ctxUserToken := UseTokenCtx(ctx)
|
||||
assert.NotEmpty(t, ctxUserToken)
|
||||
}
|
||||
|
||||
func Test_SetAuthContext_Nulls(t *testing.T) {
|
||||
ctx := SetUserCtx(context.Background(), nil, "")
|
||||
|
||||
ctxUser := UseUserCtx(ctx)
|
||||
|
||||
assert.Nil(t, ctxUser)
|
||||
|
||||
ctxUserToken := UseTokenCtx(ctx)
|
||||
assert.Empty(t, ctxUserToken)
|
||||
}
|
47
backend/internal/services/service_admin.go
Normal file
47
backend/internal/services/service_admin.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
)
|
||||
|
||||
type AdminService struct {
|
||||
repos *repo.AllRepos
|
||||
}
|
||||
|
||||
func (svc *AdminService) Create(ctx context.Context, usr types.UserCreate) (types.UserOut, error) {
|
||||
return svc.repos.Users.Create(ctx, usr)
|
||||
}
|
||||
|
||||
func (svc *AdminService) GetAll(ctx context.Context) ([]types.UserOut, error) {
|
||||
return svc.repos.Users.GetAll(ctx)
|
||||
}
|
||||
|
||||
func (svc *AdminService) GetByID(ctx context.Context, id uuid.UUID) (types.UserOut, error) {
|
||||
return svc.repos.Users.GetOneId(ctx, id)
|
||||
}
|
||||
|
||||
func (svc *AdminService) GetByEmail(ctx context.Context, email string) (types.UserOut, error) {
|
||||
return svc.repos.Users.GetOneEmail(ctx, email)
|
||||
}
|
||||
|
||||
func (svc *AdminService) UpdateProperties(ctx context.Context, ID uuid.UUID, data types.UserUpdate) (types.UserOut, error) {
|
||||
err := svc.repos.Users.Update(ctx, ID, data)
|
||||
|
||||
if err != nil {
|
||||
return types.UserOut{}, err
|
||||
}
|
||||
|
||||
return svc.repos.Users.GetOneId(ctx, ID)
|
||||
}
|
||||
|
||||
func (svc *AdminService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return svc.repos.Users.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (svc *AdminService) DeleteAll(ctx context.Context) error {
|
||||
return svc.repos.Users.DeleteAll(ctx)
|
||||
}
|
84
backend/internal/services/service_user.go
Normal file
84
backend/internal/services/service_user.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/hasher"
|
||||
)
|
||||
|
||||
var (
|
||||
oneWeek = time.Hour * 24 * 7
|
||||
ErrorInvalidLogin = errors.New("invalid username or password")
|
||||
ErrorInvalidToken = errors.New("invalid token")
|
||||
ErrorTokenIdMismatch = errors.New("token id mismatch")
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
repos *repo.AllRepos
|
||||
}
|
||||
|
||||
// GetSelf returns the user that is currently logged in based of the token provided within
|
||||
func (svc *UserService) GetSelf(ctx context.Context, requestToken string) (types.UserOut, error) {
|
||||
hash := hasher.HashToken(requestToken)
|
||||
return svc.repos.AuthTokens.GetUserFromToken(ctx, hash)
|
||||
}
|
||||
|
||||
func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data types.UserUpdate) (types.UserOut, error) {
|
||||
err := svc.repos.Users.Update(ctx, ID, data)
|
||||
|
||||
if err != nil {
|
||||
return types.UserOut{}, err
|
||||
}
|
||||
|
||||
return svc.repos.Users.GetOneId(ctx, ID)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// User Authentication
|
||||
|
||||
func (svc *UserService) createToken(ctx context.Context, userId uuid.UUID) (types.UserAuthTokenDetail, error) {
|
||||
newToken := hasher.GenerateToken()
|
||||
|
||||
created, err := svc.repos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
|
||||
UserID: userId,
|
||||
TokenHash: newToken.Hash,
|
||||
ExpiresAt: time.Now().Add(oneWeek),
|
||||
})
|
||||
|
||||
return types.UserAuthTokenDetail{Raw: newToken.Raw, ExpiresAt: created.ExpiresAt}, err
|
||||
}
|
||||
|
||||
func (svc *UserService) Login(ctx context.Context, username, password string) (types.UserAuthTokenDetail, error) {
|
||||
usr, err := svc.repos.Users.GetOneEmail(ctx, username)
|
||||
|
||||
if err != nil || !hasher.CheckPasswordHash(password, usr.Password) {
|
||||
return types.UserAuthTokenDetail{}, ErrorInvalidLogin
|
||||
}
|
||||
|
||||
return svc.createToken(ctx, usr.ID)
|
||||
}
|
||||
|
||||
func (svc *UserService) Logout(ctx context.Context, token string) error {
|
||||
hash := hasher.HashToken(token)
|
||||
err := svc.repos.AuthTokens.DeleteToken(ctx, hash)
|
||||
return err
|
||||
}
|
||||
|
||||
func (svc *UserService) RenewToken(ctx context.Context, token string) (types.UserAuthTokenDetail, error) {
|
||||
hash := hasher.HashToken(token)
|
||||
|
||||
dbToken, err := svc.repos.AuthTokens.GetUserFromToken(ctx, hash)
|
||||
|
||||
if err != nil {
|
||||
return types.UserAuthTokenDetail{}, ErrorInvalidToken
|
||||
}
|
||||
|
||||
newToken, _ := svc.createToken(ctx, dbToken.ID)
|
||||
|
||||
return newToken, nil
|
||||
}
|
11
backend/internal/types/about_types.go
Normal file
11
backend/internal/types/about_types.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package types
|
||||
|
||||
// ApiSummary
|
||||
//
|
||||
// @public
|
||||
type ApiSummary struct {
|
||||
Healthy bool `json:"health"`
|
||||
Versions []string `json:"versions"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}
|
39
backend/internal/types/token_types.go
Normal file
39
backend/internal/types/token_types.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LoginForm struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
BearerToken string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
|
||||
type UserAuthTokenDetail struct {
|
||||
Raw string `json:"raw"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
|
||||
type UserAuthToken struct {
|
||||
TokenHash []byte `json:"token"`
|
||||
UserID uuid.UUID `json:"userId"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (u UserAuthToken) IsExpired() bool {
|
||||
return u.ExpiresAt.Before(time.Now())
|
||||
}
|
||||
|
||||
type UserAuthTokenCreate struct {
|
||||
TokenHash []byte `json:"token"`
|
||||
UserID uuid.UUID `json:"userId"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
58
backend/internal/types/users_types.go
Normal file
58
backend/internal/types/users_types.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNameEmpty = errors.New("name is empty")
|
||||
ErrEmailEmpty = errors.New("email is empty")
|
||||
)
|
||||
|
||||
// UserIn is a basic user input struct containing only the fields that are
|
||||
// required for user creation.
|
||||
type UserIn struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// UserCreate is the Data object contain the requirements of creating a user
|
||||
// in the database. It should to create users from an API unless the user has
|
||||
// rights to create SuperUsers. For regular user in data use the UserIn struct.
|
||||
type UserCreate struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
IsSuperuser bool `json:"isSuperuser"`
|
||||
}
|
||||
|
||||
func (u *UserCreate) Validate() error {
|
||||
if u.Name == "" {
|
||||
return ErrNameEmpty
|
||||
}
|
||||
if u.Email == "" {
|
||||
return ErrEmailEmpty
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserOut struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"-"`
|
||||
IsSuperuser bool `json:"isSuperuser"`
|
||||
}
|
||||
|
||||
// IsNull is a proxy call for `usr.Id == uuid.Nil`
|
||||
func (usr *UserOut) IsNull() bool {
|
||||
return usr.ID == uuid.Nil
|
||||
}
|
||||
|
||||
type UserUpdate struct {
|
||||
Name *string `json:"name"`
|
||||
Email *string `json:"email"`
|
||||
}
|
76
backend/internal/types/users_types_test.go
Normal file
76
backend/internal/types/users_types_test.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserCreate_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
Name string
|
||||
Email string
|
||||
Password string
|
||||
IsSuperuser bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no_name",
|
||||
fields: fields{
|
||||
Name: "",
|
||||
Email: "",
|
||||
Password: "",
|
||||
IsSuperuser: false,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no_email",
|
||||
fields: fields{
|
||||
Name: "test",
|
||||
Email: "",
|
||||
Password: "",
|
||||
IsSuperuser: false,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
fields: fields{
|
||||
Name: "test",
|
||||
Email: "test@email.com",
|
||||
Password: "mypassword",
|
||||
IsSuperuser: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
u := &UserCreate{
|
||||
Name: tt.fields.Name,
|
||||
Email: tt.fields.Email,
|
||||
Password: tt.fields.Password,
|
||||
IsSuperuser: tt.fields.IsSuperuser,
|
||||
}
|
||||
if err := u.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("UserCreate.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserOut_IsNull(t *testing.T) {
|
||||
nullUser := UserOut{}
|
||||
|
||||
assert.True(t, nullUser.IsNull())
|
||||
|
||||
nullUser.ID = uuid.New()
|
||||
|
||||
assert.False(t, nullUser.IsNull())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue