feat: Improve OAuth handling and user data extraction in authentication flow
This commit is contained in:
307
README.md
307
README.md
@@ -1,241 +1,240 @@
|
|||||||
# Real-Time Collaboration Platform
|
# Real-Time Collaboration Platform
|
||||||
|
|
||||||
A full-stack real-time collaborative application supporting both text editing and Kanban boards, built with React and Go. The system uses Yjs CRDTs for conflict-free synchronization and WebSockets for real-time updates.
|
A full-stack real-time collaborative application supporting text editing and Kanban boards. Built with React, Go, Yjs CRDTs, and WebSockets.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Collaborative Text Editor**: Real-time document editing with TipTap editor
|
- **Collaborative Text Editor**: Real-time document editing with TipTap
|
||||||
- **Collaborative Kanban Boards**: Drag-and-drop task management
|
- **Collaborative Kanban Boards**: Drag-and-drop task management
|
||||||
- **Real-Time Synchronization**: Instant updates across all connected clients
|
- **User Authentication**: OAuth2 login (Google, GitHub)
|
||||||
|
- **Document Sharing**: Share documents with users or via public links
|
||||||
- **Offline Support**: IndexedDB persistence for offline editing
|
- **Offline Support**: IndexedDB persistence for offline editing
|
||||||
- **User Presence**: See who's currently editing with live cursors and awareness
|
- **User Presence**: Live cursors and user awareness
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
### Frontend
|
**Frontend:** React 19, TypeScript, Vite, TipTap, Yjs, y-websocket, y-indexeddb
|
||||||
- React 19 + TypeScript
|
**Backend:** Go 1.25, Gin, Gorilla WebSocket, PostgreSQL 16
|
||||||
- Vite (build tool)
|
**Infrastructure:** Docker Compose
|
||||||
- TipTap (collaborative rich text editor)
|
|
||||||
- Yjs (CRDT for conflict-free replication)
|
|
||||||
- y-websocket (WebSocket sync provider)
|
|
||||||
- y-indexeddb (offline persistence)
|
|
||||||
|
|
||||||
### Backend
|
## Quick Start
|
||||||
- Go 1.25
|
|
||||||
- Gin (web framework)
|
|
||||||
- Gorilla WebSocket
|
|
||||||
- PostgreSQL 16 (document storage)
|
|
||||||
- Redis 7 (future use)
|
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
- Docker Compose
|
|
||||||
- PostgreSQL
|
|
||||||
- Redis
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
- Node.js 18+, npm
|
||||||
- Node.js 18+ and npm
|
|
||||||
- Go 1.25+
|
- Go 1.25+
|
||||||
- Docker and Docker Compose
|
- Docker & Docker Compose
|
||||||
|
|
||||||
### Installation
|
### Setup
|
||||||
|
|
||||||
1. **Clone the repository**
|
1. **Clone and configure environment**
|
||||||
```bash
|
```bash
|
||||||
git clone <your-repo-url>
|
git clone <repo-url>
|
||||||
cd realtime-collab
|
cd realtime-collab
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set up environment variables**
|
# Setup environment files
|
||||||
|
|
||||||
Create backend environment file:
|
|
||||||
```bash
|
|
||||||
cp backend/.env.example backend/.env
|
|
||||||
# Edit backend/.env with your configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
Create frontend environment file:
|
|
||||||
```bash
|
|
||||||
cp frontend/.env.example frontend/.env
|
|
||||||
# Edit frontend/.env with your configuration (optional for local development)
|
|
||||||
```
|
|
||||||
|
|
||||||
Create root environment file for Docker:
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env with your Docker database credentials
|
cp backend/.env.example backend/.env
|
||||||
|
|
||||||
|
# Edit .env files with your configuration
|
||||||
|
# Minimum required: DATABASE_URL, JWT_SECRET
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start the infrastructure**
|
2. **Start infrastructure**
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start PostgreSQL and Redis containers.
|
3. **Run backend**
|
||||||
|
|
||||||
4. **Install and run the backend**
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
go mod download
|
go mod download
|
||||||
go run cmd/server/main.go
|
go run cmd/server/main.go
|
||||||
|
# Server runs on http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
The backend server will start on `http://localhost:8080`
|
4. **Run frontend**
|
||||||
|
|
||||||
5. **Install and run the frontend**
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
# App runs on http://localhost:5173
|
||||||
|
|
||||||
The frontend will start on `http://localhost:5173`
|
|
||||||
|
|
||||||
6. **Access the application**
|
|
||||||
|
|
||||||
Open your browser and navigate to `http://localhost:5173`
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
### Backend (backend/.env)
|
|
||||||
|
|
||||||
```env
|
|
||||||
PORT=8080
|
|
||||||
DATABASE_URL=postgres://user:password@localhost:5432/collaboration?sslmode=disable
|
|
||||||
REDIS_URL=redis://localhost:6379
|
|
||||||
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend (frontend/.env)
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_API_URL=http://localhost:8080/api
|
|
||||||
VITE_WS_URL=ws://localhost:8080/ws
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose (.env)
|
|
||||||
|
|
||||||
```env
|
|
||||||
POSTGRES_USER=collab
|
|
||||||
POSTGRES_PASSWORD=your_secure_password_here
|
|
||||||
POSTGRES_DB=collaboration
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
### System Overview
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||||
|
│ Browser │◄────────►│ Go Backend │◄────────►│ PostgreSQL │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ React + Yjs │ WS+HTTP │ Gin + Hub │ SQL │ Documents │
|
||||||
|
│ TipTap │ │ WebSocket │ │ Users │
|
||||||
|
│ IndexedDB │ │ REST API │ │ Sessions │
|
||||||
|
└─────────────┘ └──────────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
### Real-Time Collaboration Flow
|
### Real-Time Collaboration Flow
|
||||||
|
```
|
||||||
|
Client A Backend Hub Client B
|
||||||
|
│ │ │
|
||||||
|
│──1. Edit document──────────►│ │
|
||||||
|
│ (Yjs binary update) │ │
|
||||||
|
│ │──2. Broadcast update────►│
|
||||||
|
│ │ │
|
||||||
|
│ │◄─3. Edit document────────│
|
||||||
|
│◄─4. Broadcast update───────│ │
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
[Apply CRDT merge] [Relay only] [Apply CRDT merge]
|
||||||
|
```
|
||||||
|
|
||||||
1. Client connects to WebSocket endpoint `/ws/:documentId`
|
### Authentication Flow
|
||||||
2. WebSocket handler creates a client and registers it with the Hub
|
```
|
||||||
3. Hub manages rooms (one room per document)
|
User OAuth Provider Backend Database
|
||||||
4. Yjs generates binary updates on document changes
|
│ │ │ │
|
||||||
5. Client sends updates to WebSocket
|
│──1. Click login───────►│ │ │
|
||||||
6. Hub broadcasts updates to all clients in the same room
|
│◄─2. Auth page──────────│ │ │
|
||||||
7. Yjs applies updates and merges changes automatically (CRDT)
|
│──3. Approve───────────►│ │ │
|
||||||
8. IndexedDB persists state locally for offline support
|
│ │──4. Callback──────►│ │
|
||||||
|
│ │ │──5. Create user───►│
|
||||||
### API Endpoints
|
│ │ │◄──────────────────│
|
||||||
|
│◄─6. JWT token + cookie─┤◄──────────────────│ │
|
||||||
#### REST API
|
```
|
||||||
- `GET /api/documents` - List all documents
|
|
||||||
- `POST /api/documents` - Create a new document
|
|
||||||
- `GET /api/documents/:id` - Get document metadata
|
|
||||||
- `GET /api/documents/:id/state` - Get document Yjs state
|
|
||||||
- `PUT /api/documents/:id/state` - Update document Yjs state
|
|
||||||
- `DELETE /api/documents/:id` - Delete a document
|
|
||||||
|
|
||||||
#### WebSocket
|
|
||||||
- `GET /ws/:roomId` - WebSocket connection for real-time sync
|
|
||||||
|
|
||||||
#### Health Check
|
|
||||||
- `GET /health` - Server health status
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
realtime-collab/
|
realtime-collab/
|
||||||
├── backend/
|
├── backend/
|
||||||
│ ├── cmd/server/ # Application entry point
|
│ ├── cmd/server/ # Entry point
|
||||||
│ ├── internal/
|
│ ├── internal/
|
||||||
|
│ │ ├── auth/ # JWT & OAuth middleware
|
||||||
│ │ ├── handlers/ # HTTP/WebSocket handlers
|
│ │ ├── handlers/ # HTTP/WebSocket handlers
|
||||||
│ │ ├── hub/ # WebSocket hub (room management)
|
│ │ ├── hub/ # WebSocket hub & rooms
|
||||||
│ │ ├── models/ # Domain models
|
│ │ ├── models/ # Domain models
|
||||||
│ │ └── store/ # Database layer
|
│ │ └── store/ # PostgreSQL data layer
|
||||||
│ └── scripts/ # Database initialization scripts
|
│ └── scripts/init.sql # Database schema
|
||||||
├── frontend/
|
├── frontend/
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── api/ # REST API client
|
│ │ ├── api/ # REST API client
|
||||||
│ │ ├── components/ # React components
|
│ │ ├── components/ # React components
|
||||||
│ │ ├── lib/ # Yjs integration
|
│ │ ├── hooks/ # Custom hooks
|
||||||
│ │ ├── pages/ # Page components
|
│ │ ├── lib/yjs.ts # Yjs setup
|
||||||
│ │ └── hooks/ # Custom React hooks
|
│ │ └── pages/ # Page components
|
||||||
│ └── public/ # Static assets
|
└── docker-compose.yml # PostgreSQL & Redis
|
||||||
└── docker-compose.yml # Infrastructure setup
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Key Endpoints
|
||||||
|
|
||||||
### Backend Commands
|
### REST API
|
||||||
|
- `POST /api/auth/google` - Google OAuth login
|
||||||
|
- `POST /api/auth/github` - GitHub OAuth login
|
||||||
|
- `GET /api/auth/me` - Get current user
|
||||||
|
- `POST /api/auth/logout` - Logout
|
||||||
|
- `GET /api/documents` - List all documents
|
||||||
|
- `POST /api/documents` - Create document
|
||||||
|
- `GET /api/documents/:id` - Get document
|
||||||
|
- `PUT /api/documents/:id/state` - Update Yjs state
|
||||||
|
- `DELETE /api/documents/:id` - Delete document
|
||||||
|
- `POST /api/documents/:id/shares` - Share with user
|
||||||
|
- `POST /api/documents/:id/share-link` - Create public link
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
- `GET /ws/:roomId` - Real-time sync endpoint
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Backend (.env)
|
||||||
|
```bash
|
||||||
|
PORT=8080
|
||||||
|
DATABASE_URL=postgres://user:pass@localhost:5432/collaboration?sslmode=disable
|
||||||
|
JWT_SECRET=your-secret-key-here
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
|
||||||
|
|
||||||
|
# OAuth (optional)
|
||||||
|
GOOGLE_CLIENT_ID=your-google-client-id
|
||||||
|
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||||
|
GITHUB_CLIENT_ID=your-github-client-id
|
||||||
|
GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker (.env in root)
|
||||||
|
```bash
|
||||||
|
POSTGRES_USER=collab
|
||||||
|
POSTGRES_PASSWORD=your-secure-password
|
||||||
|
POSTGRES_DB=collaboration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Backend
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
go run cmd/server/main.go # Run development server
|
go run cmd/server/main.go # Run server
|
||||||
|
go test ./... # Run tests
|
||||||
|
go test -v ./internal/handlers # Run handler tests
|
||||||
go build -o server cmd/server/main.go # Build binary
|
go build -o server cmd/server/main.go # Build binary
|
||||||
go fmt ./... # Format code
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend Commands
|
### Frontend
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm run dev # Start dev server
|
npm run dev # Dev server
|
||||||
npm run build # Production build
|
npm run build # Production build
|
||||||
npm run preview # Preview production build
|
npm run lint # Lint code
|
||||||
npm run lint # Run ESLint
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
The database schema is automatically initialized on first run using `backend/scripts/init.sql`.
|
|
||||||
|
|
||||||
To reset the database:
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose down -v
|
docker-compose down -v # Reset database
|
||||||
docker-compose up -d
|
docker-compose up -d # Start fresh
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
### Conflict-Free Replication (CRDT)
|
### CRDT-Based Collaboration
|
||||||
|
- **Yjs** provides Conflict-free Replicated Data Types (CRDTs)
|
||||||
|
- Multiple users edit simultaneously without conflicts
|
||||||
|
- Changes merge automatically, no manual conflict resolution
|
||||||
|
- Works offline, syncs when reconnected
|
||||||
|
|
||||||
The application uses Yjs, a CRDT implementation, which allows:
|
### Backend as Message Broker
|
||||||
- Multiple users to edit simultaneously without conflicts
|
- Backend doesn't understand Yjs data structure
|
||||||
- Automatic merging of concurrent changes
|
- Simply broadcasts binary updates to all clients in a room
|
||||||
- Offline editing with eventual consistency
|
- All conflict resolution happens client-side via Yjs
|
||||||
- No need for operational transformation or locking
|
- Rooms are isolated by document ID
|
||||||
|
|
||||||
### WebSocket Broadcasting
|
### Data Flow
|
||||||
|
1. User edits → Yjs generates binary update
|
||||||
|
2. Update sent via WebSocket to backend
|
||||||
|
3. Backend broadcasts to all clients in room
|
||||||
|
4. Each client's Yjs applies and merges update
|
||||||
|
5. IndexedDB persists state locally
|
||||||
|
6. Periodic backup to PostgreSQL (optional)
|
||||||
|
|
||||||
The backend acts as a message broker:
|
## Testing
|
||||||
1. Receives binary Yjs updates from clients
|
|
||||||
2. Broadcasts updates to all clients in the same room
|
|
||||||
3. Does not interpret or modify the updates
|
|
||||||
4. Yjs handles all conflict resolution on the client side
|
|
||||||
|
|
||||||
## Contributing
|
The backend uses `testify/suite` for organized testing:
|
||||||
|
```bash
|
||||||
|
go test ./internal/handlers # All handler tests
|
||||||
|
go test -v ./internal/handlers -run TestDocumentHandler # Specific suite
|
||||||
|
```
|
||||||
|
|
||||||
1. Fork the repository
|
## Database Schema
|
||||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
||||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
- **documents**: Document metadata and Yjs state (BYTEA)
|
||||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
- **users**: OAuth user profiles
|
||||||
5. Open a Pull Request
|
- **sessions**: JWT tokens with expiration
|
||||||
|
- **shares**: User-to-user document sharing
|
||||||
|
- **share_links**: Public shareable links
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
MIT License
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
- [Yjs](https://github.com/yjs/yjs) - CRDT framework
|
- [Yjs](https://github.com/yjs/yjs) - CRDT framework
|
||||||
- [TipTap](https://tiptap.dev/) - Rich text editor
|
- [TipTap](https://tiptap.dev/) - Collaborative editor
|
||||||
- [Gin](https://gin-gonic.com/) - Go web framework
|
- [Gin](https://gin-gonic.com/) - Go web framework
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -67,14 +66,14 @@ func (h *AuthHandler) GoogleCallback(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
log.Println("Google callback state:", c.Query("state"))
|
log.Println("Google callback state:", c.Query("state"))
|
||||||
// Exchange code for token
|
// Exchange code for token
|
||||||
token, err := h.googleConfig.Exchange(context.Background(), c.Query("code"))
|
token, err := h.googleConfig.Exchange(c.Request.Context(), c.Query("code"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user info from Google
|
// Get user info from Google
|
||||||
client := h.googleConfig.Client(context.Background(), token)
|
client := h.googleConfig.Client(c.Request.Context(), token)
|
||||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
|
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
|
||||||
@@ -91,7 +90,12 @@ func (h *AuthHandler) GoogleCallback(c *gin.Context) {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Picture string `json:"picture"`
|
Picture string `json:"picture"`
|
||||||
}
|
}
|
||||||
json.Unmarshal(data, &userInfo)
|
|
||||||
|
if err := json.Unmarshal(data, &userInfo); err != nil {
|
||||||
|
log.Printf("Failed to parse Google response: %v | Data: %s", err, string(data))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid Google response"})
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Println("Google user info:", userInfo)
|
log.Println("Google user info:", userInfo)
|
||||||
// Upsert user in database
|
// Upsert user in database
|
||||||
user, err := h.store.UpsertUser(
|
user, err := h.store.UpsertUser(
|
||||||
@@ -124,12 +128,19 @@ func (h *AuthHandler) GoogleCallback(c *gin.Context) {
|
|||||||
|
|
||||||
// GithubLogin redirects to GitHub OAuth
|
// GithubLogin redirects to GitHub OAuth
|
||||||
func (h *AuthHandler) GithubLogin(c *gin.Context) {
|
func (h *AuthHandler) GithubLogin(c *gin.Context) {
|
||||||
url := h.githubConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
oauthState := generateStateOauthCookie(c.Writer)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, url)
|
url := h.githubConfig.AuthCodeURL(oauthState)
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GithubCallback handles GitHub OAuth callback
|
// GithubCallback handles GitHub OAuth callback
|
||||||
func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
||||||
|
oauthState, err := c.Cookie("oauthstate")
|
||||||
|
if err != nil || c.Query("state") != oauthState {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid oauth state"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("Github callback state:", c.Query("state"))
|
||||||
code := c.Query("code")
|
code := c.Query("code")
|
||||||
if code == "" {
|
if code == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No code provided"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "No code provided"})
|
||||||
@@ -137,14 +148,14 @@ func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exchange code for token
|
// Exchange code for token
|
||||||
token, err := h.githubConfig.Exchange(context.Background(), code)
|
token, err := h.githubConfig.Exchange(c.Request.Context(), code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user info from GitHub
|
// Get user info from GitHub
|
||||||
client := h.githubConfig.Client(context.Background(), token)
|
client := h.githubConfig.Client(c.Request.Context(), token)
|
||||||
|
|
||||||
// Get user profile
|
// Get user profile
|
||||||
resp, err := client.Get("https://api.github.com/user")
|
resp, err := client.Get("https://api.github.com/user")
|
||||||
@@ -162,7 +173,11 @@ func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
}
|
}
|
||||||
json.Unmarshal(data, &userInfo)
|
if err := json.Unmarshal(data, &userInfo); err != nil {
|
||||||
|
log.Printf("Failed to parse GitHub response: %v | Data: %s", err, string(data))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid GitHub response"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// If email is not public, fetch it separately
|
// If email is not public, fetch it separately
|
||||||
if userInfo.Email == "" {
|
if userInfo.Email == "" {
|
||||||
@@ -188,7 +203,8 @@ func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
|||||||
if userInfo.Name == "" {
|
if userInfo.Name == "" {
|
||||||
userInfo.Name = userInfo.Login
|
userInfo.Name = userInfo.Login
|
||||||
}
|
}
|
||||||
|
fmt.Println("Getting user info : ")
|
||||||
|
fmt.Println(userInfo)
|
||||||
// Upsert user in database
|
// Upsert user in database
|
||||||
user, err := h.store.UpsertUser(
|
user, err := h.store.UpsertUser(
|
||||||
c.Request.Context(),
|
c.Request.Context(),
|
||||||
@@ -199,7 +215,7 @@ func (h *AuthHandler) GithubCallback(c *gin.Context) {
|
|||||||
&userInfo.AvatarURL,
|
&userInfo.AvatarURL,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create user: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import { API_BASE_URL, authFetch } from './client';
|
|||||||
export const authApi = {
|
export const authApi = {
|
||||||
getCurrentUser: async (): Promise<User> => {
|
getCurrentUser: async (): Promise<User> => {
|
||||||
const response = await authFetch(`${API_BASE_URL}/auth/me`);
|
const response = await authFetch(`${API_BASE_URL}/auth/me`);
|
||||||
console.log("current user is " + response)
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to get current user');
|
throw new Error('Failed to get current user');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
const data = await response.json();
|
||||||
|
console.log("current user data:", data);
|
||||||
|
|
||||||
|
// Backend returns { user: {...} }, extract the user object
|
||||||
|
return data.user;
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: async (): Promise<void> => {
|
logout: async (): Promise<void> => {
|
||||||
|
|||||||
@@ -27,17 +27,25 @@ export const useYjsDocument = (documentId: string, shareToken?: string) => {
|
|||||||
|
|
||||||
const initializeDocument = async () => {
|
const initializeDocument = async () => {
|
||||||
// For share token access, use placeholder user info
|
// For share token access, use placeholder user info
|
||||||
const authUser = user || {
|
// Extract user data (handle both direct user object and nested structure for backwards compat)
|
||||||
|
const realUser = user || {
|
||||||
id: "anonymous",
|
id: "anonymous",
|
||||||
name: "Anonymous User",
|
name: "Anonymous User",
|
||||||
|
email: "",
|
||||||
avatar_url: undefined,
|
avatar_url: undefined,
|
||||||
};
|
};
|
||||||
const realUser = user?.user ? user.user : user || {};
|
|
||||||
const currentName = realUser.name || realUser.email || "Anonymous";
|
|
||||||
const currentId = realUser.id;
|
const currentId = realUser.id;
|
||||||
const currentAvatar = realUser.avatar_url || realUser.avatar;
|
const currentName = realUser.name || realUser.email || "Anonymous";
|
||||||
console.log("✅ [Fixed] User Name is:", currentName);
|
const currentAvatar = realUser.avatar_url;
|
||||||
console.log("🔍 [Debug] Initializing Awareness with User:", authUser); // <--- 添加这行
|
|
||||||
|
console.log("🔍 [Debug] User data for awareness:", {
|
||||||
|
id: currentId,
|
||||||
|
name: currentName,
|
||||||
|
email: realUser.email,
|
||||||
|
avatar: currentAvatar,
|
||||||
|
rawUser: realUser,
|
||||||
|
});
|
||||||
const authToken = token || "";
|
const authToken = token || "";
|
||||||
console.log("authToken is " + token);
|
console.log("authToken is " + token);
|
||||||
|
|
||||||
@@ -55,8 +63,8 @@ export const useYjsDocument = (documentId: string, shareToken?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"🔍 [Debug] Full authUser object:",
|
"🔍 [Debug] Full user object:",
|
||||||
JSON.stringify(authUser, null, 2)
|
JSON.stringify(realUser, null, 2)
|
||||||
);
|
);
|
||||||
// Set user info for awareness with authenticated user data
|
// Set user info for awareness with authenticated user data
|
||||||
yjsProviders.awareness.setLocalStateField("user", {
|
yjsProviders.awareness.setLocalStateField("user", {
|
||||||
@@ -124,8 +132,8 @@ export const useYjsDocument = (documentId: string, shareToken?: string) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Log local user info
|
// Log local user info
|
||||||
console.log(`[Awareness] Local user initialized: ${authUser.name}`, {
|
console.log(`[Awareness] Local user initialized: ${currentName}`, {
|
||||||
color: getColorFromUserId(authUser.id),
|
color: getColorFromUserId(currentId),
|
||||||
clientId: yjsProviders.awareness.clientID,
|
clientId: yjsProviders.awareness.clientID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ const Home = () => {
|
|||||||
const loadDocuments = async () => {
|
const loadDocuments = async () => {
|
||||||
try {
|
try {
|
||||||
const { documents } = await documentsApi.list();
|
const { documents } = await documentsApi.list();
|
||||||
setDocuments(documents);
|
setDocuments(documents || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load documents:", error);
|
console.error("Failed to load documents:", error);
|
||||||
|
setDocuments([]); // Set empty array on error to prevent null access
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user