(Feat): Initial Commit
This commit is contained in:
10
internal/data/dto/push.go
Normal file
10
internal/data/dto/push.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package dto
|
||||
|
||||
import "github.com/aarondl/null/v8"
|
||||
|
||||
type UpdatePushTokenRequest struct {
|
||||
User User
|
||||
Token string
|
||||
Provider string
|
||||
ExistingToken null.String
|
||||
}
|
||||
157
internal/data/dto/users.go
Normal file
157
internal/data/dto/users.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"github.com/aarondl/null/v8"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/strfmt/conv"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
Username null.String
|
||||
PasswordHash null.String
|
||||
IsActive bool
|
||||
Scopes []string
|
||||
LastAuthenticatedAt null.Time
|
||||
UpdatedAt time.Time
|
||||
Profile *AppUserProfile
|
||||
}
|
||||
|
||||
func (u User) LastUpdatedAt() time.Time {
|
||||
if u.Profile != nil && u.Profile.UpdatedAt.After(u.UpdatedAt) {
|
||||
return u.Profile.UpdatedAt
|
||||
}
|
||||
|
||||
return u.UpdatedAt
|
||||
}
|
||||
|
||||
func (u User) Ptr() *User {
|
||||
return &u
|
||||
}
|
||||
|
||||
func (u User) ToTypes() *types.GetUserInfoResponse {
|
||||
return &types.GetUserInfoResponse{
|
||||
Sub: swag.String(u.ID),
|
||||
UpdatedAt: swag.Int64(u.LastUpdatedAt().Unix()),
|
||||
Email: strfmt.Email(u.Username.String),
|
||||
Scopes: u.Scopes,
|
||||
}
|
||||
}
|
||||
|
||||
func (u User) ToModels() *models.User {
|
||||
return &models.User{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Password: u.PasswordHash,
|
||||
IsActive: u.IsActive,
|
||||
Scopes: u.Scopes,
|
||||
LastAuthenticatedAt: u.LastAuthenticatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
type AppUserProfile struct {
|
||||
UserID string
|
||||
LegalAcceptedAt null.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (aup AppUserProfile) Ptr() *AppUserProfile {
|
||||
return &aup
|
||||
}
|
||||
|
||||
type UpdatePasswordRequest struct {
|
||||
User User
|
||||
CurrentPassword string
|
||||
SkipCurrentPasswordVerification bool
|
||||
NewPassword string
|
||||
}
|
||||
|
||||
type RegisterResult struct {
|
||||
RequiresConfirmation bool
|
||||
ConfirmationToken null.String
|
||||
}
|
||||
|
||||
type LoginResult struct {
|
||||
UserID string
|
||||
AccessToken string
|
||||
ExpiresIn int64
|
||||
RefreshToken string
|
||||
TokenType string
|
||||
}
|
||||
|
||||
func (l LoginResult) ToTypes() *types.PostLoginResponse {
|
||||
return &types.PostLoginResponse{
|
||||
AccessToken: conv.UUID4(strfmt.UUID4(l.AccessToken)),
|
||||
RefreshToken: conv.UUID4(strfmt.UUID4(l.RefreshToken)),
|
||||
ExpiresIn: swag.Int64(l.ExpiresIn),
|
||||
TokenType: swag.String(l.TokenType),
|
||||
}
|
||||
}
|
||||
|
||||
type ResetPasswordRequest struct {
|
||||
ResetToken string
|
||||
NewPassword string
|
||||
}
|
||||
|
||||
type Username struct {
|
||||
val string
|
||||
}
|
||||
|
||||
func NewUsername(val string) Username {
|
||||
return Username{val: val}
|
||||
}
|
||||
|
||||
func (u Username) String() string {
|
||||
return util.ToUsernameFormat(u.val)
|
||||
}
|
||||
|
||||
type InitPasswordResetRequest struct {
|
||||
Username Username
|
||||
}
|
||||
|
||||
type InitPasswordResetResult struct {
|
||||
ResetToken null.String
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username Username
|
||||
Password string
|
||||
}
|
||||
|
||||
type LogoutRequest struct {
|
||||
AccessToken string
|
||||
RefreshToken null.String
|
||||
}
|
||||
|
||||
type AuthenticateUserRequest struct {
|
||||
User User
|
||||
InvalidateExistingTokens bool
|
||||
}
|
||||
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username Username
|
||||
Password string
|
||||
}
|
||||
|
||||
type CompleteRegisterRequest struct {
|
||||
ConfirmationToken string
|
||||
}
|
||||
|
||||
type DeleteUserAccountRequest struct {
|
||||
User User
|
||||
CurrentPassword string
|
||||
}
|
||||
|
||||
type ConfirmatioNotificationPayload struct {
|
||||
ConfirmationLink string
|
||||
}
|
||||
36
internal/data/fixtures/fixtures.go
Normal file
36
internal/data/fixtures/fixtures.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package fixtures
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"github.com/aarondl/sqlboiler/v4/boil"
|
||||
)
|
||||
|
||||
// Live Service fixtures to be applied by manually running the CLI "app db seed"
|
||||
// Note that these fixtures are not available while testing
|
||||
// see the separate internal/test/fixtures.go file
|
||||
|
||||
type Upsertable interface {
|
||||
Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns, opts ...models.UpsertOptionFunc) error
|
||||
}
|
||||
|
||||
// Mind the declaration order! The fields get upserted exactly in the order they are declared.
|
||||
type FixtureMap struct{}
|
||||
|
||||
func Fixtures() FixtureMap {
|
||||
return FixtureMap{}
|
||||
}
|
||||
|
||||
func Upserts() []Upsertable {
|
||||
fix := Fixtures()
|
||||
upsertableIfc := (*Upsertable)(nil)
|
||||
upserts, err := util.GetFieldsImplementing(&fix, upsertableIfc)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get upsertable fixture fields: %w", err))
|
||||
}
|
||||
|
||||
return upserts
|
||||
}
|
||||
18
internal/data/fixtures/fixtures_test.go
Normal file
18
internal/data/fixtures/fixtures_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package fixtures_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
data "allaboutapps.dev/aw/go-starter/internal/data/fixtures"
|
||||
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUpsertableInterface(t *testing.T) {
|
||||
var user any = &models.AppUserProfile{
|
||||
UserID: "62b13d29-5c4e-420e-b991-a631d3938776",
|
||||
}
|
||||
|
||||
_, ok := user.(data.Upsertable)
|
||||
assert.True(t, ok, "AppUserProfile should implement the Upsertable interface")
|
||||
}
|
||||
22
internal/data/local/service.go
Normal file
22
internal/data/local/service.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"github.com/dropbox/godropbox/time2"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
config config.Server
|
||||
db *sql.DB
|
||||
clock time2.Clock
|
||||
}
|
||||
|
||||
func NewService(config config.Server, db *sql.DB, clock time2.Clock) *Service {
|
||||
return &Service{
|
||||
config: config,
|
||||
db: db,
|
||||
clock: clock,
|
||||
}
|
||||
}
|
||||
75
internal/data/local/service_push.go
Normal file
75
internal/data/local/service_push.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/db"
|
||||
"github.com/aarondl/sqlboiler/v4/boil"
|
||||
)
|
||||
|
||||
func (s *Service) UpdatePushToken(ctx context.Context, request dto.UpdatePushTokenRequest) error {
|
||||
log := util.LogFromContext(ctx).With().Str("userID", request.User.ID).Logger()
|
||||
|
||||
err := db.WithTransaction(ctx, s.db, func(exec boil.ContextExecutor) error {
|
||||
tokenExists, err := models.PushTokens(
|
||||
models.PushTokenWhere.Token.EQ(request.Token),
|
||||
).Exists(ctx, exec)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to check if token exists")
|
||||
return err
|
||||
}
|
||||
|
||||
if tokenExists {
|
||||
log.Debug().Msg("Token already exists")
|
||||
return httperrors.ErrConflictPushToken
|
||||
}
|
||||
|
||||
newToken := models.PushToken{
|
||||
UserID: request.User.ID,
|
||||
Token: request.Token,
|
||||
Provider: request.Provider,
|
||||
}
|
||||
|
||||
if err := newToken.Insert(ctx, s.db, boil.Infer()); err != nil {
|
||||
log.Err(err).Msg("Failed to insert new token")
|
||||
return err
|
||||
}
|
||||
|
||||
if request.ExistingToken.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
existingToken, err := models.PushTokens(
|
||||
models.PushTokenWhere.Token.EQ(request.ExistingToken.String),
|
||||
models.PushTokenWhere.UserID.EQ(request.User.ID),
|
||||
).One(ctx, exec)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
log.Debug().Msg("Existing token not found")
|
||||
return httperrors.ErrNotFoundOldPushToken
|
||||
}
|
||||
|
||||
log.Err(err).Msg("Failed to find existing token")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := existingToken.Delete(ctx, exec); err != nil {
|
||||
log.Err(err).Msg("Failed to delete existing token")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("Failed to update push token")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
32
internal/data/mapper/users.go
Normal file
32
internal/data/mapper/users.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"allaboutapps.dev/aw/go-starter/internal/data/dto"
|
||||
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||
)
|
||||
|
||||
func LocalAppUserProfileToDTO(appUserProfile *models.AppUserProfile) dto.AppUserProfile {
|
||||
return dto.AppUserProfile{
|
||||
UserID: appUserProfile.UserID,
|
||||
LegalAcceptedAt: appUserProfile.LegalAcceptedAt,
|
||||
UpdatedAt: appUserProfile.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func LocalUserToDTO(user *models.User) dto.User {
|
||||
result := dto.User{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
IsActive: user.IsActive,
|
||||
Scopes: user.Scopes,
|
||||
LastAuthenticatedAt: user.LastAuthenticatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
PasswordHash: user.Password,
|
||||
}
|
||||
|
||||
if user.R != nil && user.R.AppUserProfile != nil {
|
||||
result.Profile = LocalAppUserProfileToDTO(user.R.AppUserProfile).Ptr()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user