feat: add guest mode, bug fixes, and self-hosted config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2
backend/.gitignore
vendored
2
backend/.gitignore
vendored
@@ -4,7 +4,7 @@
|
||||
.env.*.local
|
||||
|
||||
# Compiled binaries
|
||||
server
|
||||
/server
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
|
||||
@@ -204,6 +204,7 @@ func main() {
|
||||
authGroup.GET("/google/callback", authHandler.GoogleCallback)
|
||||
authGroup.GET("/github", authHandler.GithubLogin)
|
||||
authGroup.GET("/github/callback", authHandler.GithubCallback)
|
||||
authGroup.POST("/guest", authHandler.GuestLogin)
|
||||
authGroup.GET("/me", authMiddleware.RequireAuth(), authHandler.Me)
|
||||
authGroup.POST("/logout", authMiddleware.RequireAuth(), authHandler.Logout)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -68,7 +68,7 @@ func (h *AuthHandler) GoogleCallback(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid oauth state"})
|
||||
return
|
||||
}
|
||||
log.Println("Google callback state:", c.Query("state"))
|
||||
|
||||
// Exchange code for token
|
||||
token, err := h.googleConfig.Exchange(c.Request.Context(), c.Query("code"))
|
||||
if err != nil {
|
||||
@@ -83,8 +83,7 @@ func (h *AuthHandler) GoogleCallback(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
|
||||
return
|
||||
}
|
||||
log.Println("Google user info response status:", resp.Status)
|
||||
log.Println("Google user info response headers:", resp.Header)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
@@ -96,11 +95,11 @@ func (h *AuthHandler) GoogleCallback(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &userInfo); err != nil {
|
||||
log.Printf("Failed to parse Google response: %v | Data: %s", err, string(data))
|
||||
// Failed to parse Google response
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid Google response"})
|
||||
return
|
||||
}
|
||||
log.Println("Google user info:", userInfo)
|
||||
|
||||
// Upsert user in database
|
||||
user, err := h.store.UpsertUser(
|
||||
c.Request.Context(),
|
||||
@@ -116,12 +115,9 @@ func (h *AuthHandler) GoogleCallback(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create session and JWT
|
||||
jwt, err := h.createSessionAndJWT(c, user)
|
||||
jwt, err := h.createSessionAndJWT(c, user, 7*24*time.Hour)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ DATABASE ERROR: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": fmt.Sprintf("CreateSession Error: %v", err),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,7 +140,7 @@ func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid oauth state"})
|
||||
return
|
||||
}
|
||||
log.Println("Github callback state:", c.Query("state"))
|
||||
|
||||
code := c.Query("code")
|
||||
if code == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No code provided"})
|
||||
@@ -178,7 +174,7 @@ func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &userInfo); err != nil {
|
||||
log.Printf("Failed to parse GitHub response: %v | Data: %s", err, string(data))
|
||||
// Failed to parse GitHub response
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid GitHub response"})
|
||||
return
|
||||
}
|
||||
@@ -207,8 +203,7 @@ func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
||||
if userInfo.Name == "" {
|
||||
userInfo.Name = userInfo.Login
|
||||
}
|
||||
fmt.Println("Getting user info : ")
|
||||
fmt.Println(userInfo)
|
||||
|
||||
// Upsert user in database
|
||||
user, err := h.store.UpsertUser(
|
||||
c.Request.Context(),
|
||||
@@ -224,7 +219,7 @@ func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create session and JWT
|
||||
jwt, err := h.createSessionAndJWT(c, user)
|
||||
jwt, err := h.createSessionAndJWT(c, user, 7*24*time.Hour)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
|
||||
return
|
||||
@@ -273,12 +268,47 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
|
||||
}
|
||||
|
||||
// GuestLogin creates a temporary guest user and returns a JWT
|
||||
func (h *AuthHandler) GuestLogin(c *gin.Context) {
|
||||
// Generate random 4-byte hex string for guest ID
|
||||
b := make([]byte, 4)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate guest ID"})
|
||||
return
|
||||
}
|
||||
guestHex := fmt.Sprintf("%x", b)
|
||||
guestName := fmt.Sprintf("Guest-%s", guestHex)
|
||||
guestEmail := fmt.Sprintf("guest-%s@guest.local", guestHex)
|
||||
providerUserID := uuid.New().String()
|
||||
|
||||
user, err := h.store.UpsertUser(
|
||||
c.Request.Context(),
|
||||
"guest",
|
||||
providerUserID,
|
||||
guestEmail,
|
||||
guestName,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create guest user"})
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := h.createSessionAndJWT(c, user, 24*time.Hour)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"token": jwt})
|
||||
}
|
||||
|
||||
// Helper: create session and JWT
|
||||
func (h *AuthHandler) createSessionAndJWT(c *gin.Context, user *models.User) (string, error) {
|
||||
expiresAt := time.Now().Add(7 * 24 * time.Hour) // 7 days
|
||||
func (h *AuthHandler) createSessionAndJWT(c *gin.Context, user *models.User, expiry time.Duration) (string, error) {
|
||||
expiresAt := time.Now().Add(expiry)
|
||||
|
||||
// Generate JWT first (we need it for session) - now includes avatar URL
|
||||
jwt, err := auth.GenerateJWT(user.ID, user.Name, user.Email, user.AvatarURL, h.cfg.JWTSecret, 7*24*time.Hour)
|
||||
jwt, err := auth.GenerateJWT(user.ID, user.Name, user.Email, user.AvatarURL, h.cfg.JWTSecret, expiry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -306,7 +336,7 @@ func (h *AuthHandler) generateStateOauthCookie(w http.ResponseWriter) string {
|
||||
b := make([]byte, 16)
|
||||
n, err := rand.Read(b)
|
||||
if err != nil || n != 16 {
|
||||
fmt.Printf("Failed to generate random state: %v\n", err)
|
||||
// Failed to generate random state
|
||||
return "" // Critical for CSRF security
|
||||
}
|
||||
state := base64.URLEncoding.EncodeToString(b)
|
||||
|
||||
@@ -108,7 +108,7 @@ func (wsh *WebSocketHandler) HandleWebSocket(c *gin.Context) {
|
||||
// Validate share token
|
||||
valid, err := wsh.store.ValidateShareToken(c.Request.Context(), documentID, shareToken)
|
||||
if err != nil {
|
||||
log.Printf("Error validating share token: %v", err)
|
||||
// Error validating share token
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate share token"})
|
||||
return
|
||||
}
|
||||
@@ -134,7 +134,7 @@ func (wsh *WebSocketHandler) HandleWebSocket(c *gin.Context) {
|
||||
// Authenticated user - get their permission level
|
||||
perm, err := wsh.store.GetUserPermission(c.Request.Context(), documentID, *userID)
|
||||
if err != nil {
|
||||
log.Printf("Error getting user permission: %v", err)
|
||||
// Error getting user permission
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check permissions"})
|
||||
return
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func (wsh *WebSocketHandler) HandleWebSocket(c *gin.Context) {
|
||||
// Share token user - get share link permission
|
||||
perm, err := wsh.store.GetShareLinkPermission(c.Request.Context(), documentID)
|
||||
if err != nil {
|
||||
log.Printf("Error getting share link permission: %v", err)
|
||||
// Error getting share link permission
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check permissions"})
|
||||
return
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func (wsh *WebSocketHandler) HandleWebSocket(c *gin.Context) {
|
||||
upgrader := wsh.getUpgrader()
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
// Failed to upgrade WebSocket connection
|
||||
return
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (wsh *WebSocketHandler) HandleWebSocket(c *gin.Context) {
|
||||
go client.ReadPump()
|
||||
go wsh.replayBacklog(client, documentID)
|
||||
|
||||
log.Printf("Client connected: %s (user: %s) to room: %s", clientID, userName, roomID)
|
||||
// Client connected
|
||||
}
|
||||
|
||||
const maxReplayUpdates = 5000
|
||||
|
||||
@@ -99,7 +99,9 @@ func runUpdatePersistWorker(ctx context.Context, msgBus messagebus.MessageBus, d
|
||||
return
|
||||
}
|
||||
case <-heartbeatTicker.C:
|
||||
logWorker(logger, "Update persist worker heartbeat", zap.String("server_id", serverID))
|
||||
if logger != nil {
|
||||
logger.Debug("Update persist worker heartbeat", zap.String("server_id", serverID))
|
||||
}
|
||||
case <-ticker.C:
|
||||
if err := processUpdatePersistence(ctx, msgBus, dbStore, logger, serverID); err != nil {
|
||||
logWorker(logger, "Update persist worker tick failed", zap.Error(err))
|
||||
|
||||
3
backend/scripts/012_add_guest_provider.sql
Normal file
3
backend/scripts/012_add_guest_provider.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add 'guest' as a valid provider for guest mode login
|
||||
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_provider_check;
|
||||
ALTER TABLE users ADD CONSTRAINT users_provider_check CHECK (provider IN ('google', 'github', 'guest'));
|
||||
Reference in New Issue
Block a user