Skip to content

Commit 725c75e

Browse files
committed
feat: add a universal user struct with generic data type
1 parent b721570 commit 725c75e

File tree

2 files changed

+188
-1
lines changed

2 files changed

+188
-1
lines changed

example/main.go

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
)
1616

1717
func main() {
18-
1918
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
2019
defer cancel()
2120

@@ -29,6 +28,9 @@ func main() {
2928
// Example: Firebase Authentication
3029
firebaseExample(ctx, auth)
3130

31+
// Example: BaseUser model
32+
userModelExample(ctx, auth)
33+
3234
// Example: Cloudflare Storage
3335
cloudflareExample(ctx)
3436

@@ -83,6 +85,83 @@ func firebaseExample(ctx context.Context, auth *firebase.FirebaseAuth) {
8385
}
8486
}
8587

88+
func userModelExample(ctx context.Context, auth *firebase.FirebaseAuth) {
89+
fmt.Println("\n=== BaseUser Model Example ===")
90+
91+
// Define an application-specific user data type
92+
type AppUserData struct {
93+
Preferences map[string]string `json:"preferences"`
94+
LastActive time.Time `json:"lastActive"`
95+
ProfileComplete bool `json:"profileComplete"`
96+
}
97+
98+
// Create a test user
99+
userParams := (&officialFirebaseAuth.UserToCreate{}).
100+
101+
Password("password123").
102+
DisplayName("Base User Test")
103+
104+
uid, err := auth.CreateUser(ctx, userParams)
105+
if err != nil {
106+
log.Printf("Failed to create user: %v", err)
107+
return
108+
}
109+
log.Printf("Created user with Firebase UID: %s", uid)
110+
111+
// Set some custom claims
112+
claims := map[string]interface{}{
113+
"admin": true,
114+
"roles": []interface{}{"editor", "viewer"},
115+
"level": 5,
116+
}
117+
118+
if err := auth.SetCustomClaims(ctx, uid, claims); err != nil {
119+
log.Printf("Failed to set custom claims: %v", err)
120+
}
121+
122+
// Get the Firebase user
123+
fbUser, err := auth.GetUserByUID(ctx, uid)
124+
if err != nil {
125+
log.Printf("Failed to get user: %v", err)
126+
return
127+
}
128+
129+
// Create app-specific user data
130+
userData := AppUserData{
131+
Preferences: map[string]string{
132+
"theme": "dark",
133+
"language": "en",
134+
},
135+
LastActive: time.Now(),
136+
ProfileComplete: false,
137+
}
138+
139+
// Convert to our BaseUser model
140+
baseUser := firebase.FromFirebaseUser[AppUserData](fbUser, userData)
141+
142+
// Display the user model
143+
log.Printf("BaseUser created with ID: %s (different from Firebase UID: %s)",
144+
baseUser.ID, baseUser.FirebaseUID)
145+
146+
// Demonstrate claims/roles helpers
147+
isAdmin, _ := baseUser.GetClaim("admin")
148+
log.Printf("User is admin: %v", isAdmin)
149+
150+
log.Printf("User has editor role: %v", baseUser.HasRole("editor"))
151+
log.Printf("User has admin role: %v", baseUser.HasRole("admin"))
152+
153+
// Show app-specific data
154+
log.Printf("User preferences: %v", baseUser.Data.Preferences)
155+
log.Printf("Profile complete: %v", baseUser.Data.ProfileComplete)
156+
157+
// Clean up - delete user
158+
if err := auth.DeleteUser(ctx, uid); err != nil {
159+
log.Printf("Failed to delete user: %v", err)
160+
} else {
161+
log.Printf("Deleted user: %s", uid)
162+
}
163+
}
164+
86165
func cloudflareExample(ctx context.Context) {
87166
fmt.Println("\n=== Cloudflare Storage Example ===")
88167

firebase/user.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package firebase
2+
3+
import (
4+
"time"
5+
6+
"firebase.google.com/go/v4/auth"
7+
"github.com/google/uuid"
8+
)
9+
10+
// BaseUser contains universal user properties across any application
11+
type BaseUser[T any] struct {
12+
// Core identifiers
13+
ID string `json:"id"` // Database primary key/ID
14+
FirebaseUID string `json:"firebaseUid"` // Firebase authentication UID
15+
Email string `json:"email"` // User's email address
16+
17+
// Common user metadata
18+
DisplayName string `json:"displayName,omitempty"`
19+
PhotoURL string `json:"photoUrl,omitempty"`
20+
CreatedAt time.Time `json:"createdAt"`
21+
UpdatedAt time.Time `json:"updatedAt"`
22+
LastLoginAt *time.Time `json:"lastLoginAt,omitempty"`
23+
Disabled bool `json:"disabled"`
24+
25+
// Authentication properties
26+
EmailVerified bool `json:"emailVerified"`
27+
Claims map[string]interface{} `json:"claims,omitempty"`
28+
29+
// Application-specific user data
30+
Data T `json:"data"`
31+
}
32+
33+
// FromFirebaseUser converts a Firebase UserRecord to a BaseUser
34+
func FromFirebaseUser[T any](user *auth.UserRecord, data T) *BaseUser[T] {
35+
lastLogin := user.UserMetadata.LastLogInTimestamp
36+
var lastLoginTime *time.Time
37+
38+
if lastLogin > 0 {
39+
t := time.Unix(lastLogin/1000, 0)
40+
lastLoginTime = &t
41+
}
42+
43+
createdAt := time.Unix(user.UserMetadata.CreationTimestamp/1000, 0)
44+
45+
return &BaseUser[T]{
46+
ID: uuid.New().String(),
47+
FirebaseUID: user.UID,
48+
Email: user.Email,
49+
DisplayName: user.DisplayName,
50+
PhotoURL: user.PhotoURL,
51+
CreatedAt: createdAt,
52+
UpdatedAt: createdAt,
53+
LastLoginAt: lastLoginTime,
54+
Disabled: user.Disabled,
55+
EmailVerified: user.EmailVerified,
56+
Claims: user.CustomClaims,
57+
Data: data,
58+
}
59+
}
60+
61+
// ToFirebaseUpdate converts a BaseUser to auth.UserToUpdate for Firebase updates
62+
func (u *BaseUser[T]) ToFirebaseUpdate() *auth.UserToUpdate {
63+
update := (&auth.UserToUpdate{}).
64+
Email(u.Email).
65+
DisplayName(u.DisplayName).
66+
PhotoURL(u.PhotoURL).
67+
EmailVerified(u.EmailVerified).
68+
Disabled(u.Disabled)
69+
70+
return update
71+
}
72+
73+
// GetClaim retrieves a specific claim value with type safety
74+
func (u *BaseUser[T]) GetClaim(key string) (interface{}, bool) {
75+
if u.Claims == nil {
76+
return nil, false
77+
}
78+
79+
val, ok := u.Claims[key]
80+
return val, ok
81+
}
82+
83+
// HasRole checks if a user has a specific role
84+
func (u *BaseUser[T]) HasRole(role string) bool {
85+
if u.Claims == nil {
86+
return false
87+
}
88+
89+
// Check roles array if it exists
90+
if roles, ok := u.Claims["roles"]; ok {
91+
if rolesArr, ok := roles.([]interface{}); ok {
92+
for _, r := range rolesArr {
93+
if r == role {
94+
return true
95+
}
96+
}
97+
}
98+
}
99+
100+
// Check direct role claim
101+
if val, ok := u.Claims[role]; ok {
102+
if boolVal, ok := val.(bool); ok {
103+
return boolVal
104+
}
105+
}
106+
107+
return false
108+
}

0 commit comments

Comments
 (0)