(Feat): Initial Commit
Some checks failed
Build & Test / build-test (push) Has been cancelled
Build & Test / swagger-codegen-cli (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled

This commit is contained in:
2026-07-03 19:41:31 +05:30
commit 7e940c83a7
461 changed files with 45002 additions and 0 deletions

10
internal/data/dto/push.go Normal file
View 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
View 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
}

View 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
}

View 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")
}

View 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,
}
}

View 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
}

View 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
}