@@ -22,8 +22,10 @@ jobs:
|
||||
|
||||
- name: Create dummy .env file
|
||||
run: |
|
||||
echo "GOOGLE_CLIENT_ID=dummy_client_id" > .env
|
||||
echo "GOOGLE_CLIENT_SECRET=dummy_client_secret" >> .env
|
||||
echo "CLI_GOOGLE_CLIENT_ID=dummy_client_id" > .env
|
||||
echo "CLI_GOOGLE_CLIENT_SECRET=dummy_client_secret" >> .env
|
||||
echo "WEB_GOOGLE_CLIENT_ID=dummy_client_id" >> .env
|
||||
echo "WEB_GOOGLE_CLIENT_SECRET=dummy_client_secret" >> .env
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NotePlace Backend Server
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
+8
-1
@@ -54,8 +54,15 @@ func main() {
|
||||
if err != nil {
|
||||
zap.S().Fatalf("Failed to initialize authenticator: %v", err)
|
||||
}
|
||||
authHandler := &api.AuthHandler{Store: s, Authenticator: authenticator}
|
||||
// Get Frontend URL from environment
|
||||
frontendURL := os.Getenv("FRONTEND_URL")
|
||||
if frontendURL == "" {
|
||||
frontendURL = "http://localhost:5173"
|
||||
}
|
||||
authHandler := &api.AuthHandler{Store: s, Authenticator: authenticator, FrontendURL: frontendURL}
|
||||
|
||||
router.GET("/api/auth/google/login", authHandler.HandleGoogleLogin)
|
||||
router.GET("/api/auth/google/callback", authHandler.HandleGoogleCallback)
|
||||
router.GET("/api/auth/google/cli/login", authHandler.HandleCliLogin)
|
||||
router.POST("/api/auth/google/cli/callback", authHandler.HandleCliCallback)
|
||||
router.GET("/api/auth/user", api.AuthMiddleware(s), authHandler.HandleGetUser)
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
# Frontend Design Document - Noteplace
|
||||
|
||||
## 1. Introduction
|
||||
This document outlines the frontend design for the Noteplace application, focusing on user experience, functionality, and technical considerations for a modern, cross-platform web application with a native-like mobile experience.
|
||||
|
||||
## 2. Core Principles
|
||||
* **User-Centric Design:** Intuitive, easy-to-use interface.
|
||||
* **Responsive & Adaptive:** Seamless experience across desktop and mobile devices.
|
||||
* **Performance:** Fast loading times and smooth interactions.
|
||||
* **Modern UI/UX:** Clean, aesthetically pleasing, and consistent design.
|
||||
* **Progressive Web App (PWA):** Native-like capabilities on mobile, especially for iOS "Add to Home Screen."
|
||||
|
||||
## 3. Technology Stack (Proposed)
|
||||
* **Framework:** React (with TypeScript) for robust component-based development.
|
||||
* **Styling:** Tailwind CSS for utility-first styling, enabling rapid UI development and responsiveness. Material Design principles will guide the overall aesthetic.
|
||||
* **State Management:** React Context API or Zustand for simple, scalable state management.
|
||||
* **Routing:** React Router for declarative navigation.
|
||||
* **API Communication:** Fetch API or Axios for interacting with the backend.
|
||||
* **Mapping:** Google Maps JavaScript API for interactive maps and place search.
|
||||
|
||||
## 4. Frontend Functionalities
|
||||
|
||||
### 4.1. User Authentication
|
||||
* **Login/Signup:** Integration with Google OAuth (as per `internal/auth/google.go`).
|
||||
* **Session Management:** Secure handling of user sessions.
|
||||
|
||||
### 4.2. Place Management
|
||||
* **Create Place:**
|
||||
* User inputs place name and category.
|
||||
* **Google Maps Integration:** As the user types, suggest real places using Google Places Autocomplete API.
|
||||
* Upon selection, populate address fields (`Street`, `City`, `State`, `Zip`, `Country`) from Google Places details.
|
||||
* Optionally, display a map preview of the selected location.
|
||||
* Submit to backend to create the `Place` entry.
|
||||
* **View Place Details:** Display place name, category, address, and a list of associated ratings and attributes.
|
||||
* **Search Places:** Allow users to search for existing places by name, category, or address components.
|
||||
|
||||
### 4.3. Rating Management
|
||||
* **Add Rating:**
|
||||
* Accessed from a Place's detail view.
|
||||
* User provides a score (1-10) and an optional comment.
|
||||
* **Attribute Integration:**
|
||||
* Frontend will fetch relevant attributes for the place using `GET /api/places/:id/attributes`.
|
||||
* Display these existing attributes (e.g., "Ambiance" for a "Restaurant").
|
||||
* Allow users to search for and select existing attributes.
|
||||
* If an attribute doesn't exist, provide an option to create it (see 4.4. Create Attribute). The newly created attribute can then be selected for the rating.
|
||||
* Submitting a rating will call `POST /api/ratings` with `placeId`, `attributeId`, `score`, and `comment`.
|
||||
* **View Ratings:**
|
||||
* For a specific place, ratings will be fetched using `GET /api/places/:id/ratings`.
|
||||
* For the current user, ratings will be fetched using `GET /api/ratings/my-ratings`.
|
||||
* Display individual ratings with score, comment, associated attribute, and user.
|
||||
* **Edit/Delete Rating:**
|
||||
* Allow users to edit their own ratings, calling `PUT /api/ratings/:id` to update `score` and `comment`.
|
||||
* Allow users to delete their own ratings, calling `DELETE /api/ratings/:id`.
|
||||
|
||||
### 4.4. Attribute Management
|
||||
* **Search Attributes:**
|
||||
* While creating a rating, users can search for existing attributes. This will likely involve a frontend-side search/filter of attributes already fetched or a dedicated backend search endpoint (if available, otherwise a comprehensive `GET /api/attributes` would be needed, which is not currently defined in `DESIGN.md`). For now, assume attributes are fetched per place or a general list is available.
|
||||
* **Create Attribute:**
|
||||
* When an attribute doesn't exist during rating creation, the user can define a new one.
|
||||
* The frontend will prompt for `Name`, `Scope` (CATEGORY or PLACE), and `OwnerID` (e.g., `placeCategory` for `CATEGORY` scope, or `placeId` for `PLACE` scope).
|
||||
* This will call `POST /api/attributes` to create the new attribute.
|
||||
|
||||
### 4.5. User Profile
|
||||
* **View Profile:** Display user's `DisplayName`, `Email`, `Role`, and `CreatedAt`.
|
||||
* **Edit Profile:** (Future consideration) Allow users to update display name or other non-critical information.
|
||||
* **User's Activity:** Show a summary of places created, ratings given, etc.
|
||||
|
||||
## 5. Pages and Views
|
||||
|
||||
### 5.1. Authentication Views
|
||||
* **Login Page:** Simple page with "Sign in with Google" button.
|
||||
* **Loading/Redirect Page:** For OAuth flow.
|
||||
|
||||
### 5.2. Main Application Views
|
||||
* **Dashboard/Home Page:**
|
||||
* Overview of recent activity (e.g., recently rated places, trending attributes).
|
||||
* Quick access to "Create New Place" and "Search Places."
|
||||
* **Place List/Search Results Page:**
|
||||
* Displays a list of places, filterable and sortable.
|
||||
* Each item shows key information (name, category, address snippet).
|
||||
* Should have map and list views.
|
||||
* **Place Detail Page:**
|
||||
* Prominent display of place information.
|
||||
* Section for "Ratings" (list of all ratings for this place).
|
||||
* Button to "Add New Rating."
|
||||
* Section for "Attributes" (list of attributes associated with this place/category).
|
||||
* **Create/Edit Place Form:** Guided form for adding new places, with Google Maps integration.
|
||||
* **Add Rating Form:** Form for submitting a rating, including attribute selection/creation.
|
||||
* **User Profile Page:** Displays user details and activity.
|
||||
|
||||
## 6. Desktop and Mobile Support
|
||||
|
||||
### 6.1. Responsive Design
|
||||
* The UI will be built using a mobile-first approach, ensuring optimal layout and usability on small screens.
|
||||
* Tailwind CSS will be used to implement responsive breakpoints, adapting layouts, font sizes, and component visibility for larger screens (tablets, desktops).
|
||||
|
||||
### 6.2. Mobile-Specific Enhancements
|
||||
* **Touch-Friendly Interactions:** Larger tap targets, swipe gestures where appropriate.
|
||||
* **Optimized Navigation:** Bottom navigation bar for primary actions on mobile.
|
||||
* **Performance:** Lazy loading of components and data, image optimization.
|
||||
|
||||
### 6.3. iOS "Add to Home Screen" (PWA)
|
||||
To provide a native-like experience on iOS when users add the web application to their home screen, the following PWA features will be implemented:
|
||||
* **Web App Manifest:** A `manifest.json` file will define the app's name, icons, start URL, display mode (`standalone`), and theme colors.
|
||||
* **Service Worker:** A service worker will be registered to enable:
|
||||
* **Offline Capabilities:** Caching essential assets and data for offline access.
|
||||
* **Faster Loading:** Instant loading on subsequent visits.
|
||||
* **Push Notifications:** (Future consideration) If backend supports it.
|
||||
* **Meta Tags:** Specific `<meta>` tags in the HTML head will configure iOS behavior, such as `apple-mobile-web-app-capable`, `apple-mobile-web-app-status-bar-style`, and `apple-mobile-web-app-title`.
|
||||
* **Splash Screen:** Custom splash screens will be configured via meta tags and manifest for a branded launch experience.
|
||||
|
||||
## 7. UI/UX Modernity
|
||||
* **Clean Typography:** Use modern, readable sans-serif fonts.
|
||||
* **Consistent Color Palette:** A well-defined primary, secondary, and accent color palette, adhering to accessibility standards.
|
||||
* **Intuitive Iconography:** Use a consistent icon set (e.g., Material Icons).
|
||||
* **Subtle Animations & Transitions:** Enhance user feedback and perceived performance.
|
||||
* **Card-Based Layouts:** For displaying lists of places, ratings, and attributes, providing clear visual separation.
|
||||
* **Form Design:** Clear labels, input validation feedback, and accessible controls.
|
||||
* **Dark Mode:** (Future consideration) Offer a dark theme option.
|
||||
|
||||
## 8. Backend API Reference for Frontend
|
||||
This section provides a detailed overview of the backend API endpoints, including their expected request and response structures, to serve as a clear contract for frontend development.
|
||||
|
||||
### 8.1. Data Models (JSON Representation)
|
||||
These are the core data structures as they will be sent/received in JSON format.
|
||||
|
||||
```json
|
||||
// User
|
||||
{
|
||||
"id": "string",
|
||||
"googleId": "string",
|
||||
"displayName": "string",
|
||||
"email": "string",
|
||||
"role": "number", // 0: ADMIN, 1: TRUSTED, 2: USER
|
||||
"createdAt": "string" // ISO 8601 format
|
||||
}
|
||||
|
||||
// Session
|
||||
{
|
||||
"id": "string",
|
||||
"userId": "string",
|
||||
"createdAt": "string",
|
||||
"updatedAt": "string"
|
||||
}
|
||||
|
||||
// Address
|
||||
{
|
||||
"street": "string",
|
||||
"city": "string",
|
||||
"state": "string",
|
||||
"zip": "string",
|
||||
"country": "string"
|
||||
}
|
||||
|
||||
// Place
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"category": "string",
|
||||
"address": {
|
||||
"street": "string",
|
||||
"city": "string",
|
||||
"state": "string",
|
||||
"zip": "string",
|
||||
"country": "string"
|
||||
},
|
||||
"createdAt": "string" // ISO 8601 format
|
||||
}
|
||||
|
||||
// Attribute
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"scope": "number", // 0: CATEGORY, 1: PLACE
|
||||
"ownerID": "string", // Category ID if scope is CATEGORY, Place ID if scope is PLACE
|
||||
"description": "string", // Optional
|
||||
"attachment": "string" // Optional
|
||||
}
|
||||
|
||||
// Rating
|
||||
{
|
||||
"id": "string",
|
||||
"placeId": "string",
|
||||
"attributeId": "string",
|
||||
"userId": "string",
|
||||
"score": "number", // 1-10
|
||||
"comment": "string", // Optional
|
||||
"createdAt": "string" // ISO 8601 format
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2. Authentication Endpoints
|
||||
|
||||
#### `GET /api/auth/google/cli/login`
|
||||
* **Description:** Initiates the CLI login process.
|
||||
* **Request:** No body.
|
||||
* **Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"verification_url": "string",
|
||||
"user_code": "string"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/auth/logout`
|
||||
* **Description:** Logs the user out (invalidates the session).
|
||||
* **Request:** No body.
|
||||
* **Headers:** `Authorization: <YOUR_SESSION_ID>`
|
||||
* **Response (200 OK):** No body.
|
||||
|
||||
### 8.3. Place Endpoints
|
||||
|
||||
#### `GET /api/places`
|
||||
* **Description:** Retrieves a list of all places.
|
||||
* **Request:** No body.
|
||||
* **Response (200 OK):** `Array<Place>`
|
||||
|
||||
#### `POST /api/places`
|
||||
* **Description:** Creates a new place.
|
||||
* **Headers:** `Authorization: <YOUR_SESSION_ID>`, `Content-Type: application/json`
|
||||
* **Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"category": "string",
|
||||
"address": {
|
||||
"street": "string",
|
||||
"city": "string",
|
||||
"state": "string",
|
||||
"zip": "string",
|
||||
"country": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
* **Response (201 Created):** `Place`
|
||||
|
||||
#### `GET /api/places/:id`
|
||||
* **Description:** Retrieves details for a single place.
|
||||
* **Request:** No body.
|
||||
* **Response (200 OK):** `Place`
|
||||
|
||||
#### `GET /api/places/:id/ratings`
|
||||
* **Description:** Retrieves all ratings for a specific place.
|
||||
* **Request:** No body.
|
||||
* **Response (200 OK):** `Array<Rating>`
|
||||
|
||||
#### `GET /api/places/:id/attributes`
|
||||
* **Description:** Retrieves relevant attributes for a place (based on its category and specific place attributes).
|
||||
* **Request:** No body.
|
||||
* **Response (200 OK):** `Array<Attribute>`
|
||||
|
||||
### 8.4. Rating Endpoints
|
||||
|
||||
#### `POST /api/ratings`
|
||||
* **Description:** Creates a new rating.
|
||||
* **Headers:** `Authorization: <YOUR_SESSION_ID>`, `Content-Type: application/json`
|
||||
* **Request Body:**
|
||||
```json
|
||||
{
|
||||
"placeId": "string",
|
||||
"attributeId": "string",
|
||||
"score": "number", // 1-10
|
||||
"comment": "string" // Optional
|
||||
}
|
||||
```
|
||||
* **Response (201 Created):** `Rating`
|
||||
|
||||
#### `PUT /api/ratings/:id`
|
||||
* **Description:** Updates an existing rating.
|
||||
* **Headers:** `Authorization: <YOUR_SESSION_ID>`, `Content-Type: application/json`
|
||||
* **Request Body:**
|
||||
```json
|
||||
{
|
||||
"score": "number", // 1-10
|
||||
"comment": "string" // Optional
|
||||
}
|
||||
```
|
||||
* **Response (200 OK):** `Rating`
|
||||
|
||||
#### `DELETE /api/ratings/:id`
|
||||
* **Description:** Deletes a rating.
|
||||
* **Headers:** `Authorization: <YOUR_SESSION_ID>`
|
||||
* **Request:** No body.
|
||||
* **Response (204 No Content):** No body.
|
||||
|
||||
#### `GET /api/ratings/my-ratings`
|
||||
* **Description:** Retrieves all ratings made by the authenticated user.
|
||||
* **Headers:** `Authorization: <YOUR_SESSION_ID>`
|
||||
* **Request:** No body.
|
||||
* **Response (200 OK):** `Array<Rating>`
|
||||
|
||||
### 8.5. Attribute Endpoints
|
||||
|
||||
#### `POST /api/attributes`
|
||||
* **Description:** Creates a new attribute.
|
||||
* **Headers:** `Authorization: <YOUR_SESSION_ID>`, `Content-Type: application/json`
|
||||
* **Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"scope": "number", // 0: CATEGORY, 1: PLACE
|
||||
"ownerID": "string", // Required. Category ID if scope is CATEGORY, Place ID if scope is PLACE
|
||||
"description": "string", // Optional
|
||||
"attachment": "string" // Optional
|
||||
}
|
||||
```
|
||||
* **Response (201 Created):** `Attribute`
|
||||
|
||||
## 9. Future Considerations
|
||||
* **Push Notifications:** For new ratings on followed places or attributes.
|
||||
* **User Following:** Follow other users to see their ratings.
|
||||
* **Image Uploads:** For places and attributes.
|
||||
* **Advanced Search Filters:** More granular filtering for places and ratings.
|
||||
* **Localization:** Support for multiple languages.
|
||||
* **Accessibility:** Comprehensive WCAG compliance.
|
||||
+62
-10
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,17 +9,48 @@ import (
|
||||
"git.pengzhan.dev/noteplace-server/internal/store"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// AuthHandler handles the authentication API endpoints.
|
||||
type AuthHandler struct {
|
||||
Store *store.Store
|
||||
Authenticator *auth.Authenticator
|
||||
FrontendURL string
|
||||
}
|
||||
|
||||
// HandleCliLogin provides the user with the URL to start the auth process.
|
||||
// HandleGoogleLogin redirects the user to the Google consent page for the web flow.
|
||||
func (h *AuthHandler) HandleGoogleLogin(c *gin.Context) {
|
||||
url := h.Authenticator.GetWebAuthURL()
|
||||
zap.S().Infof("Redirecting user to Google for web authentication: %s", url)
|
||||
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||
}
|
||||
|
||||
// HandleGoogleCallback handles the redirect from Google for the web flow.
|
||||
func (h *AuthHandler) HandleGoogleCallback(c *gin.Context) {
|
||||
code := c.Query("code")
|
||||
if code == "" {
|
||||
zap.S().Warn("Web auth callback received without a code.")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "code is missing"})
|
||||
return
|
||||
}
|
||||
|
||||
zap.S().Info("Received web auth code, exchanging for token.")
|
||||
token, err := h.Authenticator.ExchangeWebCodeForToken(code)
|
||||
if err != nil {
|
||||
zap.S().Errorf("Failed to exchange web code for token: %v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Failed to validate code"})
|
||||
return
|
||||
}
|
||||
|
||||
h.handleToken(c, token)
|
||||
}
|
||||
|
||||
// HandleCliLogin provides the user with the URL to start the auth process for the CLI flow.
|
||||
func (h *AuthHandler) HandleCliLogin(c *gin.Context) {
|
||||
authURL := h.Authenticator.GetAuthURL()
|
||||
authURL := h.Authenticator.GetCliAuthURL()
|
||||
zap.S().Info("Providing CLI user with auth URL.")
|
||||
|
||||
response := gin.H{
|
||||
"message": `Please open the following URL in your browser to authorize the application.\nAfter authorizing, Google will provide a code. Paste it here to complete the login.\nExample: curl -X POST -d '{\"code\":\"YOUR_CODE_HERE\"}' http://localhost:3001/api/auth/google/cli/callback`,
|
||||
@@ -29,43 +59,53 @@ func (h *AuthHandler) HandleCliLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// HandleCliCallback exchanges the code for a token and creates a session.
|
||||
// HandleCliCallback exchanges the code for a token and creates a session for the CLI flow.
|
||||
func (h *AuthHandler) HandleCliCallback(c *gin.Context) {
|
||||
var reqBody struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
||||
zap.S().Warnf("Invalid request body for CLI callback: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.Authenticator.ExchangeCodeForToken(reqBody.Code)
|
||||
zap.S().Info("Received CLI auth code, exchanging for token.")
|
||||
token, err := h.Authenticator.ExchangeCliCodeForToken(reqBody.Code)
|
||||
if err != nil {
|
||||
log.Printf("Failed to exchange code for token: %v", err)
|
||||
zap.S().Errorf("Failed to exchange cli code for token: %v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Failed to validate code"})
|
||||
return
|
||||
}
|
||||
|
||||
h.handleToken(c, token)
|
||||
}
|
||||
|
||||
// handleToken is a helper function to handle user/session creation after getting a token.
|
||||
func (h *AuthHandler) handleToken(c *gin.Context, token *oauth2.Token) {
|
||||
zap.S().Info("Token acquired, fetching user info.")
|
||||
userInfo, err := h.Authenticator.GetUserInfo(token)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get user info: %v", err)
|
||||
zap.S().Errorf("Failed to get user info: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info from Google"})
|
||||
return
|
||||
}
|
||||
|
||||
// --- User and Session Logic ---
|
||||
user, found := h.Store.GetUserByGoogleID(userInfo.GoogleID)
|
||||
if !found {
|
||||
zap.S().Infof("User with Google ID %s not found, creating new user.", userInfo.GoogleID)
|
||||
userInfo.ID = "user-" + uuid.New().String()
|
||||
userInfo.CreatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
if err := h.Store.CreateUser(userInfo); err != nil {
|
||||
zap.S().Errorf("Failed to create user: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
|
||||
return
|
||||
}
|
||||
user = userInfo
|
||||
} else {
|
||||
zap.S().Infof("Found existing user %s for Google ID %s.", user.ID, user.GoogleID)
|
||||
}
|
||||
|
||||
// Create a new session and save it to the store
|
||||
sessionID := "session-" + uuid.New().String()
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
newSession := models.Session{
|
||||
@@ -75,11 +115,23 @@ func (h *AuthHandler) HandleCliCallback(c *gin.Context) {
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := h.Store.CreateSession(newSession); err != nil {
|
||||
zap.S().Errorf("Failed to create session: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
|
||||
return
|
||||
}
|
||||
zap.S().Infof("Created new session %s for user %s.", newSession.ID, user.ID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Login successful!", "session_token": newSession.ID})
|
||||
// Redirect back to the frontend for the web flow
|
||||
if c.Request.Method == "GET" {
|
||||
// Simple way to distinguish web callback from CLI POST
|
||||
redirectURL := h.FrontendURL + "/auth/callback?token=" + newSession.ID
|
||||
zap.S().Infof("Redirecting to frontend: %s", redirectURL)
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
} else {
|
||||
// Respond with JSON for the CLI flow
|
||||
zap.S().Info("Returning session token to CLI client.")
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Login successful!", "session_token": newSession.ID})
|
||||
}
|
||||
}
|
||||
|
||||
// HandleGetUser returns the current user based on the session token.
|
||||
|
||||
+42
-17
@@ -14,44 +14,69 @@ import (
|
||||
|
||||
// Authenticator handles the OAuth2 flow.
|
||||
type Authenticator struct {
|
||||
Config *oauth2.Config
|
||||
WebConfig *oauth2.Config
|
||||
CliConfig *oauth2.Config
|
||||
}
|
||||
|
||||
// NewAuthenticator creates a new Authenticator.
|
||||
// It requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables.
|
||||
func NewAuthenticator() (*Authenticator, error) {
|
||||
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
||||
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
cliClientID := os.Getenv("CLI_GOOGLE_CLIENT_ID")
|
||||
cliclientSecret := os.Getenv("CLI_GOOGLE_CLIENT_SECRET")
|
||||
webClientID := os.Getenv("WEB_GOOGLE_CLIENT_ID")
|
||||
webClientSecret := os.Getenv("WEB_GOOGLE_CLIENT_SECRET")
|
||||
|
||||
if clientID == "" || clientSecret == "" {
|
||||
return nil, fmt.Errorf("GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set")
|
||||
if cliClientID == "" || cliclientSecret == "" {
|
||||
return nil, fmt.Errorf("CLI_GOOGLE_CLIENT_ID and CLI_GOOGLE_CLIENT_SECRET must be set for CLI")
|
||||
}
|
||||
|
||||
config := &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
if webClientID == "" || webClientSecret == "" {
|
||||
return nil, fmt.Errorf("WEB_GOOGLE_CLIENT_ID and WEB_GOOGLE_CLIENT_SECRET must be set for web")
|
||||
}
|
||||
|
||||
webConfig := &oauth2.Config{
|
||||
ClientID: webClientID,
|
||||
ClientSecret: webClientSecret,
|
||||
RedirectURL: "http://localhost:3001/api/auth/google/callback", // For web flow
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
cliConfig := &oauth2.Config{
|
||||
ClientID: cliClientID,
|
||||
ClientSecret: cliclientSecret,
|
||||
RedirectURL: "urn:ietf:wg:oauth:2.0:oob", // Special redirect URI for desktop/CLI apps
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
return &Authenticator{Config: config}, nil
|
||||
return &Authenticator{WebConfig: webConfig, CliConfig: cliConfig}, nil
|
||||
}
|
||||
|
||||
// GetAuthURL generates the URL the user must visit to authorize the application.
|
||||
func (a *Authenticator) GetAuthURL() string {
|
||||
// We don't need a state for this simple CLI flow, but it's good practice for web apps.
|
||||
return a.Config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
// GetWebAuthURL generates the URL for the web-based auth flow.
|
||||
func (a *Authenticator) GetWebAuthURL() string {
|
||||
return a.WebConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
||||
// ExchangeCodeForToken takes an authorization code and exchanges it for a token.
|
||||
func (a *Authenticator) ExchangeCodeForToken(code string) (*oauth2.Token, error) {
|
||||
return a.Config.Exchange(context.Background(), code)
|
||||
// GetCliAuthURL generates the URL for the CLI-based auth flow.
|
||||
func (a *Authenticator) GetCliAuthURL() string {
|
||||
return a.CliConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
||||
// ExchangeWebCodeForToken takes an authorization code from the web flow and exchanges it for a token.
|
||||
func (a *Authenticator) ExchangeWebCodeForToken(code string) (*oauth2.Token, error) {
|
||||
return a.WebConfig.Exchange(context.Background(), code)
|
||||
}
|
||||
|
||||
// ExchangeCliCodeForToken takes an authorization code from the CLI flow and exchanges it for a token.
|
||||
func (a *Authenticator) ExchangeCliCodeForToken(code string) (*oauth2.Token, error) {
|
||||
return a.CliConfig.Exchange(context.Background(), code)
|
||||
}
|
||||
|
||||
// GetUserInfo uses the token to fetch the user's profile from Google.
|
||||
func (a *Authenticator) GetUserInfo(token *oauth2.Token) (models.User, error) {
|
||||
client := a.Config.Client(context.Background(), token)
|
||||
// This client can be created from either WebConfig or CliConfig, it doesn't matter which.
|
||||
client := a.WebConfig.Client(context.Background(), token)
|
||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
|
||||
Reference in New Issue
Block a user