From 9ac76777279a8e00d3c1e6a6d933931096b5226d Mon Sep 17 00:00:00 2001 From: Mathieu Coulet <114529377+Djangss@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:53:51 +0100 Subject: [PATCH 01/26] feat(db) workflow table with events (#121) --- server/internal/models/event.go | 20 ++++++++++++++++++++ server/internal/models/service.go | 8 ++++++++ server/internal/models/workflow.go | 27 +++++++++++++++++++++++++++ server/internal/pkg/db.go | 24 ++++++++++++++++++++++-- 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 server/internal/models/event.go create mode 100644 server/internal/models/service.go create mode 100644 server/internal/models/workflow.go diff --git a/server/internal/models/event.go b/server/internal/models/event.go new file mode 100644 index 0000000..b5e9545 --- /dev/null +++ b/server/internal/models/event.go @@ -0,0 +1,20 @@ +package models + +import "gorm.io/gorm" + +type WorkflowStatus string + +type Event struct { + gorm.Model + Name string `json:"name"` + Description string `json:"description"` + ServiceID uint `gorm:"foreignKey:ServiceID" json:"service_id"` + Parameters []Parameters `json:"parameters"` + Type EventType `gorm:"type:enum('action', 'reaction');not null" json:"type"` +} + +const ( + WorkflowStatusPending WorkflowStatus = "pending" + WorkflowStatusProcessed WorkflowStatus = "processed" + WorkflowStatusFailed WorkflowStatus = "failed" +) diff --git a/server/internal/models/service.go b/server/internal/models/service.go new file mode 100644 index 0000000..ac74e0a --- /dev/null +++ b/server/internal/models/service.go @@ -0,0 +1,8 @@ +package models + +import "gorm.io/gorm" + +type Service struct { + gorm.Model + Label string `json:"label"` +} diff --git a/server/internal/models/workflow.go b/server/internal/models/workflow.go new file mode 100644 index 0000000..7657e13 --- /dev/null +++ b/server/internal/models/workflow.go @@ -0,0 +1,27 @@ +package models + +import "gorm.io/gorm" + +type EventType string + +const ( + ActionEventType EventType = "action" + ReactionEventType EventType = "reaction" +) + +type Parameters struct { + gorm.Model + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + EventID uint `gorm:"foreignKey:EventID" json:"event_id"` +} + +type Workflow struct { + gorm.Model + UserID uint `gorm:"foreignKey:UserID" json:"user_id"` + Name string `json:"name"` + Description string `json:"description"` + Status WorkflowStatus `gorm:"type:enum('pending', 'processed', 'failed')" json:"status"` + Events []Event `gorm:"many2many:workflow_events" json:"events"` +} diff --git a/server/internal/pkg/db.go b/server/internal/pkg/db.go index 5139365..8bc06d2 100644 --- a/server/internal/pkg/db.go +++ b/server/internal/pkg/db.go @@ -1,6 +1,7 @@ package db import ( + "AREA/internal/models" "AREA/internal/utils" "fmt" "gorm.io/driver/mysql" @@ -10,6 +11,20 @@ import ( var DB *gorm.DB +func migrateDB() error { + err := DB.AutoMigrate( + &models.User{}, + &models.Workflow{}, + &models.Event{}, + &models.Parameters{}, + &models.Service{}, + ) + if err != nil { + log.Fatalf("Failed to migrate DB: %v", err) + } + return err +} + func InitDB() { dbHost := utils.GetEnvVar("DB_HOST") dbPort := utils.GetEnvVar("DB_PORT") @@ -20,7 +35,12 @@ func InitDB() { err := error(nil) DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { - log.Fatalf("failed to initialize database, got error %v", err) + log.Fatalf("Failed to connect database: %v", err) + return + } + log.Println("Database connection established") + if migrateDB() != nil { + return } - fmt.Println("Database connection established") + log.Println("Migration done") } From 5ce701c48b974f32b9bb1c09940c1d9a633bf29c Mon Sep 17 00:00:00 2001 From: Mael-RABOT Date: Wed, 4 Dec 2024 14:55:30 +0100 Subject: [PATCH 02/26] hotfix(security): add token check Signed-off-by: Mael-RABOT --- client_web/src/Components/Security.tsx | 28 +++----------------------- server/internal/controllers/auth.go | 5 ----- server/internal/middleware/auth.go | 16 ++++++--------- server/internal/middleware/cors.go | 3 ++- server/internal/utils/token.go | 10 ++++++++- 5 files changed, 20 insertions(+), 42 deletions(-) diff --git a/client_web/src/Components/Security.tsx b/client_web/src/Components/Security.tsx index 9d68723..cc3f63d 100644 --- a/client_web/src/Components/Security.tsx +++ b/client_web/src/Components/Security.tsx @@ -1,9 +1,8 @@ import React, { useState, useEffect } from "react"; import { useAuth } from "@/Context/ContextHooks"; import { useNavigate } from "react-router-dom"; -// import { instance, auth } from "@/Config/backend.routes"; import { Spin } from 'antd'; -import {instance, root} from "@Config/backend.routes"; +import { instance, instanceWithAuth, root, auth } from "@Config/backend.routes"; import {toast} from "react-toastify"; type SecurityProps = { @@ -21,8 +20,7 @@ const Security = ({ children }: SecurityProps) => { const checkAuth = () => { if (!isAuthenticated || !jsonWebToken) { if (localStorage.getItem("jsonWebToken")) { - // instance.post(auth.health, {jwt: localStorage.getItem("jsonWebToken")}) //TODO: Create /health endpoint - Promise.resolve() + instanceWithAuth.get(auth.health) .then(() => { setIsAuthenticated(true); setJsonWebToken(localStorage.getItem("jsonWebToken") as string); @@ -50,27 +48,7 @@ const Security = ({ children }: SecurityProps) => { }; checkAuth(); - }, [isAuthenticated, jsonWebToken, navigate, setIsAuthenticated, setJsonWebToken]); - - const ping = () => { - const response = instance.get(root.ping) - .then((response) => { - setPingResponse(true); - }) - .catch((error) => { - setPingResponse(false); - console.error(error); - navigate("/error/connection"); - toast.error('Failed to ping the server'); - }); - }; - - React.useEffect(() => { - if (!hasPinged.current) { - ping(); - hasPinged.current = true; - } - }, []); + }, [isAuthenticated, jsonWebToken]); if (loading) { return ; diff --git a/server/internal/controllers/auth.go b/server/internal/controllers/auth.go index c917223..83b9488 100644 --- a/server/internal/controllers/auth.go +++ b/server/internal/controllers/auth.go @@ -97,11 +97,6 @@ func Register(c *gin.Context) { // @Failure 401 {object} map[string]string // @Router /auth/health [get] func Health(c *gin.Context) { - token := c.GetHeader("Authorization") - if token == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: No token provided"}) - return - } _, err := utils.VerifyToken(c) if err != nil { diff --git a/server/internal/middleware/auth.go b/server/internal/middleware/auth.go index 6504434..aba024d 100644 --- a/server/internal/middleware/auth.go +++ b/server/internal/middleware/auth.go @@ -19,17 +19,8 @@ func AuthMiddleware() gin.HandlerFunc { } func isAuthenticated(c *gin.Context) bool { - token := c.GetHeader("Authorization") - if token == "" { - return false - } - user := models.User{} - db.DB.Where("token = ?", token).First(&user) - if user.ID == 0 { - return false - } - _, err := utils.VerifyToken(c) + email, err := utils.VerifyToken(c) if err != nil { if err.Error() == "Token is expired" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is expired"}) @@ -38,5 +29,10 @@ func isAuthenticated(c *gin.Context) bool { } return false } + db.DB.Where("email = ?", email).First(&user) + if user.ID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return false + } return true } diff --git a/server/internal/middleware/cors.go b/server/internal/middleware/cors.go index 88b7700..51ab6e1 100644 --- a/server/internal/middleware/cors.go +++ b/server/internal/middleware/cors.go @@ -9,6 +9,7 @@ import ( func EnableCors() gin.HandlerFunc { corsConfig := cors.DefaultConfig() corsConfig.AllowOrigins = config.AppConfig.CorsOrigins - corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"} + corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} + corsConfig.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"} return cors.New(corsConfig) } diff --git a/server/internal/utils/token.go b/server/internal/utils/token.go index 3166d1a..d771e9a 100644 --- a/server/internal/utils/token.go +++ b/server/internal/utils/token.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "net/http" "time" + "strings" ) func NewToken(c *gin.Context, email string) string { @@ -21,7 +22,14 @@ func NewToken(c *gin.Context, email string) string { } func VerifyToken(c *gin.Context) (string, error) { - tokenString := c.GetHeader("Authorization") + authHeader := c.GetHeader("Authorization") + + if !strings.HasPrefix(authHeader, "Bearer ") { + return "", errors.New("Bearer token is missing") + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == "" { return "", errors.New("Authorization token is missing") } From c2d20101fb236d079a5eb2ff4f0e0d3f6933abaf Mon Sep 17 00:00:00 2001 From: Mathieu Coulet <114529377+Djangss@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:06:30 +0100 Subject: [PATCH 03/26] Mco/feat(Workflow) can be published, listed and deleted (#123) * feat(Workflow) can be published, listed and deleted * feat(Workflow) remove debbug statement * feat(Workflow) front now publish correctly Workflows * feat(Workflow) remove log statement --- client_web/src/Config/backend.routes.ts | 7 +- .../src/Pages/Workflows/CreateWorkflow.tsx | 84 +++--- client_web/src/types.ts | 14 +- server/config.json | 2 +- server/docs/docs.go | 262 +++++++++++++++--- server/docs/swagger.json | 262 +++++++++++++++--- server/docs/swagger.yaml | 184 +++++++++--- server/internal/controllers/auth.go | 17 +- server/internal/controllers/workflow.go | 85 ++++++ server/internal/models/workflow.go | 9 + server/internal/pkg/db.go | 2 +- server/internal/pkg/request.go | 19 ++ server/internal/pkg/token.go | 21 ++ server/internal/routers/routers.go | 11 + server/main.go | 2 +- 15 files changed, 819 insertions(+), 162 deletions(-) create mode 100644 server/internal/controllers/workflow.go create mode 100644 server/internal/pkg/request.go create mode 100644 server/internal/pkg/token.go diff --git a/client_web/src/Config/backend.routes.ts b/client_web/src/Config/backend.routes.ts index f98a48e..f1165bc 100644 --- a/client_web/src/Config/backend.routes.ts +++ b/client_web/src/Config/backend.routes.ts @@ -42,9 +42,14 @@ const auth = { health: `${endpoint}/auth/health`, } +const workflow = { + create: `${endpoint}/workflow/create`, +} + export { instance, instanceWithAuth, root, - auth + auth, + workflow } diff --git a/client_web/src/Pages/Workflows/CreateWorkflow.tsx b/client_web/src/Pages/Workflows/CreateWorkflow.tsx index 415dddb..f598472 100644 --- a/client_web/src/Pages/Workflows/CreateWorkflow.tsx +++ b/client_web/src/Pages/Workflows/CreateWorkflow.tsx @@ -7,6 +7,8 @@ import { normalizeName } from "@/Pages/Workflows/CreateWorkflow.utils"; // Import types correctly import { About, Service, Action, Reaction, Workflow, Parameter } from "@/types"; import {toast} from "react-toastify"; +import {instanceWithAuth} from "@/Config/backend.routes"; +import {workflow as workflowRoute} from "@/Config/backend.routes"; const { Title, Text } = Typography; const { Panel } = Collapse; @@ -238,43 +240,59 @@ const CreateWorkflow: React.FC = () => { const workflow: Workflow = { name: workflowName, description: workflowDescription, - actions: selectedActions.map(action => { - const actionDef = about?.server.services + service: about?.server.services.find(service => + service.actions.some(action => action.name === selectedActions[0]?.name) + )?.name ?? "unknown", + events: + [ + ...selectedActions.map(action => { + const actionDef = about?.server.services .flatMap((s: Service) => s.actions) .find((a: Action) => a.name === action.name); - return { - name: action.name, - parameters: Object.entries(action.parameters || {}).map(([name, value]) => { - const paramDef = actionDef?.parameters.find((p: Parameter) => p.name === name); - return { - name, - type: paramDef?.type || 'string', - value - }; - }) - }; - }), - reactions: selectedReactions.map(reaction => { - const reactionDef = about?.server.services - .flatMap((s: Service) => s.reactions) - .find((r: Reaction) => r.name === reaction.name); - - return { - name: reaction.name, - parameters: Object.entries(reaction.parameters || {}).map(([name, value]) => { - const paramDef = reactionDef?.parameters.find((p: Parameter) => p.name === name); - return { - name, - type: paramDef?.type || 'string', - value - }; - }) - }; + return { + name: action.name, + type: 'action' as "action", + description: action.description, + parameters: Object.entries(action.parameters || {}).map(([name, value]) => { + const paramDef = actionDef?.parameters.find((p: Parameter) => p.name === name); + return { + name, + type: paramDef?.type || 'string', + value + }; + }) + } + }), + ...selectedReactions.map(reaction => { + const reactionDef = about?.server.services + .flatMap((s: Service) => s.reactions) + .find((r: Reaction) => r.name === reaction.name); + + return { + name: reaction.name, + type: 'reaction' as "reaction", + description: reaction.description, + parameters: Object.entries(reaction.parameters || {}).map(([name, value]) => { + const paramDef = reactionDef?.parameters.find((p: Parameter) => p.name === name); + return { + name, + type: paramDef?.type || 'string', + value + }; + }) + }; + }) + ] + } + instanceWithAuth.post(workflowRoute.create, workflow) + .then(() => { + toast.success("Workflow successfully published") + //TODO: Go to /workflow/{id} }) - }; - - toast.error("API not connected yet"); + .catch((error) => { + console.error(error); + }); }; const handleFoldAllActions = () => { diff --git a/client_web/src/types.ts b/client_web/src/types.ts index c550305..8965467 100644 --- a/client_web/src/types.ts +++ b/client_web/src/types.ts @@ -46,22 +46,16 @@ export interface WorkflowParameter { export interface WorkflowDefinition { name: string; + type: "action" | "reaction"; + description: string; parameters: WorkflowParameter[]; } -export interface WorkflowAction extends WorkflowDefinition { - -} - -export interface WorkflowReaction extends WorkflowDefinition { - -} - export type Workflow = { name: string; + service: string; description: string; - actions: WorkflowAction[]; - reactions: WorkflowReaction[]; + events: WorkflowDefinition[]; }; export interface WorkflowItem { diff --git a/server/config.json b/server/config.json index a2a0f5c..e145ef1 100644 --- a/server/config.json +++ b/server/config.json @@ -6,6 +6,6 @@ "swagger": true, "cors_origins": [ "https://localhost:8081", - "http://example.com" + "http://localhost:8080" ] } diff --git a/server/docs/docs.go b/server/docs/docs.go index f4d5647..e414698 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -47,11 +47,46 @@ const docTemplate = `{ } } }, - "/login": { + "/auth/health": { + "get": { + "description": "Validate the token and return 200 if valid, 401 if expired or invalid", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Check if the JWT is valid", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/login": { "post": { "description": "Authenticate a user and return a JWT token", "consumes": [ - "application/x-www-form-urlencoded" + "application/json" ], "produces": [ "application/json" @@ -62,18 +97,13 @@ const docTemplate = `{ "summary": "Login a user", "parameters": [ { - "type": "string", - "description": "Email", - "name": "email", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "Password", - "name": "password", - "in": "formData", - "required": true + "description": "Login", + "name": "Login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } } ], "responses": { @@ -98,6 +128,61 @@ const docTemplate = `{ } } }, + "/auth/register": { + "post": { + "description": "Create a new user and return a JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register a user", + "parameters": [ + { + "description": "Register", + "name": "Register", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.RegisterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/ping": { "get": { "description": "ping", @@ -153,39 +238,68 @@ const docTemplate = `{ } } }, - "/register": { + "/workflow/create": { "post": { - "description": "Create a new user and return a JWT token", + "description": "Create a new workflow", "consumes": [ - "application/x-www-form-urlencoded" + "application/json" ], "produces": [ "application/json" ], "tags": [ - "auth" + "workflow" ], - "summary": "Register a user", + "summary": "Create a workflow", "parameters": [ { - "type": "string", - "description": "Email", - "name": "email", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "Username", - "name": "username", - "in": "formData", - "required": true + "description": "workflow", + "name": "workflow", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Workflow" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Workflow" + } }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/workflow/delete/{id}": { + "delete": { + "description": "Delete a workflow by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "Delete a workflow", + "parameters": [ { - "type": "string", - "description": "Password", - "name": "password", - "in": "formData", + "type": "integer", + "description": "workflow ID", + "name": "id", + "in": "path", "required": true } ], @@ -199,8 +313,17 @@ const docTemplate = `{ } } }, - "409": { - "description": "Conflict", + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", "schema": { "type": "object", "additionalProperties": { @@ -219,6 +342,71 @@ const docTemplate = `{ } } } + }, + "/workflow/list": { + "get": { + "description": "List all workflows", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "List workflows", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Workflow" + } + } + } + } + } + } + }, + "definitions": { + "models.LoginRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "models.RegisterRequest": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.Workflow": { + "type": "object" } } }` diff --git a/server/docs/swagger.json b/server/docs/swagger.json index 006df19..6befab3 100644 --- a/server/docs/swagger.json +++ b/server/docs/swagger.json @@ -41,11 +41,46 @@ } } }, - "/login": { + "/auth/health": { + "get": { + "description": "Validate the token and return 200 if valid, 401 if expired or invalid", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Check if the JWT is valid", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/login": { "post": { "description": "Authenticate a user and return a JWT token", "consumes": [ - "application/x-www-form-urlencoded" + "application/json" ], "produces": [ "application/json" @@ -56,18 +91,13 @@ "summary": "Login a user", "parameters": [ { - "type": "string", - "description": "Email", - "name": "email", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "Password", - "name": "password", - "in": "formData", - "required": true + "description": "Login", + "name": "Login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } } ], "responses": { @@ -92,6 +122,61 @@ } } }, + "/auth/register": { + "post": { + "description": "Create a new user and return a JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register a user", + "parameters": [ + { + "description": "Register", + "name": "Register", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.RegisterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/ping": { "get": { "description": "ping", @@ -147,39 +232,68 @@ } } }, - "/register": { + "/workflow/create": { "post": { - "description": "Create a new user and return a JWT token", + "description": "Create a new workflow", "consumes": [ - "application/x-www-form-urlencoded" + "application/json" ], "produces": [ "application/json" ], "tags": [ - "auth" + "workflow" ], - "summary": "Register a user", + "summary": "Create a workflow", "parameters": [ { - "type": "string", - "description": "Email", - "name": "email", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "Username", - "name": "username", - "in": "formData", - "required": true + "description": "workflow", + "name": "workflow", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Workflow" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Workflow" + } }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/workflow/delete/{id}": { + "delete": { + "description": "Delete a workflow by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "Delete a workflow", + "parameters": [ { - "type": "string", - "description": "Password", - "name": "password", - "in": "formData", + "type": "integer", + "description": "workflow ID", + "name": "id", + "in": "path", "required": true } ], @@ -193,8 +307,17 @@ } } }, - "409": { - "description": "Conflict", + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", "schema": { "type": "object", "additionalProperties": { @@ -213,6 +336,71 @@ } } } + }, + "/workflow/list": { + "get": { + "description": "List all workflows", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "List workflows", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Workflow" + } + } + } + } + } + } + }, + "definitions": { + "models.LoginRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "models.RegisterRequest": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.Workflow": { + "type": "object" } } } \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index 4b9c056..4c7f76c 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -1,4 +1,30 @@ basePath: / +definitions: + models.LoginRequest: + properties: + email: + type: string + password: + type: string + required: + - email + - password + type: object + models.RegisterRequest: + properties: + email: + type: string + password: + type: string + username: + type: string + required: + - email + - password + - username + type: object + models.Workflow: + type: object host: localhost:8080 info: contact: @@ -28,22 +54,41 @@ paths: summary: About tags: - about - /login: + /auth/health: + get: + consumes: + - application/json + description: Validate the token and return 200 if valid, 401 if expired or invalid + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Check if the JWT is valid + tags: + - auth + /auth/login: post: consumes: - - application/x-www-form-urlencoded + - application/json description: Authenticate a user and return a JWT token parameters: - - description: Email - in: formData - name: email - required: true - type: string - - description: Password - in: formData - name: password + - description: Login + in: body + name: Login required: true - type: string + schema: + $ref: '#/definitions/models.LoginRequest' produces: - application/json responses: @@ -62,6 +107,42 @@ paths: summary: Login a user tags: - auth + /auth/register: + post: + consumes: + - application/json + description: Create a new user and return a JWT token + parameters: + - description: Register + in: body + name: Register + required: true + schema: + $ref: '#/definitions/models.RegisterRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "409": + description: Conflict + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Register a user + tags: + - auth /ping: get: consumes: @@ -98,27 +179,45 @@ paths: summary: Message tags: - publish/Message - /register: + /workflow/create: post: consumes: - - application/x-www-form-urlencoded - description: Create a new user and return a JWT token + - application/json + description: Create a new workflow parameters: - - description: Email - in: formData - name: email + - description: workflow + in: body + name: workflow required: true - type: string - - description: Username - in: formData - name: username - required: true - type: string - - description: Password - in: formData - name: password + schema: + $ref: '#/definitions/models.Workflow' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Workflow' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + summary: Create a workflow + tags: + - workflow + /workflow/delete/{id}: + delete: + consumes: + - application/json + description: Delete a workflow by ID + parameters: + - description: workflow ID + in: path + name: id required: true - type: string + type: integer produces: - application/json responses: @@ -128,8 +227,14 @@ paths: additionalProperties: type: string type: object - "409": - description: Conflict + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found schema: additionalProperties: type: string @@ -140,7 +245,24 @@ paths: additionalProperties: type: string type: object - summary: Register a user + summary: Delete a workflow tags: - - auth + - workflow + /workflow/list: + get: + consumes: + - application/json + description: List all workflows + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Workflow' + type: array + summary: List workflows + tags: + - workflow swagger: "2.0" diff --git a/server/internal/controllers/auth.go b/server/internal/controllers/auth.go index 83b9488..61acd6e 100644 --- a/server/internal/controllers/auth.go +++ b/server/internal/controllers/auth.go @@ -2,7 +2,7 @@ package controllers import ( "AREA/internal/models" - "AREA/internal/pkg" + db "AREA/internal/pkg" "AREA/internal/utils" "github.com/gin-gonic/gin" "log" @@ -13,13 +13,12 @@ import ( // @Summary Login a user // @Description Authenticate a user and return a JWT token // @Tags auth -// @Accept x-www-form-urlencoded +// @Accept json // @Produce json -// @Param email json string true "email" -// @Param password json string true "password" +// @Param Login body models.LoginRequest true "Login" // @Success 200 {object} map[string]string // @Failure 401 {object} map[string]string -// @Router /login [post] +// @Router /auth/login [post] func Login(c *gin.Context) { var LoginData models.LoginRequest err := c.ShouldBindJSON(&LoginData) @@ -48,15 +47,13 @@ func Login(c *gin.Context) { // @Summary Register a user // @Description Create a new user and return a JWT token // @Tags auth -// @Accept x-www-form-urlencoded +// @Accept json // @Produce json -// @Param email json string true "email" -// @Param username json string true "username" -// @Param password json string true "password" +// @Param Register body models.RegisterRequest true "Register" // @Success 200 {object} map[string]string // @Failure 409 {object} map[string]string // @Failure 500 {object} map[string]string -// @Router /register [post] +// @Router /auth/register [post] func Register(c *gin.Context) { var RegisterData models.RegisterRequest err := c.ShouldBindJSON(&RegisterData) diff --git a/server/internal/controllers/workflow.go b/server/internal/controllers/workflow.go new file mode 100644 index 0000000..fe6ef79 --- /dev/null +++ b/server/internal/controllers/workflow.go @@ -0,0 +1,85 @@ +package controllers + +import ( + "AREA/internal/models" + "AREA/internal/pkg" + "github.com/gin-gonic/gin" + "net/http" + "strconv" +) + +// WorkflowCreate godoc +// @Summary Create a workflow +// @Description Create a new workflow +// @Tags workflow +// @Accept json +// @Produce json +// @Param workflow body models.Workflow true "workflow" +// @Success 200 {object} models.Workflow +// @Failure 400 {object} map[string]string +// @Router /workflow/create [post] +func WorkflowCreate(c *gin.Context) { + var workflow models.Workflow + err := c.BindJSON(&workflow) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + workflow.UserID, err = pkg.GetUserFromToken(c) + if err != nil { + return + } + pkg.DB.Create(&workflow) + c.JSON(200, gin.H{"workflow": workflow}) +} + +// WorkflowList godoc +// @Summary List workflows +// @Description List all workflows +// @Tags workflow +// @Accept json +// @Produce json +// @Success 200 {object} []models.Workflow +// @Router /workflow/list [get] +func WorkflowList(c *gin.Context) { + var workflows []models.Workflow + userID, err := pkg.GetUserFromToken(c) + if err != nil { + return + } + pkg.DB.Where("user_id = ?", userID).Find(&workflows) + c.JSON(200, gin.H{"workflows": workflows}) +} + +// WorkflowDelete godoc +// @Summary Delete a workflow +// @Description Delete a workflow by ID +// @Tags workflow +// @Accept json +// @Produce json +// @Param id path int true "workflow ID" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /workflow/delete/{id} [delete] +func WorkflowDelete(c *gin.Context) { + idParam := c.Param("id") + workflowID, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid workflow ID"}) + return + } + var workflow models.Workflow + result := pkg.DB.First(&workflow, workflowID) + if result.Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Workflow not found"}) + return + } + result = pkg.DB.Delete(&workflow) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete workflow"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Workflow deleted successfully"}) +} diff --git a/server/internal/models/workflow.go b/server/internal/models/workflow.go index 7657e13..8cdc29c 100644 --- a/server/internal/models/workflow.go +++ b/server/internal/models/workflow.go @@ -23,5 +23,14 @@ type Workflow struct { Name string `json:"name"` Description string `json:"description"` Status WorkflowStatus `gorm:"type:enum('pending', 'processed', 'failed')" json:"status"` + IsActive bool `json:"is_active"` Events []Event `gorm:"many2many:workflow_events" json:"events"` } + +func (w *Workflow) BeforeCreate(tx *gorm.DB) (err error) { + if w.Status == "" { + w.Status = WorkflowStatusPending + } + w.IsActive = true + return +} diff --git a/server/internal/pkg/db.go b/server/internal/pkg/db.go index 8bc06d2..47f6b1e 100644 --- a/server/internal/pkg/db.go +++ b/server/internal/pkg/db.go @@ -1,4 +1,4 @@ -package db +package pkg import ( "AREA/internal/models" diff --git a/server/internal/pkg/request.go b/server/internal/pkg/request.go new file mode 100644 index 0000000..5a326ca --- /dev/null +++ b/server/internal/pkg/request.go @@ -0,0 +1,19 @@ +package pkg + +import ( + "bytes" + "fmt" + "github.com/gin-gonic/gin" + "io" +) + +func PrintRequestJSON(c *gin.Context) { + var requestBody bytes.Buffer + _, err := io.Copy(&requestBody, c.Request.Body) + if err != nil { + c.JSON(500, gin.H{"error": "Failed to read request body"}) + return + } + fmt.Println("Request JSON:", requestBody.String()) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody.Bytes())) +} diff --git a/server/internal/pkg/token.go b/server/internal/pkg/token.go new file mode 100644 index 0000000..1767c74 --- /dev/null +++ b/server/internal/pkg/token.go @@ -0,0 +1,21 @@ +package pkg + +import ( + "AREA/internal/models" + "AREA/internal/utils" + "errors" + "github.com/gin-gonic/gin" +) + +func GetUserFromToken(c *gin.Context) (uint, error) { + email, err := utils.VerifyToken(c) + if err != nil { + return 0, err + } + user := models.User{} + DB.Where("email = ?", email).First(&user) + if user.ID == 0 { + return 0, errors.New("User not found") + } + return user.ID, nil +} diff --git a/server/internal/routers/routers.go b/server/internal/routers/routers.go index 4ea5f34..2536527 100644 --- a/server/internal/routers/routers.go +++ b/server/internal/routers/routers.go @@ -32,6 +32,16 @@ func setUpAuthGroup(router *gin.Engine) { } } +func setUpWorkflowGroup(router *gin.Engine) { + workflow := router.Group("/workflow") + workflow.Use(middleware.AuthMiddleware()) + { + workflow.POST("/create", controllers.WorkflowCreate) + workflow.GET("/list", controllers.WorkflowList) + workflow.DELETE("/delete/:id", controllers.WorkflowDelete) + } +} + func SetupRouter() *gin.Engine { router := gin.Default() router.Use(middleware.ErrorHandlerMiddleware()) @@ -48,6 +58,7 @@ func SetupRouter() *gin.Engine { } setUpAuthGroup(router) setUpOauthGroup(router) + setUpWorkflowGroup(router) protected := router.Group("/") protected.Use(middleware.AuthMiddleware()) { diff --git a/server/main.go b/server/main.go index 5fe61e5..bfb0bef 100644 --- a/server/main.go +++ b/server/main.go @@ -3,7 +3,7 @@ package main import ( "AREA/internal/config" "AREA/internal/models" - "AREA/internal/pkg" + db "AREA/internal/pkg" "AREA/internal/routers" "fmt" "github.com/gin-gonic/gin" From 805608dcf4fdd4158c87a06139e00a4782898ec5 Mon Sep 17 00:00:00 2001 From: Samuel Bruschet Date: Thu, 5 Dec 2024 09:10:27 +0100 Subject: [PATCH 04/26] Sbr/feat microsoft oauth mobile (#124) * feat(oauth): getting microsoft access token via oauth * feat(style): changed button with microsoft icons * fix(spotify): removed useless spotify oauth code * fix(warnings): removed unnecessary comments and warnings --------- Signed-off-by: Samuel Bruschet --- client_mobile/android/settings.gradle | 4 +- .../assets/images/microsoft_logo.png | Bin 0 -> 1584 bytes client_mobile/lib/features/auth/login.dart | 230 ++++-------------- client_mobile/lib/features/auth/register.dart | 157 +++++------- client_mobile/lib/main.dart | 2 + .../microsoft/microsoft_auth_service.dart | 38 +++ client_mobile/lib/widgets/button.dart | 2 - client_mobile/lib/widgets/form_field.dart | 5 +- client_mobile/lib/widgets/sign_in_button.dart | 28 +-- client_mobile/lib/widgets/simple_text.dart | 1 - .../Flutter/GeneratedPluginRegistrant.swift | 4 + client_mobile/pubspec.lock | 148 +++++++++-- client_mobile/pubspec.yaml | 2 + 13 files changed, 303 insertions(+), 318 deletions(-) create mode 100644 client_mobile/assets/images/microsoft_logo.png create mode 100644 client_mobile/lib/services/microsoft/microsoft_auth_service.dart diff --git a/client_mobile/android/settings.gradle b/client_mobile/android/settings.gradle index 536165d..74bd010 100644 --- a/client_mobile/android/settings.gradle +++ b/client_mobile/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "com.android.application" version "7.3.1" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/client_mobile/assets/images/microsoft_logo.png b/client_mobile/assets/images/microsoft_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3e62d6dc3d0eff604b2beda60ddf206d3ee28cf7 GIT binary patch literal 1584 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&w@L)Zt|+Cx8@7x}&cn1H;CC?mvmFK)yn< zN02WALzNl>LqiJ#!!Mvv!wUw6QUeBtR|yOZRx=nF#0%!^3bX-A_yzccxPAtLInAGI zqdxcKueuOD|CICAt3{uKRX?ZO^&e7h+{5;{GHlLC`=~=7mDYc-+wt+w35UN2Rezt4 zao+Wjb=imiyEy)wbS*mh>DQIw|NsA+J`T(Qn#`Ew?d~G^CU0v8ki%Z$>Fdh=f}M?3 zn{{Dibsx~t_dQ)4Ln>~)y>wErDS(01F(oj-kl&Da7?@g_7+V>bXd4(<85sPQ@wkJc cAvZrIGp!Q0hAzhs2|x`Dp00i_>zopr00IQKhX4Qo literal 0 HcmV?d00001 diff --git a/client_mobile/lib/features/auth/login.dart b/client_mobile/lib/features/auth/login.dart index 2b8f996..26982eb 100644 --- a/client_mobile/lib/features/auth/login.dart +++ b/client_mobile/lib/features/auth/login.dart @@ -1,4 +1,5 @@ // import 'dart:io'; +import 'package:client_mobile/services/microsoft/microsoft_auth_service.dart'; import 'package:client_mobile/widgets/button.dart'; import 'package:client_mobile/widgets/clickable_text.dart'; import 'package:client_mobile/widgets/form_field.dart'; @@ -6,15 +7,8 @@ import 'package:client_mobile/widgets/sign_in_button.dart'; import 'package:client_mobile/widgets/simple_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; -import 'package:flutter_web_auth/flutter_web_auth.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; -import 'package:oauth2/oauth2.dart' as oauth2; -// import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; -// import 'package:oauth2_client/access_token_response.dart'; -// import 'package:oauth2_client/authorization_response.dart'; -// import 'package:oauth2_client/spotify_oauth2_client.dart'; class LoginPage extends StatefulWidget { @override @@ -23,190 +17,64 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { final String callbackUrlScheme = 'my.area.app'; - String get redirectUrlMobile => '$callbackUrlScheme://callback'; + String get spotifyRedirectUrlMobile => '$callbackUrlScheme://callback'; final String clientId = dotenv.env["VITE_SPOTIFY_CLIENT_ID"] ?? ""; - final appAuth = FlutterAppAuth(); - final String clientSecret = dotenv.env["VITE_SPOTIFY_CLIENT_SECRET"] ?? ""; - final String issuer = "https://accounts.spotify.com/"; - final Uri authorizationEndpoint = - Uri.parse('https://accounts.spotify.com/authorize'); - final Uri tokenEndpoint = Uri.parse('https://accounts.spotify.com/api/token'); - final String redirectUrlWeb = "https://localhost:8081/auth/spotify/callback"; - final Uri redirectUri = - Uri.parse('https://localhost:8081/auth/spotify/callback'); - oauth2.Client? client; - - Future authenticateWithSpotify() async { - try { - final String spotifyUrl = 'https://accounts.spotify.com/authorize' - '&client_id=$clientId' - '&redirect_uri=$redirectUrlMobile'; - - print("Authentification en cours ..."); - final result = await FlutterWebAuth.authenticate( - url: spotifyUrl, - callbackUrlScheme: callbackUrlScheme, - ); - - print("voici le résultat : ${result}"); - } catch (e) { - print("erreur d'authentification : ${e}"); - } - } - - Future authenticateWithSpotifyOld() async { - try { - // print("client id : ${clientId}"); - // AccessTokenResponse? accessToken; - // SpotifyOAuth2Client client = SpotifyOAuth2Client( - // redirectUri: redirectUrlMobile, - // customUriScheme: "my.area.app" - // ); - - // var authResp = - // await client.requestAuthorization(clientId: clientId, customParams: { - // // 'show_dialog': 'true' - // }, scopes: [ - // 'user-read-private', - // 'user-read-playback-state', - // 'user-modify-playback-state', - // 'user-read-currently-playing', - // 'user-read-email' - // ]); - // var authCode = authResp.code; - - // print("auth code : ${authCode}"); - - final authorizationTokenRequest = AuthorizationTokenRequest( - clientId, redirectUrlMobile, - issuer: issuer, - clientSecret: clientSecret, - scopes: ["openid", "profile", "email", "offline_access"], - additionalParameters: {'show_dialog': 'true'}); - - final result = - await appAuth.authorizeAndExchangeCode(authorizationTokenRequest); - - print("--------------------------------"); - print(""); - print(""); - print("Le code échangé est le suivant : ${result}"); - print(""); - print(""); - print("--------------------------------"); - - return; - // Start a small HTTP server to capture the callback - // final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080); - // print("Listening on ${server.address}:${server.port}"); - - // // Build the OAuth2 flow - // final grant = oauth2.AuthorizationCodeGrant( - // clientId, - // authorizationEndpoint, - // tokenEndpoint, - // secret: clientSecret, - // httpClient: http.Client(), - // ); - - // // Generate the authorization URL - // final authorizationUrl = grant.getAuthorizationUrl(redirectUri, scopes: [ - // 'user-read-email', - // 'user-read-private', - // ]); - - // // Open the browser for user login - // print('Opening browser: $authorizationUrl'); - // await Process.run('open', [authorizationUrl.toString()]); - - // // Wait for the callback - // final request = await server.first; - - // // Validate and close the server - // final queryParameters = request.uri.queryParameters; - // final code = queryParameters['code']; - // final state = queryParameters['state']; - // request.response - // ..statusCode = 200 - // ..headers.set('Content-Type', ContentType.html.mimeType) - // ..write('You can now close this page.') - // ..close(); - // await server.close(); - - // // Exchange the authorization code for tokens - // if (code != null) { - // client = await grant.handleAuthorizationCode(code); - // setState(() {}); - // print( - // 'Authenticated successfully! Access token: ${client?.credentials.accessToken}'); - // } - } catch (e) { - print('Authentication failed: $e'); - } - } + final appAuth = const FlutterAppAuth(); @override Widget build(BuildContext context) { return Scaffold( body: Center( - child: client == null - ? Container( - padding: const EdgeInsets.all(20), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SimpleText( - "Email", - bold: true, - ), - const AreaFormField(label: "Value"), - const SizedBox(height: 50), - const SimpleText("Password", bold: true), - const AreaFormField(label: "Value"), - const SizedBox(height: 15), - Align( - alignment: Alignment.center, - child: AreaButton( - label: "Login", - onPressed: () {}, - color: Colors.black, - ), - ), - const SizedBox(height: 30), - Align( - alignment: Alignment.center, - child: SignInButton( - onPressed: () { - authenticateWithSpotifyOld(); - }, - label: "Sign in with Spotify", - icon: const FaIcon( - size: 34, - FontAwesomeIcons.spotify, - color: Colors.green, - ), - ), - ), - const SizedBox(height: 5), - Align( - alignment: Alignment.center, - child: SmallClickableText( - "I don't have an account", - onPressed: () { - context.push("/register"); - }, - ), - ) - ], + child: Container( + padding: const EdgeInsets.all(20), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SimpleText( + "Email", + bold: true, + ), + const AreaFormField(label: "Value"), + const SizedBox(height: 50), + const SimpleText("Password", bold: true), + const AreaFormField(label: "Value"), + const SizedBox(height: 15), + AreaButton( + label: "Login", + onPressed: () {}, + color: Colors.black, + ), + const SizedBox(height: 30), + Align( + alignment: Alignment.center, + child: SignInButton( + onPressed: () { + MicrosoftAuthService.auth(context); + }, + label: "Sign in with Microsoft", + image: Image.asset( + "assets/images/microsoft_logo.png", + width: 40, + height: 30, ), ), + ), + const SizedBox(height: 5), + Align( + alignment: Alignment.center, + child: SmallClickableText( + "I don't have an account", + onPressed: () { + context.push("/register"); + }, + ), ) - : Text( - 'Authenticated! Access token: ${client?.credentials.accessToken}'), - ), + ], + ), + ), + )), ); } } diff --git a/client_mobile/lib/features/auth/register.dart b/client_mobile/lib/features/auth/register.dart index 86ea991..af46783 100644 --- a/client_mobile/lib/features/auth/register.dart +++ b/client_mobile/lib/features/auth/register.dart @@ -1,4 +1,5 @@ // import 'dart:io'; +import 'package:client_mobile/services/microsoft/microsoft_auth_service.dart'; import 'package:client_mobile/widgets/button.dart'; import 'package:client_mobile/widgets/clickable_text.dart'; import 'package:client_mobile/widgets/form_field.dart'; @@ -6,15 +7,8 @@ import 'package:client_mobile/widgets/sign_in_button.dart'; import 'package:client_mobile/widgets/simple_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; -// import 'package:flutter_web_auth/flutter_web_auth.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; -import 'package:oauth2/oauth2.dart' as oauth2; -// import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; -// import 'package:oauth2_client/access_token_response.dart'; -// import 'package:oauth2_client/authorization_response.dart'; -// import 'package:oauth2_client/spotify_oauth2_client.dart'; class RegisterPage extends StatefulWidget { @override @@ -23,110 +17,71 @@ class RegisterPage extends StatefulWidget { class _RegisterPageState extends State { final String callbackUrlScheme = 'my.area.app'; - String get redirectUrlMobile => '$callbackUrlScheme://callback'; + String get spotifyRedirectUrlMobile => '$callbackUrlScheme://callback'; final String clientId = dotenv.env["VITE_SPOTIFY_CLIENT_ID"] ?? ""; - final appAuth = FlutterAppAuth(); - final String clientSecret = dotenv.env["VITE_SPOTIFY_CLIENT_SECRET"] ?? ""; - final String issuer = "https://accounts.spotify.com/"; - final Uri authorizationEndpoint = - Uri.parse('https://accounts.spotify.com/authorize'); - final Uri tokenEndpoint = Uri.parse('https://accounts.spotify.com/api/token'); - final String redirectUrlWeb = "https://localhost:8081/auth/spotify/callback"; - final Uri redirectUri = - Uri.parse('https://localhost:8081/auth/spotify/callback'); - oauth2.Client? client; - - Future authenticateWithSpotifyOld() async { - try { - final authorizationTokenRequest = AuthorizationTokenRequest( - clientId, redirectUrlMobile, - issuer: issuer, - clientSecret: clientSecret, - scopes: ["openid", "profile", "email", "offline_access"], - additionalParameters: {'show_dialog': 'true'}); - - final result = - await appAuth.authorizeAndExchangeCode(authorizationTokenRequest); - - print("--------------------------------"); - print(""); - print(""); - print("Le code échangé est le suivant : ${result}"); - print(""); - print(""); - print("--------------------------------"); - - return; - } catch (e) { - print('Authentication failed: $e'); - } - } + final appAuth = const FlutterAppAuth(); @override Widget build(BuildContext context) { return Scaffold( body: Center( - child: client == null - ? Container( - padding: const EdgeInsets.all(20), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SimpleText( - "Username", - bold: true, - ), - const AreaFormField(label: "Value"), - const SizedBox(height: 25), - const SimpleText("Password", bold: true), - const AreaFormField(label: "Value"), - const SizedBox(height: 15), - const SimpleText("Confirm password", bold: true), - const AreaFormField(label: "Value"), - const SizedBox(height: 15), - Align( - alignment: Alignment.center, - child: AreaButton( - label: "Register", - onPressed: () {}, - color: Colors.black, - ), - ), - const SizedBox(height: 30), - Align( - alignment: Alignment.center, - child: SignInButton( - onPressed: () { - authenticateWithSpotifyOld(); - }, - label: "Sign in with Spotify", - icon: const FaIcon( - size: 34, - FontAwesomeIcons.spotify, - color: Colors.green, - ), - ), - ), - const SizedBox(height: 5), - Align( - alignment: Alignment.center, - child: SmallClickableText( - "I already have an account", - onPressed: () { - context.pop(); - }, - ), - ) - ], + child: Container( + padding: const EdgeInsets.all(20), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SimpleText( + "Username", + bold: true, + ), + const AreaFormField(label: "Value"), + const SizedBox(height: 25), + const SimpleText("Password", bold: true), + const AreaFormField(label: "Value"), + const SizedBox(height: 15), + const SimpleText("Confirm password", bold: true), + const AreaFormField(label: "Value"), + const SizedBox(height: 15), + Align( + alignment: Alignment.center, + child: AreaButton( + label: "Register", + onPressed: () {}, + color: Colors.black, + ), + ), + const SizedBox(height: 30), + Align( + alignment: Alignment.center, + child: SignInButton( + onPressed: () { + MicrosoftAuthService.auth(context); + }, + label: "Sign in with Microsoft", + image: Image.asset( + "assets/images/microsoft_logo.png", + width: 40, + height: 30, ), ), + ), + const SizedBox(height: 5), + Align( + alignment: Alignment.center, + child: SmallClickableText( + "I already have an account", + onPressed: () { + context.pop(); + }, + ), ) - : Text( - 'Authenticated! Access token: ${client?.credentials.accessToken}'), - ), + ], + ), + ), + )), ); } } diff --git a/client_mobile/lib/main.dart b/client_mobile/lib/main.dart index 4488297..8f8a249 100644 --- a/client_mobile/lib/main.dart +++ b/client_mobile/lib/main.dart @@ -4,10 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:go_router/go_router.dart'; +final GlobalKey navigatorKey = GlobalKey(); Future main() async { await dotenv.load(fileName: ".env"); final GoRouter router = GoRouter( + navigatorKey: navigatorKey, routes: [ GoRoute( path: '/', diff --git a/client_mobile/lib/services/microsoft/microsoft_auth_service.dart b/client_mobile/lib/services/microsoft/microsoft_auth_service.dart new file mode 100644 index 0000000..fc7eb3a --- /dev/null +++ b/client_mobile/lib/services/microsoft/microsoft_auth_service.dart @@ -0,0 +1,38 @@ +import 'package:aad_oauth/model/config.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:aad_oauth/aad_oauth.dart'; +import '../../main.dart'; + +class MicrosoftAuthService { + static final String clientId = dotenv.env["VITE_MICROSOFT_CLIENT_ID"] ?? ""; + static const String redirectUri = + "msauth://my.area.app/lvGC0B4SWYU8tNPHg%2FbdMjQinZQ%3D"; + static const String authority = "https://login.microsoftonline.com/common"; + static const FlutterSecureStorage secureStorage = FlutterSecureStorage(); + + static Future auth(BuildContext context) async { + final Config config = Config( + tenant: "common", + clientId: clientId, + scope: "https://graph.microsoft.com/.default", + navigatorKey: navigatorKey, + redirectUri: redirectUri); + config.webUseRedirect = false; + + final AadOAuth oauth = AadOAuth(config); + final result = await oauth.login(); + result.fold( + (l) => ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l.message), + backgroundColor: Colors.red, + ), + ), + (r) async { + await secureStorage.write(key: 'microsoft_access_token', value: r.accessToken); + }, + ); + } +} diff --git a/client_mobile/lib/widgets/button.dart b/client_mobile/lib/widgets/button.dart index 1389341..3ca9261 100644 --- a/client_mobile/lib/widgets/button.dart +++ b/client_mobile/lib/widgets/button.dart @@ -34,8 +34,6 @@ class AreaButton extends StatelessWidget { fontSize: 24, color: Colors.white, )), - // const Spacer(), - // if (icon != null) FaIcon(size: 25, icon!), ), ); } diff --git a/client_mobile/lib/widgets/form_field.dart b/client_mobile/lib/widgets/form_field.dart index 116cdd3..cafcfb9 100644 --- a/client_mobile/lib/widgets/form_field.dart +++ b/client_mobile/lib/widgets/form_field.dart @@ -48,13 +48,12 @@ class AreaFormField extends StatelessWidget { color: Theme.of(context).colorScheme.onSurface, ), decoration: InputDecoration( - // fillColor: Theme.of(context).colorScheme., labelText: label, // Placeholder text border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide( - color: Color.fromARGB(255, 221, 228, 222), // Couleur de la bordure lorsque le champ est actif - width: 2.0, // Largeur de la bordure + color: Color.fromARGB(255, 221, 228, 222), + width: 2.0, ), ), suffixIcon: suffixIcon, diff --git a/client_mobile/lib/widgets/sign_in_button.dart b/client_mobile/lib/widgets/sign_in_button.dart index ad982f1..827921c 100644 --- a/client_mobile/lib/widgets/sign_in_button.dart +++ b/client_mobile/lib/widgets/sign_in_button.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class SignInButton extends StatelessWidget { const SignInButton( - {super.key, required this.label, this.icon, this.onPressed}); + {super.key, required this.label, this.image, this.onPressed}); final String label; - final FaIcon? icon; + final Image? image; final void Function()? onPressed; @override @@ -16,14 +15,14 @@ class SignInButton extends StatelessWidget { child: FilledButton( onPressed: onPressed ?? () {}, style: ButtonStyle( + padding: const WidgetStatePropertyAll(EdgeInsets.all(10)), backgroundColor: WidgetStateProperty.all(Colors.white), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(6.0), side: const BorderSide( - color: Color.fromARGB(255, 221, 228, - 222), // Couleur de la bordure lorsque le champ est actif - width: 2.0, // Largeur de la bordure + color: Color.fromARGB(255, 221, 228, 222), + width: 2.0, ), ), ), @@ -34,14 +33,15 @@ class SignInButton extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - if (icon != null) icon!, - const SizedBox(width: 5), - Text(label, - style: const TextStyle( - fontFamily: "CoolveticaCondensed", - fontSize: 12, - color: Colors.black, - )), + if (image != null) image!, + Text( + label, + style: const TextStyle( + fontFamily: "CoolveticaCondensed", + fontSize: 12, + color: Colors.black, + ), + ), ], ), ), diff --git a/client_mobile/lib/widgets/simple_text.dart b/client_mobile/lib/widgets/simple_text.dart index 6f954ef..002461a 100644 --- a/client_mobile/lib/widgets/simple_text.dart +++ b/client_mobile/lib/widgets/simple_text.dart @@ -22,7 +22,6 @@ class SimpleText extends StatelessWidget { text, textAlign: textAlign, style: TextStyle( - // fontFamily: HodFonts.coolvetica, fontWeight: bold ? FontWeight.bold : null, fontSize: textSize, color: color, diff --git a/client_mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/client_mobile/macos/Flutter/GeneratedPluginRegistrant.swift index 4f2a89c..0550540 100644 --- a/client_mobile/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/client_mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,7 +10,9 @@ import flutter_secure_storage_macos import flutter_web_auth import flutter_web_auth_2 import path_provider_foundation +import shared_preferences_foundation import url_launcher_macos +import webview_flutter_wkwebview import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -19,6 +21,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin")) WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) } diff --git a/client_mobile/pubspec.lock b/client_mobile/pubspec.lock index cc5d156..467f5f1 100644 --- a/client_mobile/pubspec.lock +++ b/client_mobile/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + aad_oauth: + dependency: "direct main" + description: + name: aad_oauth + sha256: "7678046a7b4b5e967d324e7431626343084451e04ac847e382948c8e1286c30e" + url: "https://pub.dev" + source: hosted + version: "1.0.1" args: dependency: transitive description: @@ -65,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dartz: + dependency: transitive + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" dotenv: dependency: "direct main" description: @@ -73,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -89,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -252,18 +284,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -300,18 +332,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" oauth2: dependency: "direct main" description: @@ -408,6 +440,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "7f172d1b06de5da47b6264c2692ee2ead20bbbc246690427cdb4fc301cd0c549" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -457,10 +545,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" typed_data: dependency: transitive description: @@ -545,10 +633,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" web: dependency: transitive description: @@ -557,6 +645,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec" + url: "https://pub.dev" + source: hosted + version: "4.10.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "285cedfd9441267f6cca8843458620b5fda1af75b04f5818d0441acda5d7df19" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: b7e92f129482460951d96ef9a46b49db34bd2e1621685de26e9eaafd9674e7eb + url: "https://pub.dev" + source: hosted + version: "3.16.3" win32: dependency: transitive description: @@ -582,5 +702,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.4.1 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/client_mobile/pubspec.yaml b/client_mobile/pubspec.yaml index 664d0f6..a5640fa 100644 --- a/client_mobile/pubspec.yaml +++ b/client_mobile/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: flutter_dotenv: ^5.2.1 font_awesome_flutter: ^10.8.0 go_router: ^14.6.1 + aad_oauth: ^1.0.1 dev_dependencies: flutter_test: @@ -69,6 +70,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - .env.mobile + - assets/images/microsoft_logo.png # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg From 29fca04b784bb6393afdde84d31b10463f748aab Mon Sep 17 00:00:00 2001 From: Samuel Bruschet Date: Thu, 5 Dec 2024 09:26:02 +0100 Subject: [PATCH 05/26] fix(pubspec): pubspec.lock file was missing (#127) From e749213e5d7007045f079cd2a4d7dc17cec53e93 Mon Sep 17 00:00:00 2001 From: Mathieu Coulet <114529377+Djangss@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:44:28 +0100 Subject: [PATCH 06/26] fix(Security) using golang-jwt instead of jwt-go (#125) --- server/go.mod | 2 +- server/go.sum | 2 ++ server/internal/utils/token.go | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/server/go.mod b/server/go.mod index ba3c3a8..92792c6 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,7 +1,6 @@ module AREA require ( - github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.0 github.com/gookit/config/v2 v2.2.5 @@ -30,6 +29,7 @@ require ( github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.3 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gookit/goutil v0.6.17 // indirect github.com/hashicorp/hcl v1.0.0 // indirect diff --git a/server/go.sum b/server/go.sum index 52de721..7ae59d3 100644 --- a/server/go.sum +++ b/server/go.sum @@ -54,6 +54,8 @@ github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU github.com/goccy/go-yaml v1.11.2 h1:joq77SxuyIs9zzxEjgyLBugMQ9NEgTWxXfz2wVqwAaQ= github.com/goccy/go-yaml v1.11.2/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/server/internal/utils/token.go b/server/internal/utils/token.go index d771e9a..72863d8 100644 --- a/server/internal/utils/token.go +++ b/server/internal/utils/token.go @@ -2,11 +2,11 @@ package utils import ( "errors" - "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" "net/http" - "time" "strings" + "time" ) func NewToken(c *gin.Context, email string) string { @@ -24,11 +24,11 @@ func NewToken(c *gin.Context, email string) string { func VerifyToken(c *gin.Context) (string, error) { authHeader := c.GetHeader("Authorization") - if !strings.HasPrefix(authHeader, "Bearer ") { - return "", errors.New("Bearer token is missing") - } + if !strings.HasPrefix(authHeader, "Bearer ") { + return "", errors.New("Bearer token is missing") + } - tokenString := strings.TrimPrefix(authHeader, "Bearer ") + tokenString := strings.TrimPrefix(authHeader, "Bearer ") if tokenString == "" { return "", errors.New("Authorization token is missing") From a58dcab50e54bb72c6e80e06e8cd91684d749513 Mon Sep 17 00:00:00 2001 From: Charles Madjeri Date: Thu, 5 Dec 2024 12:28:23 +0100 Subject: [PATCH 07/26] feat(docker): add fullstack docker setup Signed-off-by: Charles Madjeri --- .env.example | 9 +++- .gitignore | 2 + Makefile | 16 +++++- README.md | 36 +++++-------- client_mobile/.env.mobile.example | 14 ----- client_mobile/Dockerfile | 28 +++++----- client_mobile/pubspec.yaml | 2 +- client_web/Dockerfile | 58 ++++++++++++-------- client_web/nginx.conf | 7 ++- client_web/vite.config.ts | 5 -- docker-compose.yml | 88 +++++++++++++++++++++++++------ pubspec.yaml | 5 ++ server/.env.example | 7 --- server/.env.server.example | 7 +++ server/.gitignore | 2 + server/Dockerfile | 32 +++++++---- server/build/docker-compose.yml | 60 --------------------- server/init.sql | 6 +++ server/main.go | 11 +++- server/start_server.sh | 12 ----- server/stop_server.sh | 12 ----- 21 files changed, 217 insertions(+), 202 deletions(-) create mode 100644 pubspec.yaml delete mode 100644 server/.env.example create mode 100644 server/.env.server.example create mode 100644 server/.gitignore delete mode 100644 server/build/docker-compose.yml create mode 100644 server/init.sql delete mode 100755 server/start_server.sh delete mode 100755 server/stop_server.sh diff --git a/.env.example b/.env.example index b86b39a..2f46d8c 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,8 @@ -VITE_PORT=8081 \ No newline at end of file +VITE_PORT=8081 + +# Server database environment variables +DB_NAME=area +DB_PASSWORD=change-me +DB_HOST=mariadb +DB_PORT=3306 +DB_USER=root \ No newline at end of file diff --git a/.gitignore b/.gitignore index e4c9141..93ed00d 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +ssl/ diff --git a/Makefile b/Makefile index 35c5e34..f9aec34 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ TARGET_MAX_CHAR_NUM=20 .PHONY: start build stop restart reset logs clean help -PROJECT_IMAGES = area-client-web area-client-mobile +PROJECT_IMAGES = area-client-web area-client-mobile area-server mariadb rabbitmq ## Show help help: @@ -56,4 +56,16 @@ logs: ## Clean up containers, images, volumes and orphans clean: - docker compose down --rmi local -v --remove-orphans \ No newline at end of file + docker compose down --rmi local -v --remove-orphans + +# Flutter mobile client commands +flutter-build: + docker build -t flutter-app ./client_mobile + +flutter-run: + docker run -it \ + --network host \ + -v $(PWD)/client_mobile:/app \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + -e DISPLAY=${DISPLAY} \ + flutter-app \ No newline at end of file diff --git a/README.md b/README.md index fc47963..2560090 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ With AREA, you can create automated workflows that integrate various services an ## Table of Contents - [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation & Usage](#installation--usage) + - [Prerequisites](#prerequisites) + - [Installation & Usage](#installation--usage) - [Documentation](#documentation) - - [Requirements](#requirements) - - [Usage](#usage) + - [Requirements](#requirements) + - [Usage](#usage) - [Tests](#tests) - [License](#license) - [Contributors](#contributors) @@ -19,9 +19,8 @@ With AREA, you can create automated workflows that integrate various services an ### Prerequisites -- vite js -- go - docker +- make ### Installation & Usage @@ -29,40 +28,30 @@ With AREA, you can create automated workflows that integrate various services an Click to expand 1. Clone the repo + ```sh git clone git@github.com:ASM-Studios/AREA.git ``` 2. Create .env files + - Run the following command to create private env files + ```sh cp .env.example .env +cp server/.env.server.example server/.env.server cp client_web/.env.local.example .env.local cp client_mobile/.env.mobile.example .env.mobile ``` + - Fill the .env, .env.web and .env.mobile files -3. Install NPM packages -```sh -cd AREA/client-web -npm install -``` +4. Run the project -3. Install Go packages ```sh -cd AREA/server - +make start ``` -4. Run the project -```sh -cd AREA/client-web -npm run start -``` -```sh -cd AREA/server -go run ./... -``` ### Documentation @@ -87,6 +76,7 @@ The documentation is automatically built and deployed to GitHub Pages when a pus You can consult the documentation online at [AREA Documentation](https://asm-studios.github.io/AREA/). You can build the documentation locally by running the following command: + ```sh cd AREA/docs make docs diff --git a/client_mobile/.env.mobile.example b/client_mobile/.env.mobile.example index 965d1d9..e538184 100644 --- a/client_mobile/.env.mobile.example +++ b/client_mobile/.env.mobile.example @@ -1,17 +1,3 @@ -VITE_PORT=8081 -VITE_ENDPOINT=http://localhost:8080 - -VITE_GOOGLE_CLIENT_ID= -VITE_GOOGLE_CLIENT_SECRET= - -VITE_MICROSOFT_CLIENT_ID= - -VITE_LINKEDIN_CLIENT_ID= -VITE_LINKEDIN_CLIENT_SECRET= - -VITE_SPOTIFY_CLIENT_ID= -VITE_SPOTIFY_CLIENT_SECRET= - # Server URLs API_URL=http://localhost:8080 WEB_CLIENT_URL=http://localhost:8081 diff --git a/client_mobile/Dockerfile b/client_mobile/Dockerfile index 778bcff..824acdd 100644 --- a/client_mobile/Dockerfile +++ b/client_mobile/Dockerfile @@ -1,26 +1,22 @@ -FROM ghcr.io/cirruslabs/flutter:stable +FROM ghcr.io/cirruslabs/flutter:3.24.5 AS builder WORKDIR /app -ARG VITE_PORT -ARG VITE_ENDPOINT -ARG VITE_GOOGLE_CLIENT_ID -ARG VITE_GOOGLE_CLIENT_SECRET -ARG VITE_MICROSOFT_CLIENT_ID -ARG VITE_LINKEDIN_CLIENT_ID -ARG VITE_LINKEDIN_CLIENT_SECRET -ARG VITE_SPOTIFY_CLIENT_ID -ARG VITE_SPOTIFY_CLIENT_SECRET +RUN git config --system --add safe.directory /sdks/flutter && \ + git config --system --add safe.directory /app && \ + chmod -R 777 /sdks/flutter + +COPY pubspec.* ./ + +RUN flutter pub get + +COPY . . + ARG API_URL ARG WEB_CLIENT_URL ARG MOBILE_CLIENT_URL ARG GITHUB_CLIENT_ID ARG GITHUB_CLIENT_SECRET -COPY . . - -RUN flutter pub get RUN flutter build apk --release - -RUN mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/client.apk -RUN chmod -R 755 build/app/outputs/flutter-apk/ \ No newline at end of file +RUN mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/client.apk \ No newline at end of file diff --git a/client_mobile/pubspec.yaml b/client_mobile/pubspec.yaml index a5640fa..248ced5 100644 --- a/client_mobile/pubspec.yaml +++ b/client_mobile/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.4.1 <4.0.0' + sdk: '^3.5.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/client_web/Dockerfile b/client_web/Dockerfile index 2d06b21..cbc4058 100644 --- a/client_web/Dockerfile +++ b/client_web/Dockerfile @@ -1,25 +1,26 @@ ###----------------------- Certificate generation stage -----------------------### -FROM alpine:3.19 AS cert-builder +FROM alpine:3.20.3 AS cert-builder -# Install mkcert dependencies -RUN apk add --no-cache \ - curl \ - nss \ - nss-tools - -# Install mkcert -RUN curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64" \ - && chmod +x mkcert-v*-linux-amd64 \ - && mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert \ - && mkcert -install \ - && mkcert localhost +RUN apk add --no-cache openssl +RUN mkdir -p /etc/nginx/ssl && \ + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/nginx/ssl/private.key \ + -out /etc/nginx/ssl/certificate.crt \ + -subj "/C=FR/ST=Paris/L=Paris/O=Area/OU=IT/CN=localhost" \ + -addext "subjectAltName = DNS:localhost" && \ + chmod 644 /etc/nginx/ssl/certificate.crt /etc/nginx/ssl/private.key ###----------------------- Build stage for Node.js application -----------------------### -FROM node:latest AS builder +FROM node:20-alpine AS builder WORKDIR /app +COPY package*.json ./ +RUN npm ci + +COPY . . + ARG VITE_PORT ARG VITE_ENDPOINT ARG VITE_GOOGLE_CLIENT_ID @@ -35,20 +36,33 @@ ARG MOBILE_CLIENT_URL ARG GITHUB_CLIENT_ID ARG GITHUB_CLIENT_SECRET -COPY ./package*.json ./ -RUN npm install -COPY . . - RUN npm run build ###----------------------- Production stage -----------------------### -FROM nginx:alpine AS production +FROM nginx:1.25.3-alpine -COPY --from=builder /app/dist /usr/share/nginx/html -COPY ./nginx.conf /etc/nginx/conf.d/default.conf -RUN mkdir -p /usr/share/nginx/html/mobile_builds +ARG VITE_PORT + +RUN adduser -D nginxuser && \ + mkdir -p /usr/share/nginx/html/mobile_builds && \ + mkdir -p /etc/nginx/ssl && \ + chown -R nginxuser:nginxuser /usr/share/nginx/html && \ + chown -R nginxuser:nginxuser /etc/nginx/ssl && \ + chown -R nginxuser:nginxuser /var/cache/nginx && \ + chown -R nginxuser:nginxuser /var/log/nginx && \ + touch /var/run/nginx.pid && \ + chown -R nginxuser:nginxuser /var/run/nginx.pid + +COPY --from=cert-builder --chown=nginxuser:nginxuser /etc/nginx/ssl /etc/nginx/ssl +COPY --from=builder --chown=nginxuser:nginxuser /app/dist /usr/share/nginx/html +COPY --chown=nginxuser:nginxuser ./nginx.conf /etc/nginx/conf.d/default.conf + +USER nginxuser EXPOSE ${VITE_PORT} +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:${VITE_PORT}/health || exit 1 + CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/client_web/nginx.conf b/client_web/nginx.conf index 036ee9a..32e8d6e 100644 --- a/client_web/nginx.conf +++ b/client_web/nginx.conf @@ -1,7 +1,12 @@ server { - listen 8081; + listen 8081 ssl; server_name localhost; + ssl_certificate /etc/nginx/ssl/certificate.crt; + ssl_certificate_key /etc/nginx/ssl/private.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + location / { root /usr/share/nginx/html; index index.html index.htm; diff --git a/client_web/vite.config.ts b/client_web/vite.config.ts index 1371208..42b73cd 100644 --- a/client_web/vite.config.ts +++ b/client_web/vite.config.ts @@ -1,6 +1,5 @@ import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' -import fs from 'fs' import path from 'path' export default defineConfig(({ mode }) => { @@ -10,10 +9,6 @@ export default defineConfig(({ mode }) => { plugins: [react()], server: { port: parseInt(env.VITE_PORT) || 8081, - https: { - key: fs.readFileSync(path.resolve(__dirname, 'localhost-key.pem')), - cert: fs.readFileSync(path.resolve(__dirname, 'localhost.pem')), - }, }, resolve: { alias: { diff --git a/docker-compose.yml b/docker-compose.yml index f8dde58..7fa6411 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,27 +1,81 @@ services: + rabbitmq: + image: rabbitmq:4.0.4-management-alpine + ports: + - "8082:15672" + - "5000:5673" + networks: + - area_network + volumes: + - ./rabbit-mq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro + healthcheck: + test: [ "CMD", "rabbitmqctl", "status" ] + interval: 5s + timeout: 15s + retries: 5 + restart: unless-stopped + + mariadb: + image: mariadb:11.4.4 + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_NAME} + MYSQL_ROOT_HOST: "%" + volumes: + - mariadb_data:/var/lib/mysql + - ./server/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + networks: + - area_network + healthcheck: + test: [ "CMD", "mariadb-admin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD}" ] + interval: 10s + timeout: 5s + retries: 5 + env_file: + - .env + restart: unless-stopped + + area-server: + build: + context: ./server + dockerfile: Dockerfile + ports: + - "8080:8080" + volumes: + - ./ssl:/app/ssl:ro + environment: + DB_HOST: ${DB_HOST} + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_NAME: ${DB_NAME} + DB_PASSWORD: ${DB_PASSWORD} + depends_on: + mariadb: + condition: service_healthy + rabbitmq: + condition: service_healthy + networks: + - area_network + env_file: + - server/.env.server + restart: unless-stopped + area-client-mobile: build: context: ./client_mobile dockerfile: Dockerfile args: - - VITE_PORT - - VITE_ENDPOINT - - VITE_GOOGLE_CLIENT_ID - - VITE_GOOGLE_CLIENT_SECRET - - VITE_MICROSOFT_CLIENT_ID - - VITE_LINKEDIN_CLIENT_ID - - VITE_LINKEDIN_CLIENT_SECRET - - VITE_SPOTIFY_CLIENT_ID - - VITE_SPOTIFY_CLIENT_SECRET - API_URL - WEB_CLIENT_URL - MOBILE_CLIENT_URL - GITHUB_CLIENT_ID - GITHUB_CLIENT_SECRET volumes: - - area-client-data:/app/build/app/outputs/flutter-apk + - area_client_data:/app/build/app/outputs/flutter-apk networks: - - area-network + - area_network env_file: - ./client_mobile/.env.mobile @@ -47,17 +101,21 @@ services: ports: - "${VITE_PORT}:${VITE_PORT}" volumes: - - area-client-data:/usr/share/nginx/html/mobile_builds + - area_client_data:/usr/share/nginx/html/mobile_builds:ro depends_on: - area-client-mobile + - area-server networks: - - area-network + - area_network env_file: - ./client_web/.env.local + restart: unless-stopped volumes: - area-client-data: + area_client_data: + mariadb_data: networks: - area-network: + area_network: + driver: bridge diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..a268295 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,5 @@ +dependencies: + flutter: + sdk: flutter + flutter_dotenv: ^5.2.1 + go_router: ^14.6.1 \ No newline at end of file diff --git a/server/.env.example b/server/.env.example deleted file mode 100644 index 510adf3..0000000 --- a/server/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -SECRET_KEY= -DB_HOST= -DB_PORT= -DB_NAME= -DB_USER= -DB_PASSWORD= -RMQ_URL= diff --git a/server/.env.server.example b/server/.env.server.example new file mode 100644 index 0000000..d544f4a --- /dev/null +++ b/server/.env.server.example @@ -0,0 +1,7 @@ +SECRET_KEY="change-this-secret-key" +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=area +DB_USER=root +DB_PASSWORD=change-me +RMQ_URL="amqp://root:root@localhost:5672/" \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..796fa7f --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,2 @@ +.env +.env.server diff --git a/server/Dockerfile b/server/Dockerfile index cc75960..dbe5521 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,22 +1,36 @@ -FROM golang:1.23.3-alpine as builder +FROM golang:1.23.4-alpine3.20 AS builder + +RUN apk add --no-cache gcc musl-dev + WORKDIR /app -COPY . . -RUN go mod tidy +COPY go.mod go.sum ./ + RUN go mod download -RUN go get -u github.com/swaggo/swag -RUN go get -u github.com/gin-gonic/gin -RUN go build -o main . +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . -FROM debian:bullseye-slim +FROM alpine:3.20.3 + +RUN apk add --no-cache ca-certificates && \ + adduser -D appuser WORKDIR /app + COPY --from=builder /app/main . -COPY --from=builder /app/.env . COPY --from=builder /app/config.json . +COPY .env.server .env + +RUN chown -R appuser:appuser /app && \ + chmod +x /app/main -RUN chmod +x /app/main +USER appuser EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + CMD ["./main"] diff --git a/server/build/docker-compose.yml b/server/build/docker-compose.yml deleted file mode 100644 index a3aeff1..0000000 --- a/server/build/docker-compose.yml +++ /dev/null @@ -1,60 +0,0 @@ -services: - rabbitmq: - image: rabbitmq:3-management - ports: - - "8082:15672" - - "5000:5673" - networks: - - app_network - volumes: - - ./rabbit-mq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf - healthcheck: - test: [ "CMD", "rabbitmqctl", "status" ] - interval: 5s - timeout: 15s - retries: 5 - - mariadb: - image: mariadb:10.4 - ports: - - "3306:3306" - environment: - MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} - MYSQL_DATABASE: ${DB_NAME} - volumes: - - mariadb_data:/var/lib/mysql - networks: - - app_network - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - interval: 10s - timeout: 5s - retries: 5 - - go-app: - build: - context: ../. - dockerfile: Dockerfile - ports: - - "8080:8080" - environment: - DB_HOST: mariadb - DB_PORT: 3306 - DB_NAME: ${DB_NAME} - DB_USER: root - DB_PASSWORD: ${DB_PASSWORD} - SECRET_KEY: ${SECRET_KEY} - depends_on: - mariadb: - condition: service_healthy - rabbitmq: - condition: service_healthy - networks: - - app_network - -volumes: - mariadb_data: - -networks: - app_network: - driver: bridge \ No newline at end of file diff --git a/server/init.sql b/server/init.sql new file mode 100644 index 0000000..ea08740 --- /dev/null +++ b/server/init.sql @@ -0,0 +1,6 @@ +CREATE DATABASE IF NOT EXISTS area; +USE area; + +-- Grant all privileges to root user from any host +GRANT ALL PRIVILEGES ON area.* TO 'root'@'%' IDENTIFIED BY 'root'; +FLUSH PRIVILEGES; \ No newline at end of file diff --git a/server/main.go b/server/main.go index bfb0bef..cd03209 100644 --- a/server/main.go +++ b/server/main.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "log" + "net/http" "strconv" ) @@ -36,7 +37,13 @@ func main() { router := routers.SetupRouter() port := strconv.Itoa(config.AppConfig.Port) log.Printf("Starting %s on port %s in %s mode", config.AppConfig.AppName, port, config.AppConfig.GinMode) - if err := router.Run(fmt.Sprintf(":%s", port)); err != nil { - log.Fatalf("Failed to start server: %v", err) + + server := &http.Server{ + Addr: fmt.Sprintf(":%s", port), + Handler: router, + } + + if err := server.ListenAndServe(); err != nil { + log.Printf("Server error: %v", err) } } diff --git a/server/start_server.sh b/server/start_server.sh deleted file mode 100755 index 257b7b0..0000000 --- a/server/start_server.sh +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/bash - -# Start the server -COMPOSE_FILE=build/docker-compose.yml -ENV_FILE=.env - -if command -v docker-compose &> /dev/null -then - docker-compose --env-file $ENV_FILE -f $COMPOSE_FILE -p server up --build -d -else - docker compose --env-file $ENV_FILE -f $COMPOSE_FILE -p server up --build -d -fi diff --git a/server/stop_server.sh b/server/stop_server.sh deleted file mode 100755 index f703b8f..0000000 --- a/server/stop_server.sh +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/bash - -# Stop the server -COMPOSE_FILE=build/docker-compose.yml -ENV_FILE=.env - -if command -v docker-compose &> /dev/null -then - docker-compose --env-file $ENV_FILE -f $COMPOSE_FILE down --remove-orphans -else - docker compose --env-file $ENV_FILE -f $COMPOSE_FILE down --remove-orphans -fi From 8a16dace907c33d270f6492aff3759ef0b1354ac Mon Sep 17 00:00:00 2001 From: Mathieu Coulet <114529377+Djangss@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:23:32 +0100 Subject: [PATCH 08/26] Mco/ feat(about): load at startup service from file into db and give about.json as templated * feat(About) Load about from .json in services directory * feat(about): changing json * feat(about): load at startup service from file into db and give about.json as templated --- server/docs/docs.go | 131 +++++++++++++- server/docs/swagger.json | 131 +++++++++++++- server/docs/swagger.yaml | 87 ++++++++- server/internal/consts/const.go | 2 + server/internal/controllers/about.go | 107 ++++++++---- server/internal/controllers/workflow.go | 9 +- server/internal/models/about.go | 25 +++ server/internal/models/apiDTO.go | 25 +++ server/internal/models/service.go | 3 +- server/internal/pkg/about.go | 114 ++++++++++++ server/internal/pkg/db.go | 2 +- server/main.go | 14 +- server/services/discord.json | 122 +++++++++++++ server/services/google.json | 105 +++++++++++ server/services/microsoft.json | 223 ++++++++++++++++++++++++ server/services/spotify.json | 68 ++++++++ 16 files changed, 1096 insertions(+), 72 deletions(-) create mode 100644 server/internal/models/about.go create mode 100644 server/internal/models/apiDTO.go create mode 100644 server/internal/pkg/about.go create mode 100644 server/services/discord.json create mode 100644 server/services/google.json create mode 100644 server/services/microsoft.json create mode 100644 server/services/spotify.json diff --git a/server/docs/docs.go b/server/docs/docs.go index e414698..85b3dad 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -26,7 +26,12 @@ const docTemplate = `{ "paths": { "/about.json": { "get": { - "description": "about", + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information about the server", "consumes": [ "application/json" ], @@ -36,12 +41,13 @@ const docTemplate = `{ "tags": [ "about" ], - "summary": "About", + "summary": "Get information about the server", "responses": { "200": { "description": "OK", "schema": { - "type": "msg" + "type": "object", + "additionalProperties": true } } } @@ -240,6 +246,11 @@ const docTemplate = `{ }, "/workflow/create": { "post": { + "security": [ + { + "Bearer": [] + } + ], "description": "Create a new workflow", "consumes": [ "application/json" @@ -258,7 +269,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } } ], @@ -266,7 +277,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } }, "400": { @@ -283,6 +294,11 @@ const docTemplate = `{ }, "/workflow/delete/{id}": { "delete": { + "security": [ + { + "Bearer": [] + } + ], "description": "Delete a workflow by ID", "consumes": [ "application/json" @@ -345,6 +361,11 @@ const docTemplate = `{ }, "/workflow/list": { "get": { + "security": [ + { + "Bearer": [] + } + ], "description": "List all workflows", "consumes": [ "application/json" @@ -362,7 +383,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } } } @@ -371,6 +392,40 @@ const docTemplate = `{ } }, "definitions": { + "models.EventDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ParametersDTO" + } + }, + "service_id": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/models.EventType" + } + } + }, + "models.EventType": { + "type": "string", + "enum": [ + "action", + "reaction" + ], + "x-enum-varnames": [ + "ActionEventType", + "ReactionEventType" + ] + }, "models.LoginRequest": { "type": "object", "required": [ @@ -386,6 +441,23 @@ const docTemplate = `{ } } }, + "models.ParametersDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "event_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "models.RegisterRequest": { "type": "object", "required": [ @@ -405,8 +477,51 @@ const docTemplate = `{ } } }, - "models.Workflow": { - "type": "object" + "models.WorkflowDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EventDTO" + } + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.WorkflowStatus" + }, + "user_id": { + "type": "integer" + } + } + }, + "models.WorkflowStatus": { + "type": "string", + "enum": [ + "pending", + "processed", + "failed" + ], + "x-enum-varnames": [ + "WorkflowStatusPending", + "WorkflowStatusProcessed", + "WorkflowStatusFailed" + ] + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" } } }` diff --git a/server/docs/swagger.json b/server/docs/swagger.json index 6befab3..647141d 100644 --- a/server/docs/swagger.json +++ b/server/docs/swagger.json @@ -20,7 +20,12 @@ "paths": { "/about.json": { "get": { - "description": "about", + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information about the server", "consumes": [ "application/json" ], @@ -30,12 +35,13 @@ "tags": [ "about" ], - "summary": "About", + "summary": "Get information about the server", "responses": { "200": { "description": "OK", "schema": { - "type": "msg" + "type": "object", + "additionalProperties": true } } } @@ -234,6 +240,11 @@ }, "/workflow/create": { "post": { + "security": [ + { + "Bearer": [] + } + ], "description": "Create a new workflow", "consumes": [ "application/json" @@ -252,7 +263,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } } ], @@ -260,7 +271,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } }, "400": { @@ -277,6 +288,11 @@ }, "/workflow/delete/{id}": { "delete": { + "security": [ + { + "Bearer": [] + } + ], "description": "Delete a workflow by ID", "consumes": [ "application/json" @@ -339,6 +355,11 @@ }, "/workflow/list": { "get": { + "security": [ + { + "Bearer": [] + } + ], "description": "List all workflows", "consumes": [ "application/json" @@ -356,7 +377,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } } } @@ -365,6 +386,40 @@ } }, "definitions": { + "models.EventDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ParametersDTO" + } + }, + "service_id": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/models.EventType" + } + } + }, + "models.EventType": { + "type": "string", + "enum": [ + "action", + "reaction" + ], + "x-enum-varnames": [ + "ActionEventType", + "ReactionEventType" + ] + }, "models.LoginRequest": { "type": "object", "required": [ @@ -380,6 +435,23 @@ } } }, + "models.ParametersDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "event_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "models.RegisterRequest": { "type": "object", "required": [ @@ -399,8 +471,51 @@ } } }, - "models.Workflow": { - "type": "object" + "models.WorkflowDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EventDTO" + } + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.WorkflowStatus" + }, + "user_id": { + "type": "integer" + } + } + }, + "models.WorkflowStatus": { + "type": "string", + "enum": [ + "pending", + "processed", + "failed" + ], + "x-enum-varnames": [ + "WorkflowStatusPending", + "WorkflowStatusProcessed", + "WorkflowStatusFailed" + ] + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" } } } \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index 4c7f76c..d091bc0 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -1,5 +1,28 @@ basePath: / definitions: + models.EventDTO: + properties: + description: + type: string + name: + type: string + parameters: + items: + $ref: '#/definitions/models.ParametersDTO' + type: array + service_id: + type: integer + type: + $ref: '#/definitions/models.EventType' + type: object + models.EventType: + enum: + - action + - reaction + type: string + x-enum-varnames: + - ActionEventType + - ReactionEventType models.LoginRequest: properties: email: @@ -10,6 +33,17 @@ definitions: - email - password type: object + models.ParametersDTO: + properties: + description: + type: string + event_id: + type: integer + name: + type: string + type: + type: string + type: object models.RegisterRequest: properties: email: @@ -23,8 +57,33 @@ definitions: - password - username type: object - models.Workflow: + models.WorkflowDTO: + properties: + description: + type: string + events: + items: + $ref: '#/definitions/models.EventDTO' + type: array + is_active: + type: boolean + name: + type: string + status: + $ref: '#/definitions/models.WorkflowStatus' + user_id: + type: integer type: object + models.WorkflowStatus: + enum: + - pending + - processed + - failed + type: string + x-enum-varnames: + - WorkflowStatusPending + - WorkflowStatusProcessed + - WorkflowStatusFailed host: localhost:8080 info: contact: @@ -43,15 +102,18 @@ paths: get: consumes: - application/json - description: about + description: Get information about the server produces: - application/json responses: "200": description: OK schema: - type: msg - summary: About + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Get information about the server tags: - about /auth/health: @@ -190,20 +252,22 @@ paths: name: workflow required: true schema: - $ref: '#/definitions/models.Workflow' + $ref: '#/definitions/models.WorkflowDTO' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/models.Workflow' + $ref: '#/definitions/models.WorkflowDTO' "400": description: Bad Request schema: additionalProperties: type: string type: object + security: + - Bearer: [] summary: Create a workflow tags: - workflow @@ -245,6 +309,8 @@ paths: additionalProperties: type: string type: object + security: + - Bearer: [] summary: Delete a workflow tags: - workflow @@ -260,9 +326,16 @@ paths: description: OK schema: items: - $ref: '#/definitions/models.Workflow' + $ref: '#/definitions/models.WorkflowDTO' type: array + security: + - Bearer: [] summary: List workflows tags: - workflow +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/server/internal/consts/const.go b/server/internal/consts/const.go index ce21816..93c48a1 100644 --- a/server/internal/consts/const.go +++ b/server/internal/consts/const.go @@ -4,3 +4,5 @@ const EnvFile = ".env" const EnvFileDirectory = "." const MessageQueue = "message_queue" + +const ServiceFileDirectory = "./services" diff --git a/server/internal/controllers/about.go b/server/internal/controllers/about.go index d459a94..dd8043c 100644 --- a/server/internal/controllers/about.go +++ b/server/internal/controllers/about.go @@ -1,55 +1,87 @@ package controllers import ( + "AREA/internal/models" + "AREA/internal/pkg" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" "net/http" "strconv" "time" ) -type action struct { - Name string `json:"name"` - Description string `json:"description"` -} +func getServiceFromType(serviceType string, service models.Service) models.ServiceList { + if err := pkg.DB.Preload("Events", "type = ?", serviceType). + Where("id = ?", service.ID). + First(&service).Error; err != nil { + log.Error().Err(err).Msg("Failed to load service with events") + return models.ServiceList{} + } + var actions []models.Action + var reactions []models.Reaction + for _, event := range service.Events { + pkg.DB.Preload("Parameters").Find(&event) + var parameters []models.Parameter + for _, param := range event.Parameters { + parameters = append(parameters, models.Parameter{ + Name: param.Name, + Description: param.Description, + Type: param.Type, + }) + } -type reaction struct { - Name string `json:"name"` - Description string `json:"description"` -} + if serviceType == "action" { + actions = append(actions, models.Action{ + Name: event.Name, + Description: event.Description, + Parameters: parameters, + }) + } else if serviceType == "reaction" { + reactions = append(reactions, models.Reaction{ + Name: event.Name, + Description: event.Description, + Parameters: parameters, + }) + } + } -type service struct { - Name string `json:"name"` - Actions []action `json:"actions"` - Reaction []reaction `json:"reaction"` + return models.ServiceList{ + Name: service.Name, + Actions: actions, + Reaction: reactions, + } } -func getServiceList() []service { - return []service{ - { - Name: "mail", - Actions: []action{ - { - Name: "send", - Description: "send mail", - }, - }, - Reaction: []reaction{ - { - Name: "receive", - Description: "receive mail", - }, - }, - }, +func getServices() []models.ServiceList { + var services []models.Service + var serviceList []models.ServiceList + + if err := pkg.DB.Preload("Events").Find(&services).Error; err != nil { + log.Error().Err(err).Msg("Failed to load services") + return nil + } + + for _, service := range services { + actions := getServiceFromType("action", service) + reactions := getServiceFromType("reaction", service) + serviceList = append(serviceList, models.ServiceList{ + Name: service.Name, + Actions: actions.Actions, + Reaction: reactions.Reaction, + }) } + + return serviceList } // About godoc -// @Summary About -// @Description about +// @Summary Get information about the server +// @Description Get information about the server // @Tags about -// @Accept json -// @Produce json -// @Success 200 {msg} string +// @Accept json +// @Security Bearer +// @Produce json +// @Success 200 {object} map[string]interface{} // @Router /about.json [get] func About(c *gin.Context) { var msg struct { @@ -57,12 +89,13 @@ func About(c *gin.Context) { Host string `json:"host"` } `json:"client"` Server struct { - CurrentTime string `json:"current_time"` - Services []service + CurrentTime string `json:"current_time"` + Services []models.ServiceList `json:"services"` } `json:"server"` } + msg.Client.Host = c.ClientIP() msg.Server.CurrentTime = strconv.FormatInt(time.Now().Unix(), 10) - msg.Server.Services = getServiceList() + msg.Server.Services = getServices() c.JSON(http.StatusOK, msg) } diff --git a/server/internal/controllers/workflow.go b/server/internal/controllers/workflow.go index fe6ef79..e12ab8e 100644 --- a/server/internal/controllers/workflow.go +++ b/server/internal/controllers/workflow.go @@ -11,11 +11,12 @@ import ( // WorkflowCreate godoc // @Summary Create a workflow // @Description Create a new workflow +// @Security Bearer // @Tags workflow // @Accept json // @Produce json -// @Param workflow body models.Workflow true "workflow" -// @Success 200 {object} models.Workflow +// @Param workflow body models.WorkflowDTO true "workflow" +// @Success 200 {object} models.WorkflowDTO // @Failure 400 {object} map[string]string // @Router /workflow/create [post] func WorkflowCreate(c *gin.Context) { @@ -36,10 +37,11 @@ func WorkflowCreate(c *gin.Context) { // WorkflowList godoc // @Summary List workflows // @Description List all workflows +// @Security Bearer // @Tags workflow // @Accept json // @Produce json -// @Success 200 {object} []models.Workflow +// @Success 200 {object} []models.WorkflowDTO // @Router /workflow/list [get] func WorkflowList(c *gin.Context) { var workflows []models.Workflow @@ -54,6 +56,7 @@ func WorkflowList(c *gin.Context) { // WorkflowDelete godoc // @Summary Delete a workflow // @Description Delete a workflow by ID +// @Security Bearer // @Tags workflow // @Accept json // @Produce json diff --git a/server/internal/models/about.go b/server/internal/models/about.go new file mode 100644 index 0000000..f5795fa --- /dev/null +++ b/server/internal/models/about.go @@ -0,0 +1,25 @@ +package models + +type Parameter struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` +} + +type Action struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters []Parameter `json:"parameters"` +} + +type Reaction struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters []Parameter `json:"parameters"` +} + +type ServiceList struct { + Name string `json:"name"` + Actions []Action `json:"actions"` + Reaction []Reaction `json:"reactions"` +} diff --git a/server/internal/models/apiDTO.go b/server/internal/models/apiDTO.go new file mode 100644 index 0000000..07821ca --- /dev/null +++ b/server/internal/models/apiDTO.go @@ -0,0 +1,25 @@ +package models + +type WorkflowDTO struct { + UserID uint `json:"user_id"` + Name string `json:"name"` + Description string `json:"description"` + Status WorkflowStatus `json:"status"` + IsActive bool `json:"is_active"` + Events []EventDTO `json:"events"` +} + +type EventDTO struct { + Name string `json:"name"` + Description string `json:"description"` + ServiceID uint `gorm:"foreignKey:ServiceID" json:"service_id"` + Parameters []ParametersDTO `json:"parameters"` + Type EventType `gorm:"type:enum('action', 'reaction');not null" json:"type"` +} + +type ParametersDTO struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + EventID uint `gorm:"foreignKey:EventID" json:"event_id"` +} diff --git a/server/internal/models/service.go b/server/internal/models/service.go index ac74e0a..e0c3ed7 100644 --- a/server/internal/models/service.go +++ b/server/internal/models/service.go @@ -4,5 +4,6 @@ import "gorm.io/gorm" type Service struct { gorm.Model - Label string `json:"label"` + Name string `json:"name"` + Events []Event `gorm:"constraint:OnDelete:CASCADE;" json:"events"` } diff --git a/server/internal/pkg/about.go b/server/internal/pkg/about.go new file mode 100644 index 0000000..fc32461 --- /dev/null +++ b/server/internal/pkg/about.go @@ -0,0 +1,114 @@ +package pkg + +import ( + "AREA/internal/consts" + "AREA/internal/models" + "errors" + "github.com/goccy/go-json" + "gorm.io/gorm" + "io/ioutil" + "log" + "path/filepath" +) + +func InitServiceList() { + files, err := filepath.Glob(filepath.Join(consts.ServiceFileDirectory, "*.json")) + if err != nil { + log.Println("Error loading service files:", err) + return + } + + var services []models.Service + for _, file := range files { + data, err := ioutil.ReadFile(file) + if err != nil { + log.Printf("Error reading file %s: %v", file, err) + continue + } + var srv models.Service + err = json.Unmarshal(data, &srv) + if err != nil { + log.Printf("Error unmarshalling file %s: %v", file, err) + continue + } + if err := processService(&srv); err != nil { + log.Printf("Error processing service '%s': %v", srv.Name, err) + continue + } + services = append(services, srv) + } + + log.Printf("Loaded %d services at startup in db.", len(services)) +} + +func processService(srv *models.Service) error { + var existingService models.Service + err := DB.Where("name = ?", srv.Name).First(&existingService).Error + if err == nil { + return updateService(srv, &existingService) + } else if errors.Is(err, gorm.ErrRecordNotFound) { + if err := DB.Create(srv).Error; err != nil { + return err + } + log.Printf("Created new service '%s' in the database.", srv.Name) + return nil + } + return err +} + +func updateService(newService *models.Service, existingService *models.Service) error { + for _, newEvent := range newService.Events { + if err := processEvent(newEvent, existingService.ID); err != nil { + log.Printf("Error processing event '%s': %v", newEvent.Name, err) + } + } + + log.Printf("Updated existing service '%s' in the database.", existingService.Name) + return DB.Save(existingService).Error +} + +func processEvent(newEvent models.Event, serviceID uint) error { + var existingEvent models.Event + + err := DB.Where("name = ? AND service_id = ?", newEvent.Name, serviceID).First(&existingEvent).Error + if err == nil { + return updateEvent(newEvent, &existingEvent) + } else if errors.Is(err, gorm.ErrRecordNotFound) { + newEvent.ServiceID = serviceID + if err := DB.Create(&newEvent).Error; err != nil { + return err + } + log.Printf("Created new event '%s' in the database.", newEvent.Name) + return nil + } + return err +} + +func updateEvent(newEvent models.Event, existingEvent *models.Event) error { + for _, newParam := range newEvent.Parameters { + if err := processParameter(newParam, existingEvent.ID); err != nil { + log.Printf("Error processing parameter '%s': %v", newParam.Name, err) + } + } + existingEvent.Description = newEvent.Description + existingEvent.Type = newEvent.Type + return DB.Save(existingEvent).Error +} + +func processParameter(newParam models.Parameters, eventID uint) error { + var existingParam models.Parameters + err := DB.Where("name = ? AND event_id = ?", newParam.Name, eventID).First(&existingParam).Error + if err == nil { + existingParam.Description = newParam.Description + existingParam.Type = newParam.Type + return DB.Save(&existingParam).Error + } else if errors.Is(err, gorm.ErrRecordNotFound) { + newParam.EventID = eventID + if err := DB.Create(&newParam).Error; err != nil { + return err + } + log.Printf("Created new parameter '%s' in the database.", newParam.Name) + return nil + } + return err +} diff --git a/server/internal/pkg/db.go b/server/internal/pkg/db.go index 47f6b1e..56ec36e 100644 --- a/server/internal/pkg/db.go +++ b/server/internal/pkg/db.go @@ -15,9 +15,9 @@ func migrateDB() error { err := DB.AutoMigrate( &models.User{}, &models.Workflow{}, + &models.Service{}, &models.Event{}, &models.Parameters{}, - &models.Service{}, ) if err != nil { log.Fatalf("Failed to migrate DB: %v", err) diff --git a/server/main.go b/server/main.go index bfb0bef..55dc9db 100644 --- a/server/main.go +++ b/server/main.go @@ -2,8 +2,7 @@ package main import ( "AREA/internal/config" - "AREA/internal/models" - db "AREA/internal/pkg" + "AREA/internal/pkg" "AREA/internal/routers" "fmt" "github.com/gin-gonic/gin" @@ -25,13 +24,14 @@ import ( // @host localhost:8080 // @BasePath / + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization func main() { config.LoadConfig() - db.InitDB() - err := db.DB.AutoMigrate(&models.User{}) - if err != nil { - return - } + pkg.InitDB() + pkg.InitServiceList() gin.SetMode(config.AppConfig.GinMode) router := routers.SetupRouter() port := strconv.Itoa(config.AppConfig.Port) diff --git a/server/services/discord.json b/server/services/discord.json new file mode 100644 index 0000000..c2363f0 --- /dev/null +++ b/server/services/discord.json @@ -0,0 +1,122 @@ +{ + "name": "discord", + "events": [ + { + "name": "A new message is sent", + "description": "Triggered when a new message is sent in a Discord channel.", + "type" : "action", + "parameters": [ + { + "name": "channel_id", + "description": "The ID of the Discord channel where the message was sent.", + "type": "string" + }, + { + "name": "message_content", + "description": "The content of the message.", + "type": "string" + }, + { + "name": "author_id", + "description": "The ID of the user who sent the message.", + "type": "string" + } + ] + }, + { + "name": "A message is deleted", + "description": "Triggered when a message is deleted in a Discord channel.", + "type" : "action", + "parameters": [ + { + "name": "channel_id", + "description": "The ID of the Discord channel where the message was deleted.", + "type": "string" + }, + { + "name": "message_id", + "description": "The ID of the deleted message.", + "type": "string" + } + ] + }, + { + "name": "A message is edited", + "description": "Triggered when a message is edited in a Discord channel.", + "type" : "action", + "parameters": [ + { + "name": "channel_id", + "description": "The ID of the Discord channel where the message was edited.", + "type": "string" + }, + { + "name": "message_id", + "description": "The ID of the edited message.", + "type": "string" + }, + { + "name": "new_content", + "description": "The new content of the edited message.", + "type": "string" + } + ] + }, + { + "name": "Send a message in a channel", + "description": "Send a message to a specific Discord channel.", + "type" : "reaction", + "parameters": [ + { + "name": "channel_id", + "description": "The ID of the Discord channel where the message should be sent.", + "type": "string" + }, + { + "name": "message_content", + "description": "The content of the message to send.", + "type": "string" + } + ] + }, + { + "name": "Delete a message", + "description": "Delete a specific message in a Discord channel.", + "type" : "reaction", + "parameters": [ + { + "name": "channel_id", + "description": "The ID of the Discord channel where the message should be deleted.", + "type": "string" + }, + { + "name": "message_id", + "description": "The ID of the message to delete.", + "type": "string" + } + ] + }, + { + "name": "Pin/Unpin a message", + "description": "Pin or unpin a specific message in a Discord channel.", + "type" : "reaction", + "parameters": [ + { + "name": "channel_id", + "description": "The ID of the Discord channel where the message should be pinned or unpinned.", + "type": "string" + }, + { + "name": "message_id", + "description": "The ID of the message to pin or unpin.", + "type": "string" + }, + { + "name": "is_pinned", + "description": "Set to true to pin the message, or false to unpin it.", + "type": "bool" + } + ] + } + ] +} diff --git a/server/services/google.json b/server/services/google.json new file mode 100644 index 0000000..c150222 --- /dev/null +++ b/server/services/google.json @@ -0,0 +1,105 @@ +{ + "name": "google", + "events": [ + { + "name": "Email Received", + "description": "Triggered when a new email is received in your Gmail inbox.", + "type": "action", + "parameters": [ + { + "name": "sender_email", + "description": "The email address of the sender.", + "type": "string" + }, + { + "name": "subject", + "description": "The subject of the email.", + "type": "string" + }, + { + "name": "received_date", + "description": "The date and time when the email was received.", + "type": "string" + } + ] + }, + { + "name": "Email Sent", + "description": "Triggered when an email is sent from your Gmail account.", + "type": "action", + "parameters": [ + { + "name": "recipient_email", + "description": "The email address of the recipient.", + "type": "string" + }, + { + "name": "subject", + "description": "The subject of the sent email.", + "type": "string" + }, + { + "name": "sent_date", + "description": "The date and time when the email was sent.", + "type": "string" + } + ] + }, + { + "name": "Send an Email", + "description": "Send an email using your Gmail account.", + "type": "reaction", + "parameters": [ + { + "name": "recipient_email", + "description": "The email address of the recipient.", + "type": "string" + }, + { + "name": "subject", + "description": "The subject of the email.", + "type": "string" + }, + { + "name": "body", + "description": "The content of the email.", + "type": "string" + } + ] + }, + { + "name": "Reply to an Email", + "description": "Reply to a specific email in your Gmail inbox.", + "type": "reaction", + "parameters": [ + { + "name": "email_id", + "description": "The unique ID of the email to reply to.", + "type": "string" + }, + { + "name": "reply_body", + "description": "The content of the reply.", + "type": "string" + } + ] + }, + { + "name": "Marked as read/unread", + "description": "Mark an email as read or unread in your Gmail inbox.", + "type": "reaction", + "parameters": [ + { + "name": "email_id", + "description": "The unique ID of the email to mark.", + "type": "string" + }, + { + "name": "is_read", + "description": "Set to true to mark as read, or false to mark as unread.", + "type": "bool" + } + ] + } + ] +} diff --git a/server/services/microsoft.json b/server/services/microsoft.json new file mode 100644 index 0000000..35c7870 --- /dev/null +++ b/server/services/microsoft.json @@ -0,0 +1,223 @@ +{ + "name": "microsoft", + "events": [ + { + "name": "New Email Received", + "description": "Triggered when a new email is received in your mailbox.", + "type": "action", + "parameters": [] + }, + { + "name": "Email Sent", + "description": "Triggered when an email is sent from your mailbox.", + "type": "action", + "parameters": [] + }, + { + "name": "New message in channel", + "description": "Triggered when a new message is posted in a Teams channel.", + "type": "action", + "parameters": [ + { + "name": "channel_id", + "description": "ID of the Teams channel.", + "type": "string" + } + ] + }, + { + "name": "New channel created", + "description": "Triggered when a new channel is created in Teams.", + "type": "action", + "parameters": [ + { + "name": "team_id", + "description": "ID of the Teams team.", + "type": "string" + } + ] + }, + { + "name": "File uploaded", + "description": "Triggered when a new file is uploaded to OneDrive.", + "type": "action", + "parameters": [ + { + "name": "folder_path", + "description": "Path of the folder where the file is uploaded.", + "type": "string" + } + ] + }, + { + "name": "File updated", + "description": "Triggered when a file is updated in OneDrive.", + "type": "action", + "parameters": [ + { + "name": "file_id", + "description": "ID of the updated file.", + "type": "string" + } + ] + }, + { + "name": "File deleted", + "description": "Triggered when a file is deleted from OneDrive.", + "type": "action", + "parameters": [ + { + "name": "file_id", + "description": "ID of the deleted file.", + "type": "string" + } + ] + }, + { + "name": "Send an Email", + "description": "Send an email from your mailbox.", + "type": "reaction", + "parameters": [ + { + "name": "recipient", + "description": "Email address of the recipient.", + "type": "string" + }, + { + "name": "subject", + "description": "Subject of the email.", + "type": "string" + }, + { + "name": "body", + "description": "Content of the email body.", + "type": "string" + } + ] + }, + { + "name": "Mark Email as read/unread", + "description": "Mark an email as read or unread.", + "type": "reaction", + "parameters": [ + { + "name": "email_id", + "description": "ID of the email to mark.", + "type": "string" + }, + { + "name": "is_read", + "description": "Set to true to mark as read, false to mark as unread.", + "type": "bool" + } + ] + }, + { + "name": "Send a message to a channel", + "description": "Send a message to a Teams channel.", + "type": "reaction", + "parameters": [ + { + "name": "channel_id", + "description": "ID of the Teams channel.", + "type": "string" + }, + { + "name": "message", + "description": "Content of the message.", + "type": "string" + } + ] + }, + { + "name": "Create a new channel", + "description": "Create a new channel in a Teams team.", + "type": "reaction", + "parameters": [ + { + "name": "team_id", + "description": "ID of the Teams team.", + "type": "string" + }, + { + "name": "channel_name", + "description": "Name of the new channel.", + "type": "string" + } + ] + }, + { + "name": "Add a team member", + "description": "Add a member to a Teams team.", + "type": "reaction", + "parameters": [ + { + "name": "team_id", + "description": "ID of the Teams team.", + "type": "string" + }, + { + "name": "user_email", + "description": "Email of the user to add.", + "type": "string" + } + ] + }, + { + "name": "Upload a file", + "description": "Upload a file to OneDrive.", + "type": "reaction", + "parameters": [ + { + "name": "folder_path", + "description": "Path to the folder where the file will be uploaded.", + "type": "string" + }, + { + "name": "file_name", + "description": "Name of the file to upload.", + "type": "string" + }, + { + "name": "file_content", + "description": "Content of the file.", + "type": "string" + } + ] + }, + { + "name": "Update file metadata", + "description": "Update the metadata of a file in OneDrive.", + "type": "reaction", + "parameters": [ + { + "name": "file_id", + "description": "ID of the file to update.", + "type": "string" + }, + { + "name": "metadata", + "description": "New metadata for the file.", + "type": "map[string]string" + } + ] + }, + { + "name": "Create a folder", + "description": "Create a new folder in OneDrive.", + "type": "reaction", + "parameters": [ + { + "name": "parent_folder_path", + "description": "Path of the parent folder.", + "type": "string" + }, + { + "name": "folder_name", + "description": "Name of the new folder.", + "type": "string" + } + ] + } + ] +} diff --git a/server/services/spotify.json b/server/services/spotify.json new file mode 100644 index 0000000..6f8c0e4 --- /dev/null +++ b/server/services/spotify.json @@ -0,0 +1,68 @@ +{ + "name": "spotify", + "events": [ + { + "name": "A track start playing", + "description": "Triggered when a track starts playing on Spotify.", + "type": "action", + "parameters": [ + { + "name": "track_id", + "description": "The ID of the track that starts playing.", + "type": "string" + }, + { + "name": "device_id", + "description": "The ID of the device playing the track.", + "type": "string" + } + ] + }, + { + "name": "A track is paused/stopped", + "description": "Triggered when a track is paused or stopped on Spotify.", + "type": "action", + "parameters": [ + { + "name": "track_id", + "description": "The ID of the track that is paused or stopped.", + "type": "string" + }, + { + "name": "device_id", + "description": "The ID of the device where the track is paused or stopped.", + "type": "string" + } + ] + }, + { + "name": "Pause a track", + "description": "Pause the currently playing track on Spotify.", + "type": "reaction", + "parameters": [ + { + "name": "device_id", + "description": "The ID of the device where the track should be paused.", + "type": "string" + } + ] + }, + { + "name": "Add track to a playlist", + "description": "Add a track to a specific Spotify playlist.", + "type": "reaction", + "parameters": [ + { + "name": "track_id", + "description": "The ID of the track to add.", + "type": "string" + }, + { + "name": "playlist_id", + "description": "The ID of the playlist to which the track should be added.", + "type": "string" + } + ] + } + ] +} From e6852c913930ea250a12ff72b4e831cf0c3c366e Mon Sep 17 00:00:00 2001 From: Charles Madjeri Date: Sun, 8 Dec 2024 14:37:02 +0100 Subject: [PATCH 09/26] feat(docker): add prod and dev mode for web client with dev hot reload Signed-off-by: Charles Madjeri --- .env.example | 1 + Makefile | 4 ++++ client_web/Dockerfile | 47 ++++++++++++++++++++----------------- client_web/package.json | 2 +- client_web/vite.config.ts | 9 +++++++ docker-compose.override.yml | 14 +++++++++++ docker-compose.prod.yml | 6 +++++ docker-compose.yml | 1 - pubspec.yaml | 5 ---- 9 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 docker-compose.override.yml create mode 100644 docker-compose.prod.yml delete mode 100644 pubspec.yaml diff --git a/.env.example b/.env.example index 2f46d8c..09afcd6 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ +NODE_ENV=development # development or production VITE_PORT=8081 # Server database environment variables diff --git a/Makefile b/Makefile index f9aec34..9ebdbd5 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,10 @@ help: start: docker compose up -d +## Start containers in detached mode for production +start-prod: + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d + ## Build and start containers in detached mode build: docker compose up --build -d diff --git a/client_web/Dockerfile b/client_web/Dockerfile index cbc4058..3223c34 100644 --- a/client_web/Dockerfile +++ b/client_web/Dockerfile @@ -10,37 +10,23 @@ RUN mkdir -p /etc/nginx/ssl && \ -addext "subjectAltName = DNS:localhost" && \ chmod 644 /etc/nginx/ssl/certificate.crt /etc/nginx/ssl/private.key - -###----------------------- Build stage for Node.js application -----------------------### +###----------------------- Build stage -----------------------### FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ -RUN npm ci -COPY . . +# Development or Production installation based on NODE_ENV +RUN if [ "$NODE_ENV" = "production" ] ; then npm ci ; else npm install ; fi -ARG VITE_PORT -ARG VITE_ENDPOINT -ARG VITE_GOOGLE_CLIENT_ID -ARG VITE_GOOGLE_CLIENT_SECRET -ARG VITE_MICROSOFT_CLIENT_ID -ARG VITE_LINKEDIN_CLIENT_ID -ARG VITE_LINKEDIN_CLIENT_SECRET -ARG VITE_SPOTIFY_CLIENT_ID -ARG VITE_SPOTIFY_CLIENT_SECRET -ARG API_URL -ARG WEB_CLIENT_URL -ARG MOBILE_CLIENT_URL -ARG GITHUB_CLIENT_ID -ARG GITHUB_CLIENT_SECRET - -RUN npm run build +COPY . . +# Build only if in production +RUN if [ "$NODE_ENV" = "production" ] ; then npm run build ; fi ###----------------------- Production stage -----------------------### -FROM nginx:1.25.3-alpine +FROM nginx:1.25.3-alpine AS production ARG VITE_PORT @@ -65,4 +51,21 @@ EXPOSE ${VITE_PORT} HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:${VITE_PORT}/health || exit 1 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] + +###----------------------- Development stage -----------------------### +FROM builder AS development + +# Install mkcert and its dependencies +RUN apk add --no-cache \ + nss-tools \ + curl \ + && curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64" \ + && chmod +x mkcert-v*-linux-amd64 \ + && cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert + +ENV NODE_ENV=development +EXPOSE ${VITE_PORT} + +# Create and move certificates to the correct location +CMD ["sh", "-c", "if [ ! -f localhost-key.pem ]; then mkcert -install && mkcert localhost; fi && mv localhost*.pem /app/ && npm run dev -- --host"] \ No newline at end of file diff --git a/client_web/package.json b/client_web/package.json index 8013ac0..a2002d1 100644 --- a/client_web/package.json +++ b/client_web/package.json @@ -6,7 +6,7 @@ "main": "electron.js", "scripts": { "start": "mkcert -install && mkcert localhost && vite", - "dev": "mkcert -install && mkcert localhost && vite", + "dev": "vite --host", "production": "vite --mode production", "build": "tsc -b && vite build", "lint": "eslint .", diff --git a/client_web/vite.config.ts b/client_web/vite.config.ts index 42b73cd..4f1df54 100644 --- a/client_web/vite.config.ts +++ b/client_web/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' +import fs from 'fs' export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()) @@ -9,6 +10,14 @@ export default defineConfig(({ mode }) => { plugins: [react()], server: { port: parseInt(env.VITE_PORT) || 8081, + watch: { + usePolling: true, + }, + host: true, + https: { + key: fs.readFileSync('localhost-key.pem'), + cert: fs.readFileSync('localhost.pem'), + }, }, resolve: { alias: { diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..cda3e7e --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,14 @@ +services: + area-client-web: + build: + target: development + volumes: + - ./client_web:/app + - /app/node_modules + - ssl-certs:/app/certs + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + +volumes: + ssl-certs: \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..04315d6 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,6 @@ +services: + area-client-web: + build: + target: production + environment: + - NODE_ENV=production \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7fa6411..0d1aa8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,7 +115,6 @@ volumes: area_client_data: mariadb_data: - networks: area_network: driver: bridge diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100644 index a268295..0000000 --- a/pubspec.yaml +++ /dev/null @@ -1,5 +0,0 @@ -dependencies: - flutter: - sdk: flutter - flutter_dotenv: ^5.2.1 - go_router: ^14.6.1 \ No newline at end of file From cd77760e67bf01db087ac28322db001b754ad0b6 Mon Sep 17 00:00:00 2001 From: Charles Madjeri Date: Sun, 8 Dec 2024 15:52:31 +0100 Subject: [PATCH 10/26] feat(docker): add prod and dev mode for server with dev hot reload Signed-off-by: Charles Madjeri --- Makefile | 14 +------------- client_mobile/Dockerfile | 31 ++++++++++++++++++++++++++----- client_web/Dockerfile | 4 ---- docker-compose.override.yml | 12 +++++++++++- docker-compose.prod.yml | 8 +++++++- server/.air.toml | 26 ++++++++++++++++++++++++++ server/.gitignore | 2 ++ server/Dockerfile | 28 +++++++++++++++++++++++----- 8 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 server/.air.toml diff --git a/Makefile b/Makefile index 9ebdbd5..7a5ee1f 100644 --- a/Makefile +++ b/Makefile @@ -60,16 +60,4 @@ logs: ## Clean up containers, images, volumes and orphans clean: - docker compose down --rmi local -v --remove-orphans - -# Flutter mobile client commands -flutter-build: - docker build -t flutter-app ./client_mobile - -flutter-run: - docker run -it \ - --network host \ - -v $(PWD)/client_mobile:/app \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e DISPLAY=${DISPLAY} \ - flutter-app \ No newline at end of file + docker compose down --rmi local -v --remove-orphans \ No newline at end of file diff --git a/client_mobile/Dockerfile b/client_mobile/Dockerfile index 824acdd..4898004 100644 --- a/client_mobile/Dockerfile +++ b/client_mobile/Dockerfile @@ -2,15 +2,28 @@ FROM ghcr.io/cirruslabs/flutter:3.24.5 AS builder WORKDIR /app -RUN git config --system --add safe.directory /sdks/flutter && \ - git config --system --add safe.directory /app && \ - chmod -R 777 /sdks/flutter +# Create a non-root user and give permissions to Android SDK directory +RUN useradd -m -d /home/flutteruser -s /bin/bash flutteruser && \ + chown -R flutteruser:flutteruser /app && \ + chown -R flutteruser:flutteruser /sdks/flutter && \ + chown -R flutteruser:flutteruser /opt/android-sdk-linux && \ + mkdir -p /opt/android-sdk-linux/licenses && \ + chown -R flutteruser:flutteruser /opt/android-sdk-linux/licenses -COPY pubspec.* ./ +USER flutteruser + +RUN git config --global --add safe.directory /sdks/flutter && \ + git config --global --add safe.directory /app + +# Ensure Flutter is properly set up +RUN flutter doctor -v && \ + flutter config --no-analytics + +COPY --chown=flutteruser:flutteruser pubspec.* ./ RUN flutter pub get -COPY . . +COPY --chown=flutteruser:flutteruser . . ARG API_URL ARG WEB_CLIENT_URL @@ -18,5 +31,13 @@ ARG MOBILE_CLIENT_URL ARG GITHUB_CLIENT_ID ARG GITHUB_CLIENT_SECRET +# Accept Android licenses and install SDK components +RUN yes | sdkmanager --licenses && \ + sdkmanager "build-tools;30.0.3" + +# Clean and get dependencies again after copying all files +RUN flutter clean && \ + flutter pub get + RUN flutter build apk --release RUN mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/client.apk \ No newline at end of file diff --git a/client_web/Dockerfile b/client_web/Dockerfile index 3223c34..4448ccf 100644 --- a/client_web/Dockerfile +++ b/client_web/Dockerfile @@ -17,12 +17,10 @@ WORKDIR /app COPY package*.json ./ -# Development or Production installation based on NODE_ENV RUN if [ "$NODE_ENV" = "production" ] ; then npm ci ; else npm install ; fi COPY . . -# Build only if in production RUN if [ "$NODE_ENV" = "production" ] ; then npm run build ; fi ###----------------------- Production stage -----------------------### @@ -56,7 +54,6 @@ CMD ["nginx", "-g", "daemon off;"] ###----------------------- Development stage -----------------------### FROM builder AS development -# Install mkcert and its dependencies RUN apk add --no-cache \ nss-tools \ curl \ @@ -67,5 +64,4 @@ RUN apk add --no-cache \ ENV NODE_ENV=development EXPOSE ${VITE_PORT} -# Create and move certificates to the correct location CMD ["sh", "-c", "if [ ! -f localhost-key.pem ]; then mkcert -install && mkcert localhost; fi && mv localhost*.pem /app/ && npm run dev -- --host"] \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml index cda3e7e..ad43493 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -10,5 +10,15 @@ services: - NODE_ENV=development - CHOKIDAR_USEPOLLING=true + area-server: + build: + target: development + volumes: + - ./server:/app + - go-modules:/go/pkg/mod + environment: + - GO_ENV=development + volumes: - ssl-certs: \ No newline at end of file + ssl-certs: + go-modules: \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 04315d6..849844e 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -3,4 +3,10 @@ services: build: target: production environment: - - NODE_ENV=production \ No newline at end of file + - NODE_ENV=production + + area-server: + build: + target: production + environment: + - GO_ENV=production diff --git a/server/.air.toml b/server/.air.toml new file mode 100644 index 0000000..40a26d8 --- /dev/null +++ b/server/.air.toml @@ -0,0 +1,26 @@ +root = "." +tmp_dir = "tmp" + +[build] +cmd = "go build -o ./tmp/main ." +bin = "./tmp/main" +full_bin = "./tmp/main" +include_ext = ["go", "tpl", "tmpl", "html"] +exclude_dir = ["assets", "tmp", "vendor"] +include_dir = [] +exclude_file = [] +delay = 1000 +stop_on_error = true +log = "air_errors.log" + +[log] +time = true + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index 796fa7f..ad909a5 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,2 +1,4 @@ .env .env.server +tmp/ +air_errors.log diff --git a/server/Dockerfile b/server/Dockerfile index dbe5521..df2d2ae 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,6 +1,21 @@ -FROM golang:1.23.4-alpine3.20 AS builder +# Development stage +FROM golang:1.23.4-alpine AS development -RUN apk add --no-cache gcc musl-dev +WORKDIR /app + +RUN go install github.com/air-verse/air@latest + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +EXPOSE 8080 + +CMD ["air", "-c", ".air.toml"] + +# Production stage +FROM golang:1.23.4-alpine AS production WORKDIR /app @@ -10,8 +25,11 @@ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . +RUN CGO_ENABLED=0 GOOS=linux go build -o main . + +CMD ["./main"] +# Final stage FROM alpine:3.20.3 RUN apk add --no-cache ca-certificates && \ @@ -19,8 +37,8 @@ RUN apk add --no-cache ca-certificates && \ WORKDIR /app -COPY --from=builder /app/main . -COPY --from=builder /app/config.json . +COPY --from=production /app/main . +COPY --from=production /app/config.json . COPY .env.server .env RUN chown -R appuser:appuser /app && \ From 3082b1f97345740ab0741d99b2e021d164519c26 Mon Sep 17 00:00:00 2001 From: Mael-RABOT Date: Mon, 9 Dec 2024 12:55:59 +0100 Subject: [PATCH 11/26] evol(about): add ids to paylaod Signed-off-by: Mael-RABOT --- .../Components/Auth/Buttons/GoogleAuth.tsx | 15 ++-- client_web/src/Components/Layout/Header.tsx | 28 ++++++- .../src/Pages/Workflows/CreateWorkflow.tsx | 74 ++++++++++++------- client_web/src/types.ts | 13 +++- 4 files changed, 92 insertions(+), 38 deletions(-) diff --git a/client_web/src/Components/Auth/Buttons/GoogleAuth.tsx b/client_web/src/Components/Auth/Buttons/GoogleAuth.tsx index ae8b42e..d3152a7 100644 --- a/client_web/src/Components/Auth/Buttons/GoogleAuth.tsx +++ b/client_web/src/Components/Auth/Buttons/GoogleAuth.tsx @@ -1,6 +1,5 @@ import { Form, Button } from 'antd'; import { GoogleOAuthProvider } from '@react-oauth/google'; -// @ts-ignore import { uri } from '@Config/uri'; interface GoogleAuthProps { @@ -15,14 +14,18 @@ const GoogleAuth = ({ onSuccess, onError, buttonText }: GoogleAuthProps) => { } const handleGoogleLogin = () => { - // Initialize the Google Sign-In client - let google: any; + const google = (window as any).google; + if (!google) { + console.error('Google API not loaded'); + onError(); + return; + } + const client = google.accounts.oauth2.initCodeClient({ client_id: uri.google.auth.clientId, - scope: 'email profile', // TODO: Add other scopes as needed + scope: 'email profile', callback: (response: unknown) => { - // @ts-ignore - if (response?.code) { + if (response && typeof response === 'object' && 'code' in response) { onSuccess(response); } else { onError(); diff --git a/client_web/src/Components/Layout/Header.tsx b/client_web/src/Components/Layout/Header.tsx index ec3c7f1..6aefa9e 100644 --- a/client_web/src/Components/Layout/Header.tsx +++ b/client_web/src/Components/Layout/Header.tsx @@ -1,6 +1,6 @@ -import { Layout, Menu } from 'antd'; +import { Layout, Menu, Button } from 'antd'; import { Link, useLocation } from 'react-router-dom'; -import { useTheme } from '@/Context/ContextHooks'; +import { useAuth, useTheme } from '@/Context/ContextHooks'; import React, { useEffect, useState } from "react"; const { Header: AntHeader } = Layout; @@ -20,13 +20,22 @@ const Header: React.FC = () => { const location = useLocation(); const [selectedKey, setSelectedKey] = useState(location.pathname); + const { setIsAuthenticated, setJsonWebToken } = useAuth(); + useEffect(() => { setSelectedKey(location.pathname); }, [location.pathname]); + function handleLogout() { + localStorage.removeItem('jsonWebToken'); + setIsAuthenticated(false); + setJsonWebToken(''); + window.location.href = '/'; + } + return (
- + { items={menuItems} selectedKeys={[selectedKey]} /> +
); diff --git a/client_web/src/Pages/Workflows/CreateWorkflow.tsx b/client_web/src/Pages/Workflows/CreateWorkflow.tsx index f598472..baf68db 100644 --- a/client_web/src/Pages/Workflows/CreateWorkflow.tsx +++ b/client_web/src/Pages/Workflows/CreateWorkflow.tsx @@ -4,23 +4,13 @@ import Security from "@/Components/Security"; import LinkButton from "@/Components/LinkButton"; import { normalizeName } from "@/Pages/Workflows/CreateWorkflow.utils"; -// Import types correctly -import { About, Service, Action, Reaction, Workflow, Parameter } from "@/types"; -import {toast} from "react-toastify"; -import {instanceWithAuth} from "@/Config/backend.routes"; -import {workflow as workflowRoute} from "@/Config/backend.routes"; +import { About, Service, Action, Reaction, Workflow, Parameter, SelectedAction, SelectedReaction } from "@/types"; +import { toast } from "react-toastify"; +import { instanceWithAuth, workflow as workflowRoute } from "@/Config/backend.routes"; const { Title, Text } = Typography; const { Panel } = Collapse; -interface SelectedAction extends Action { - id: string; -} - -interface SelectedReaction extends Reaction { - id: string; -} - const _data: About = { // Fixtures client: { host: "10.101.53.35" @@ -29,14 +19,17 @@ const _data: About = { // Fixtures current_time: 1531680780, services: [ { + id: 1, name: "facebook", actions: [ { + id: 1, name: "new_message_in_group_w/o", description: "A new message is posted in the group", parameters: [] }, { + id: 2, name: "new_message_inbox", description: "A new private message is received by the user", parameters: [ @@ -44,6 +37,7 @@ const _data: About = { // Fixtures ] }, { + id: 3, name: "new_like_w/o", description: "The user gains a like from one of their messages", parameters: [] @@ -51,6 +45,7 @@ const _data: About = { // Fixtures ], reactions: [ { + id: 4, name: "like_message", description: "The user likes a message", parameters: [ @@ -60,9 +55,11 @@ const _data: About = { // Fixtures ] }, { + id: 2, name: "twitter", actions: [ { + id: 5, name: "new_tweet", description: "A new tweet is posted", parameters: [ @@ -70,6 +67,7 @@ const _data: About = { // Fixtures ] }, { + id: 6, name: "new_follower_w/o", description: "The user gains a new follower", parameters: [] @@ -77,6 +75,7 @@ const _data: About = { // Fixtures ], reactions: [ { + id: 7, name: "retweet", description: "The user retweets a tweet", parameters: [ @@ -84,6 +83,7 @@ const _data: About = { // Fixtures ] }, { + id: 8, name: "like_tweet_w/o", description: "The user likes a tweet", parameters: [] @@ -91,9 +91,11 @@ const _data: About = { // Fixtures ] }, { + id: 3, name: "github", actions: [ { + id: 9, name: "new_issue", description: "A new issue is created in a repository", parameters: [ @@ -101,6 +103,7 @@ const _data: About = { // Fixtures ] }, { + id: 10, name: "new_pull_request_w/o", description: "A new pull request is created in a repository", parameters: [] @@ -108,6 +111,7 @@ const _data: About = { // Fixtures ], reactions: [ { + id: 11, name: "create_issue", description: "The user creates a new issue", parameters: [ @@ -116,6 +120,7 @@ const _data: About = { // Fixtures ] }, { + id: 12, name: "merge_pull_request_w/o", description: "The user merges a pull request", parameters: [] @@ -123,9 +128,11 @@ const _data: About = { // Fixtures ] }, { + id: 4, name: "slack", actions: [ { + id: 13, name: "new_message", description: "A new message is posted in a channel", parameters: [ @@ -134,6 +141,7 @@ const _data: About = { // Fixtures ] }, { + id: 14, name: "new_reaction_w/o", description: "A new reaction is added to a message", parameters: [] @@ -141,6 +149,7 @@ const _data: About = { // Fixtures ], reactions: [ { + id: 15, name: "send_message", description: "The user sends a message to a channel", parameters: [ @@ -149,6 +158,7 @@ const _data: About = { // Fixtures ] }, { + id: 16, name: "add_reaction_w/o", description: "The user adds a reaction to a message", parameters: [] @@ -184,16 +194,15 @@ const CreateWorkflow: React.FC = () => { const parameters = action.parameters?.length ? action.parameters.reduce((acc: Record, param: Parameter) => ({...acc, [param.name]: ''}), {}) - : undefined; + : []; - // @ts-ignore setSelectedActions(prev => [ ...prev, { - id: `${action.name}-${Date.now()}`, + id: Number(action.id), name: action.name, description: action.description, - parameters + parameters: parameters as Record } ]); }; @@ -206,16 +215,15 @@ const CreateWorkflow: React.FC = () => { const parameters = reaction.parameters?.length ? reaction.parameters.reduce((acc: Record, param: Parameter) => ({...acc, [param.name]: ''}), {}) - : undefined; + : []; - // @ts-ignore setSelectedReactions(prev => [ ...prev, { - id: `${reaction.name}-${Date.now()}`, + id: reaction.id, name: reaction.name, description: reaction.description, - parameters + parameters: parameters as Record } ]); }; @@ -223,13 +231,11 @@ const CreateWorkflow: React.FC = () => { const areAllParametersFilled = () => { const actionsComplete = selectedActions.every(action => { if (!action.parameters) return true; - // @ts-ignore return Object.values(action.parameters).every(value => value !== ''); }); const reactionsComplete = selectedReactions.every(reaction => { if (!reaction.parameters) return true; - // @ts-ignore return Object.values(reaction.parameters).every(value => value !== ''); }); @@ -240,17 +246,28 @@ const CreateWorkflow: React.FC = () => { const workflow: Workflow = { name: workflowName, description: workflowDescription, - service: about?.server.services.find(service => - service.actions.some(action => action.name === selectedActions[0]?.name) - )?.name ?? "unknown", - events: - [ + services: [...new Set([ + ...selectedActions.map(action => { + const service = about?.server.services.find(s => + s.actions.some(a => a.name === action.name) + ); + return service?.id; + }), + ...selectedReactions.map(reaction => { + const service = about?.server.services.find(s => + s.reactions.some(r => r.name === reaction.name) + ); + return service?.id; + }) + ])].filter(id => id !== undefined) as number[], + events: [ ...selectedActions.map(action => { const actionDef = about?.server.services .flatMap((s: Service) => s.actions) .find((a: Action) => a.name === action.name); return { + id: action.id, name: action.name, type: 'action' as "action", description: action.description, @@ -270,6 +287,7 @@ const CreateWorkflow: React.FC = () => { .find((r: Reaction) => r.name === reaction.name); return { + id: reaction.id, name: reaction.name, type: 'reaction' as "reaction", description: reaction.description, diff --git a/client_web/src/types.ts b/client_web/src/types.ts index 8965467..e05dc2f 100644 --- a/client_web/src/types.ts +++ b/client_web/src/types.ts @@ -5,18 +5,21 @@ export type Parameter = { }; export type Action = { + id: number; name: string; description: string; parameters: Parameter[]; }; export type Reaction = { + id: number; name: string; description: string; parameters: Parameter[]; }; export type Service = { + id: number; name: string; actions: Action[]; reactions: Reaction[]; @@ -53,7 +56,7 @@ export interface WorkflowDefinition { export type Workflow = { name: string; - service: string; + services: number[]; description: string; events: WorkflowDefinition[]; }; @@ -63,3 +66,11 @@ export interface WorkflowItem { name: string; parameters?: Record; } + +export interface SelectedAction extends Omit { + parameters: Record; +} + +export interface SelectedReaction extends Omit { + parameters: Record; +} From 444d2e7f278244d19d50cf4e8ab1fb2c4252760d Mon Sep 17 00:00:00 2001 From: Mathieu Coulet Date: Mon, 9 Dec 2024 14:24:16 +0100 Subject: [PATCH 12/26] feat(Models): add ids in System and Event field in models --- server/internal/controllers/about.go | 4 ++++ server/internal/models/about.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/server/internal/controllers/about.go b/server/internal/controllers/about.go index dd8043c..586fcb2 100644 --- a/server/internal/controllers/about.go +++ b/server/internal/controllers/about.go @@ -32,12 +32,14 @@ func getServiceFromType(serviceType string, service models.Service) models.Servi if serviceType == "action" { actions = append(actions, models.Action{ + Id: event.ID, Name: event.Name, Description: event.Description, Parameters: parameters, }) } else if serviceType == "reaction" { reactions = append(reactions, models.Reaction{ + Id: event.ID, Name: event.Name, Description: event.Description, Parameters: parameters, @@ -46,6 +48,7 @@ func getServiceFromType(serviceType string, service models.Service) models.Servi } return models.ServiceList{ + Id: service.ID, Name: service.Name, Actions: actions, Reaction: reactions, @@ -65,6 +68,7 @@ func getServices() []models.ServiceList { actions := getServiceFromType("action", service) reactions := getServiceFromType("reaction", service) serviceList = append(serviceList, models.ServiceList{ + Id: service.ID, Name: service.Name, Actions: actions.Actions, Reaction: reactions.Reaction, diff --git a/server/internal/models/about.go b/server/internal/models/about.go index f5795fa..93c6e2e 100644 --- a/server/internal/models/about.go +++ b/server/internal/models/about.go @@ -7,18 +7,21 @@ type Parameter struct { } type Action struct { + Id uint `json:"id"` Name string `json:"name"` Description string `json:"description"` Parameters []Parameter `json:"parameters"` } type Reaction struct { + Id uint `json:"id"` Name string `json:"name"` Description string `json:"description"` Parameters []Parameter `json:"parameters"` } type ServiceList struct { + Id uint `json:"id"` Name string `json:"name"` Actions []Action `json:"actions"` Reaction []Reaction `json:"reactions"` From 441e1918effaac7f83f53658eb2e7b44683d5fec Mon Sep 17 00:00:00 2001 From: Samuel Bruschet Date: Mon, 9 Dec 2024 15:56:31 +0100 Subject: [PATCH 13/26] Sbr/feat mobile login (#132) * feat(form): added a form to verify users input during login / registration * feat(login): added verification at the beginning of the program to check whether we are login or not * feat(login): can now login, linked with backend route /auth/login * feat(register): added register option for user * fix(env): added BACKEND_BASE_URL env variable to setup backend ip * feat(logout): added button to logout in the dashboard --- client_mobile/.env.mobile.example | 1 + client_mobile/lib/features/auth/login.dart | 80 ----- client_mobile/lib/features/auth/register.dart | 87 ------ client_mobile/lib/main.dart | 16 +- client_mobile/lib/pages/auth/login.dart | 122 ++++++++ client_mobile/lib/pages/auth/register.dart | 153 ++++++++++ .../lib/pages/dashboard/dashboard.dart | 30 ++ client_mobile/lib/pages/home/home.dart | 32 ++ .../lib/services/login/auth_service.dart | 190 ++++++++++++ client_mobile/lib/tools/utils.dart | 24 ++ client_mobile/pubspec.lock | 2 +- client_mobile/pubspec.yaml | 1 + client_mobile/windows/.gitignore | 17 -- client_mobile/windows/CMakeLists.txt | 108 ------- client_mobile/windows/flutter/CMakeLists.txt | 109 ------- .../flutter/generated_plugin_registrant.cc | 20 -- .../flutter/generated_plugin_registrant.h | 15 - .../windows/flutter/generated_plugins.cmake | 26 -- client_mobile/windows/runner/CMakeLists.txt | 40 --- client_mobile/windows/runner/Runner.rc | 121 -------- .../windows/runner/flutter_window.cpp | 71 ----- client_mobile/windows/runner/flutter_window.h | 33 -- client_mobile/windows/runner/main.cpp | 43 --- client_mobile/windows/runner/resource.h | 16 - .../windows/runner/resources/app_icon.ico | Bin 33772 -> 0 bytes .../windows/runner/runner.exe.manifest | 20 -- client_mobile/windows/runner/utils.cpp | 65 ---- client_mobile/windows/runner/utils.h | 19 -- client_mobile/windows/runner/win32_window.cpp | 288 ------------------ client_mobile/windows/runner/win32_window.h | 102 ------- 30 files changed, 567 insertions(+), 1284 deletions(-) delete mode 100644 client_mobile/lib/features/auth/login.dart delete mode 100644 client_mobile/lib/features/auth/register.dart create mode 100644 client_mobile/lib/pages/auth/login.dart create mode 100644 client_mobile/lib/pages/auth/register.dart create mode 100644 client_mobile/lib/pages/dashboard/dashboard.dart create mode 100644 client_mobile/lib/pages/home/home.dart create mode 100644 client_mobile/lib/services/login/auth_service.dart create mode 100644 client_mobile/lib/tools/utils.dart delete mode 100644 client_mobile/windows/.gitignore delete mode 100644 client_mobile/windows/CMakeLists.txt delete mode 100644 client_mobile/windows/flutter/CMakeLists.txt delete mode 100644 client_mobile/windows/flutter/generated_plugin_registrant.cc delete mode 100644 client_mobile/windows/flutter/generated_plugin_registrant.h delete mode 100644 client_mobile/windows/flutter/generated_plugins.cmake delete mode 100644 client_mobile/windows/runner/CMakeLists.txt delete mode 100644 client_mobile/windows/runner/Runner.rc delete mode 100644 client_mobile/windows/runner/flutter_window.cpp delete mode 100644 client_mobile/windows/runner/flutter_window.h delete mode 100644 client_mobile/windows/runner/main.cpp delete mode 100644 client_mobile/windows/runner/resource.h delete mode 100644 client_mobile/windows/runner/resources/app_icon.ico delete mode 100644 client_mobile/windows/runner/runner.exe.manifest delete mode 100644 client_mobile/windows/runner/utils.cpp delete mode 100644 client_mobile/windows/runner/utils.h delete mode 100644 client_mobile/windows/runner/win32_window.cpp delete mode 100644 client_mobile/windows/runner/win32_window.h diff --git a/client_mobile/.env.mobile.example b/client_mobile/.env.mobile.example index e538184..d496c7e 100644 --- a/client_mobile/.env.mobile.example +++ b/client_mobile/.env.mobile.example @@ -2,6 +2,7 @@ API_URL=http://localhost:8080 WEB_CLIENT_URL=http://localhost:8081 MOBILE_CLIENT_URL=http://localhost:8082 +BACKEND_BASE_URL=http://10.109.253.48:8080 # OAuth credentials GITHUB_CLIENT_ID=your_github_client_id diff --git a/client_mobile/lib/features/auth/login.dart b/client_mobile/lib/features/auth/login.dart deleted file mode 100644 index 26982eb..0000000 --- a/client_mobile/lib/features/auth/login.dart +++ /dev/null @@ -1,80 +0,0 @@ -// import 'dart:io'; -import 'package:client_mobile/services/microsoft/microsoft_auth_service.dart'; -import 'package:client_mobile/widgets/button.dart'; -import 'package:client_mobile/widgets/clickable_text.dart'; -import 'package:client_mobile/widgets/form_field.dart'; -import 'package:client_mobile/widgets/sign_in_button.dart'; -import 'package:client_mobile/widgets/simple_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_appauth/flutter_appauth.dart'; -import 'package:go_router/go_router.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -class LoginPage extends StatefulWidget { - @override - _LoginPageState createState() => _LoginPageState(); -} - -class _LoginPageState extends State { - final String callbackUrlScheme = 'my.area.app'; - String get spotifyRedirectUrlMobile => '$callbackUrlScheme://callback'; - - final String clientId = dotenv.env["VITE_SPOTIFY_CLIENT_ID"] ?? ""; - final appAuth = const FlutterAppAuth(); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Container( - padding: const EdgeInsets.all(20), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SimpleText( - "Email", - bold: true, - ), - const AreaFormField(label: "Value"), - const SizedBox(height: 50), - const SimpleText("Password", bold: true), - const AreaFormField(label: "Value"), - const SizedBox(height: 15), - AreaButton( - label: "Login", - onPressed: () {}, - color: Colors.black, - ), - const SizedBox(height: 30), - Align( - alignment: Alignment.center, - child: SignInButton( - onPressed: () { - MicrosoftAuthService.auth(context); - }, - label: "Sign in with Microsoft", - image: Image.asset( - "assets/images/microsoft_logo.png", - width: 40, - height: 30, - ), - ), - ), - const SizedBox(height: 5), - Align( - alignment: Alignment.center, - child: SmallClickableText( - "I don't have an account", - onPressed: () { - context.push("/register"); - }, - ), - ) - ], - ), - ), - )), - ); - } -} diff --git a/client_mobile/lib/features/auth/register.dart b/client_mobile/lib/features/auth/register.dart deleted file mode 100644 index af46783..0000000 --- a/client_mobile/lib/features/auth/register.dart +++ /dev/null @@ -1,87 +0,0 @@ -// import 'dart:io'; -import 'package:client_mobile/services/microsoft/microsoft_auth_service.dart'; -import 'package:client_mobile/widgets/button.dart'; -import 'package:client_mobile/widgets/clickable_text.dart'; -import 'package:client_mobile/widgets/form_field.dart'; -import 'package:client_mobile/widgets/sign_in_button.dart'; -import 'package:client_mobile/widgets/simple_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_appauth/flutter_appauth.dart'; -import 'package:go_router/go_router.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -class RegisterPage extends StatefulWidget { - @override - _RegisterPageState createState() => _RegisterPageState(); -} - -class _RegisterPageState extends State { - final String callbackUrlScheme = 'my.area.app'; - String get spotifyRedirectUrlMobile => '$callbackUrlScheme://callback'; - - final String clientId = dotenv.env["VITE_SPOTIFY_CLIENT_ID"] ?? ""; - final appAuth = const FlutterAppAuth(); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Container( - padding: const EdgeInsets.all(20), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SimpleText( - "Username", - bold: true, - ), - const AreaFormField(label: "Value"), - const SizedBox(height: 25), - const SimpleText("Password", bold: true), - const AreaFormField(label: "Value"), - const SizedBox(height: 15), - const SimpleText("Confirm password", bold: true), - const AreaFormField(label: "Value"), - const SizedBox(height: 15), - Align( - alignment: Alignment.center, - child: AreaButton( - label: "Register", - onPressed: () {}, - color: Colors.black, - ), - ), - const SizedBox(height: 30), - Align( - alignment: Alignment.center, - child: SignInButton( - onPressed: () { - MicrosoftAuthService.auth(context); - }, - label: "Sign in with Microsoft", - image: Image.asset( - "assets/images/microsoft_logo.png", - width: 40, - height: 30, - ), - ), - ), - const SizedBox(height: 5), - Align( - alignment: Alignment.center, - child: SmallClickableText( - "I already have an account", - onPressed: () { - context.pop(); - }, - ), - ) - ], - ), - ), - )), - ); - } -} diff --git a/client_mobile/lib/main.dart b/client_mobile/lib/main.dart index 8f8a249..7a1e67b 100644 --- a/client_mobile/lib/main.dart +++ b/client_mobile/lib/main.dart @@ -1,24 +1,34 @@ -import 'package:client_mobile/features/auth/login.dart'; -import 'package:client_mobile/features/auth/register.dart'; +import 'package:client_mobile/pages/auth/login.dart'; +import 'package:client_mobile/pages/auth/register.dart'; +import 'package:client_mobile/pages/dashboard/dashboard.dart'; +import 'package:client_mobile/pages/home/home.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:go_router/go_router.dart'; final GlobalKey navigatorKey = GlobalKey(); Future main() async { - await dotenv.load(fileName: ".env"); + await dotenv.load(fileName: ".env.mobile"); final GoRouter router = GoRouter( navigatorKey: navigatorKey, routes: [ GoRoute( path: '/', + builder: (context, state) => HomePage(), + ), + GoRoute( + path: '/login', builder: (context, state) => LoginPage(), ), GoRoute( path: '/register', builder: (context, state) => RegisterPage(), ), + GoRoute( + path: '/dashboard', + builder: (context, state) => DashboardPage(), + ), ], ); diff --git a/client_mobile/lib/pages/auth/login.dart b/client_mobile/lib/pages/auth/login.dart new file mode 100644 index 0000000..4cf6bcb --- /dev/null +++ b/client_mobile/lib/pages/auth/login.dart @@ -0,0 +1,122 @@ +// import 'dart:io'; +import 'package:client_mobile/services/login/auth_service.dart'; +import 'package:client_mobile/services/microsoft/microsoft_auth_service.dart'; +import 'package:client_mobile/tools/utils.dart'; +import 'package:client_mobile/widgets/button.dart'; +import 'package:client_mobile/widgets/clickable_text.dart'; +import 'package:client_mobile/widgets/form_field.dart'; +import 'package:client_mobile/widgets/sign_in_button.dart'; +import 'package:client_mobile/widgets/simple_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class LoginPage extends StatefulWidget { + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final String callbackUrlScheme = 'my.area.app'; + String get spotifyRedirectUrlMobile => '$callbackUrlScheme://callback'; + + final String clientId = dotenv.env["VITE_SPOTIFY_CLIENT_ID"] ?? ""; + final appAuth = const FlutterAppAuth(); + + final _formKey = GlobalKey(); + + final TextEditingController emailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Container( + padding: const EdgeInsets.all(20), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SimpleText( + "Email", + bold: true, + ), + AreaFormField( + label: "Value", + controller: emailController, + validator: (email) { + if (email == null || email.isEmpty) { + return "Please input your email."; + } + if (!Utils.isValidEmail(email)) { + return "Your email isn't correct."; + } + return (null); + }, + ), + const SizedBox(height: 50), + const SimpleText("Password", bold: true), + AreaFormField( + controller: passwordController, + validator: (password) { + if (password == null || password.isEmpty) + return "Please input your password."; + return (null); + }, + label: "Value", + ), + const SizedBox(height: 15), + AreaButton( + label: "Login", + onPressed: () async { + if (_formKey.currentState!.validate()) { + bool isLogin = await AuthService.login( + context, + LoginObject( + email: emailController.text, + password: passwordController.text) + .toJson()); + if (isLogin) { + context.pushReplacement("/dashboard"); + } + } + }, + color: Colors.black, + ), + const SizedBox(height: 30), + Align( + alignment: Alignment.center, + child: SignInButton( + onPressed: () { + MicrosoftAuthService.auth(context); + }, + label: "Sign in with Microsoft", + image: Image.asset( + "assets/images/microsoft_logo.png", + width: 40, + height: 30, + ), + ), + ), + const SizedBox(height: 5), + Align( + alignment: Alignment.center, + child: SmallClickableText( + "I don't have an account", + onPressed: () { + context.push("/register"); + }, + ), + ) + ], + ), + ), + ), + )), + ); + } +} diff --git a/client_mobile/lib/pages/auth/register.dart b/client_mobile/lib/pages/auth/register.dart new file mode 100644 index 0000000..b8e7380 --- /dev/null +++ b/client_mobile/lib/pages/auth/register.dart @@ -0,0 +1,153 @@ +// import 'dart:io'; +import 'package:client_mobile/services/login/auth_service.dart'; +import 'package:client_mobile/services/microsoft/microsoft_auth_service.dart'; +import 'package:client_mobile/tools/utils.dart'; +import 'package:client_mobile/widgets/button.dart'; +import 'package:client_mobile/widgets/clickable_text.dart'; +import 'package:client_mobile/widgets/form_field.dart'; +import 'package:client_mobile/widgets/sign_in_button.dart'; +import 'package:client_mobile/widgets/simple_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class RegisterPage extends StatefulWidget { + @override + _RegisterPageState createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + final String callbackUrlScheme = 'my.area.app'; + String get spotifyRedirectUrlMobile => '$callbackUrlScheme://callback'; + + final String clientId = dotenv.env["VITE_SPOTIFY_CLIENT_ID"] ?? ""; + final appAuth = const FlutterAppAuth(); + + final _formKey = GlobalKey(); + + final TextEditingController userController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final TextEditingController confirmPasswordController = + TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Container( + padding: const EdgeInsets.all(20), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SimpleText( + "Username", + bold: true, + ), + AreaFormField( + label: "Value", + controller: userController, + validator: (user) { + if (user == null || user.isEmpty) + return "Please input your username."; + return (null); + }), + const SimpleText( + "Email", + bold: true, + ), + AreaFormField( + label: "Value", + controller: emailController, + validator: (email) { + if (email == null || email.isEmpty) { + return "Please input your email."; + } + if (!Utils.isValidEmail(email)) { + return "Your email isn't correct."; + } + return (null); + }, + ), + const SizedBox(height: 25), + const SimpleText("Password", bold: true), + AreaFormField( + label: "Value", + controller: passwordController, + validator: (password) { + if (password == null || password.isEmpty) { + return "Please input your password."; + } + String pwdError = Utils.isValidPassword(password); + if (pwdError.isNotEmpty) return (pwdError); + return (null); + }, + ), + const SizedBox(height: 15), + const SimpleText("Confirm password", bold: true), + AreaFormField( + label: "Value", + controller: confirmPasswordController, + validator: (password) { + if (password == null || password.isEmpty) { + return "Please confirm your password."; + } + if (password != passwordController.text) + return "Your password doesn't match."; + return (null); + }, + ), + const SizedBox(height: 15), + Align( + alignment: Alignment.center, + child: AreaButton( + label: "Register", + onPressed: () async { + if (_formKey.currentState!.validate()) { + bool isRegistered = await AuthService.register(context, RegisterObject(email: emailController.text, password: passwordController.text, username: userController.text).toJson()); + if (isRegistered) { + context.pushReplacement("/dashboard"); + } + } + }, + color: Colors.black, + ), + ), + const SizedBox(height: 30), + Align( + alignment: Alignment.center, + child: SignInButton( + onPressed: () { + MicrosoftAuthService.auth(context); + }, + label: "Sign in with Microsoft", + image: Image.asset( + "assets/images/microsoft_logo.png", + width: 40, + height: 30, + ), + ), + ), + const SizedBox(height: 5), + Align( + alignment: Alignment.center, + child: SmallClickableText( + "I already have an account", + onPressed: () { + context.pop(); + }, + ), + ) + ], + ), + ), + ), + )), + ); + } +} diff --git a/client_mobile/lib/pages/dashboard/dashboard.dart b/client_mobile/lib/pages/dashboard/dashboard.dart new file mode 100644 index 0000000..ef1e271 --- /dev/null +++ b/client_mobile/lib/pages/dashboard/dashboard.dart @@ -0,0 +1,30 @@ +import 'package:client_mobile/services/login/auth_service.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class DashboardPage extends StatefulWidget { + const DashboardPage({super.key}); + + @override + State createState() => _DashboardPageState(); +} + +class _DashboardPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: const Center( + child: Text("Dashboard "), + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + bool hasLogout = await AuthService.logout(); + if (hasLogout) + context.pushReplacement("/login"); + }, + tooltip: 'Logout', + child: const Icon(Icons.login), + ), + ); + } +} diff --git a/client_mobile/lib/pages/home/home.dart b/client_mobile/lib/pages/home/home.dart new file mode 100644 index 0000000..6e84ad6 --- /dev/null +++ b/client_mobile/lib/pages/home/home.dart @@ -0,0 +1,32 @@ +import 'package:client_mobile/services/login/auth_service.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + @override + void initState() { + super.initState(); + redirect(); + } + + void redirect() async { + if (await AuthService.isUserLogin()) + context.pushReplacement("/dashboard"); + else + context.pushReplacement("/login"); + } + + @override + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator(), + ); + } +} diff --git a/client_mobile/lib/services/login/auth_service.dart b/client_mobile/lib/services/login/auth_service.dart new file mode 100644 index 0000000..29fdf6e --- /dev/null +++ b/client_mobile/lib/services/login/auth_service.dart @@ -0,0 +1,190 @@ +import 'dart:convert'; + +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +class LoginObject { + final String email; + final String password; + + LoginObject({ + required this.email, + required this.password, + }); + + Map toJson() { + return { + 'email': email, + 'password': password, + }; + } + + factory LoginObject.fromJson(Map json) { + return LoginObject( + email: json['email'] as String, + password: json['password'] as String, + ); + } +} + +class RegisterObject { + final String email; + final String password; + final String username; + + RegisterObject( + {required this.email, required this.password, required this.username}); + + Map toJson() { + return { + 'email': email, + 'password': password, + 'username': username, + }; + } + + factory RegisterObject.fromJson(Map json) { + return RegisterObject( + email: json['email'] as String, + password: json['password'] as String, + username: json['username'] as String, + ); + } +} + +class AuthService { + static const FlutterSecureStorage secureStorage = FlutterSecureStorage(); + + static String baseUrl = dotenv.env["BACKEND_BASE_URL"] ?? "http://127.0.0.1:8080"; + + static Future validateBearerToken(String token) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/auth/health'), + headers: {'Authorization': 'Bearer $token'}, + ); + return response.statusCode == 200; + } catch (e) { + print('Erreur lors de la validation du token: $e'); + return false; + } + } + + static Future isUserLogin() async { + final String? token = await secureStorage.read(key: 'bearer_token'); + if (token == null) return (false); + if (!await validateBearerToken(token)) { + await secureStorage.delete(key: 'bearer_token'); // delete expired token + return (false); + } + return (true); + } + + static Future login( + BuildContext context, Map jsonInfos) async { + try { + print("Tentative de login..."); + + final url = Uri.parse('$baseUrl/auth/login'); + + final response = await http.post( + url, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(jsonInfos), + ); + + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body); + final String token = responseData['jwt']; + + await secureStorage.write(key: 'bearer_token', value: token); + + print("Connexion réussie et token sauvegardé !"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Login effectué avec succès !"), + backgroundColor: Colors.black, + ), + ); + return (true); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Erreur de connexion : Code ${response.statusCode} - ${response.body}"), + backgroundColor: Colors.red, + ), + ); + print( + "Erreur de connexion : Code ${response.statusCode} - ${response.body}"); + return (false); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Erreur de login : $e"), + backgroundColor: Colors.red, + ), + ); + return (false); + } + } + + static Future register( + BuildContext context, Map jsonInfos) async { + try { + print("Tentative de connexion..."); + + final url = Uri.parse('$baseUrl/auth/register'); + + final response = await http.post( + url, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(jsonInfos), + ); + + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body); + final String token = responseData['jwt']; + + await secureStorage.write(key: 'bearer_token', value: token); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Register avec succès !"), + backgroundColor: Colors.black, + ), + ); + return (true); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Unauthrorized to process."), + backgroundColor: Colors.red, + ), + ); + return (false); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Erreur de register : $e"), + backgroundColor: Colors.red, + ), + ); + return (false); + } + } + + static Future logout() async { + bool isLogin = await secureStorage.read(key: "bearer_token") != null; + + if (isLogin) { + await secureStorage.delete(key: "bearer_token"); + return (true); + } + return (false); + } +} diff --git a/client_mobile/lib/tools/utils.dart b/client_mobile/lib/tools/utils.dart new file mode 100644 index 0000000..0364d61 --- /dev/null +++ b/client_mobile/lib/tools/utils.dart @@ -0,0 +1,24 @@ +class Utils { + static bool isValidEmail(String email) { + final emailRegex = + RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'); + return emailRegex.hasMatch(email); + } + + static String isValidPassword(String password) { + String error = ""; + + if (password.length < 8) + error += "Your password must be at least 8 characters.\n"; + if (!RegExp(r'[a-zA-Z]').hasMatch(password)) { + error += "Password must contain at least one letter.\n"; + } + if (!RegExp(r'\d').hasMatch(password)) { + error += "Password must contain at least one number.\n"; + } + if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) { + error += "Password must contain at least one special character.\n"; + } + return (error); + } +} diff --git a/client_mobile/pubspec.lock b/client_mobile/pubspec.lock index 467f5f1..384b6e1 100644 --- a/client_mobile/pubspec.lock +++ b/client_mobile/pubspec.lock @@ -257,7 +257,7 @@ packages: source: hosted version: "14.6.1" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 diff --git a/client_mobile/pubspec.yaml b/client_mobile/pubspec.yaml index 248ced5..2bc38aa 100644 --- a/client_mobile/pubspec.yaml +++ b/client_mobile/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: font_awesome_flutter: ^10.8.0 go_router: ^14.6.1 aad_oauth: ^1.0.1 + http: ^1.2.2 dev_dependencies: flutter_test: diff --git a/client_mobile/windows/.gitignore b/client_mobile/windows/.gitignore deleted file mode 100644 index d492d0d..0000000 --- a/client_mobile/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/client_mobile/windows/CMakeLists.txt b/client_mobile/windows/CMakeLists.txt deleted file mode 100644 index 3272ad7..0000000 --- a/client_mobile/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(client_mobile LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "client_mobile") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(VERSION 3.14...3.25) - -# Define build configuration option. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() -# Define settings for the Profile build mode. -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/client_mobile/windows/flutter/CMakeLists.txt b/client_mobile/windows/flutter/CMakeLists.txt deleted file mode 100644 index 903f489..0000000 --- a/client_mobile/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# Set fallback configurations for older versions of the flutter tool. -if (NOT DEFINED FLUTTER_TARGET_PLATFORM) - set(FLUTTER_TARGET_PLATFORM "windows-x64") -endif() - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - ${FLUTTER_TARGET_PLATFORM} $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/client_mobile/windows/flutter/generated_plugin_registrant.cc b/client_mobile/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 25d8f42..0000000 --- a/client_mobile/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,20 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - FlutterSecureStorageWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); - WindowToFrontPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WindowToFrontPlugin")); -} diff --git a/client_mobile/windows/flutter/generated_plugin_registrant.h b/client_mobile/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d8..0000000 --- a/client_mobile/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/client_mobile/windows/flutter/generated_plugins.cmake b/client_mobile/windows/flutter/generated_plugins.cmake deleted file mode 100644 index 7fcd0fe..0000000 --- a/client_mobile/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,26 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - flutter_secure_storage_windows - url_launcher_windows - window_to_front -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/client_mobile/windows/runner/CMakeLists.txt b/client_mobile/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c..0000000 --- a/client_mobile/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/client_mobile/windows/runner/Runner.rc b/client_mobile/windows/runner/Runner.rc deleted file mode 100644 index 2ee3448..0000000 --- a/client_mobile/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "client_mobile" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "client_mobile" "\0" - VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "client_mobile.exe" "\0" - VALUE "ProductName", "client_mobile" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/client_mobile/windows/runner/flutter_window.cpp b/client_mobile/windows/runner/flutter_window.cpp deleted file mode 100644 index 955ee30..0000000 --- a/client_mobile/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - // Flutter can complete the first frame before the "show window" callback is - // registered. The following call ensures a frame is pending to ensure the - // window is shown. It is a no-op if the first frame hasn't completed yet. - flutter_controller_->ForceRedraw(); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/client_mobile/windows/runner/flutter_window.h b/client_mobile/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652..0000000 --- a/client_mobile/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/client_mobile/windows/runner/main.cpp b/client_mobile/windows/runner/main.cpp deleted file mode 100644 index 042c81c..0000000 --- a/client_mobile/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); - if (!window.Create(L"client_mobile", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/client_mobile/windows/runner/resource.h b/client_mobile/windows/runner/resource.h deleted file mode 100644 index 66a65d1..0000000 --- a/client_mobile/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/client_mobile/windows/runner/resources/app_icon.ico b/client_mobile/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20caf6370ebb9253ad831cc31de4a9c965f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33772 zcmeHQc|26z|35SKE&G-*mXah&B~fFkXr)DEO&hIfqby^T&>|8^_Ub8Vp#`BLl3lbZ zvPO!8k!2X>cg~Elr=IVxo~J*a`+9wR=A83c-k-DFd(XM&UI1VKCqM@V;DDtJ09WB} zRaHKiW(GT00brH|0EeTeKVbpbGZg?nK6-j827q-+NFM34gXjqWxJ*a#{b_apGN<-L_m3#8Z26atkEn& ze87Bvv^6vVmM+p+cQ~{u%=NJF>#(d;8{7Q{^rWKWNtf14H}>#&y7$lqmY6xmZryI& z($uy?c5-+cPnt2%)R&(KIWEXww>Cnz{OUpT>W$CbO$h1= z#4BPMkFG1Y)x}Ui+WXr?Z!w!t_hjRq8qTaWpu}FH{MsHlU{>;08goVLm{V<&`itk~ zE_Ys=D(hjiy+5=?=$HGii=Y5)jMe9|wWoD_K07(}edAxh`~LBorOJ!Cf@f{_gNCC| z%{*04ViE!#>@hc1t5bb+NO>ncf@@Dv01K!NxH$3Eg1%)|wLyMDF8^d44lV!_Sr}iEWefOaL z8f?ud3Q%Sen39u|%00W<#!E=-RpGa+H8}{ulxVl4mwpjaU+%2pzmi{3HM)%8vb*~-M9rPUAfGCSos8GUXp02|o~0BTV2l#`>>aFV&_P$ejS;nGwSVP8 zMbOaG7<7eKD>c12VdGH;?2@q7535sa7MN*L@&!m?L`ASG%boY7(&L5imY#EQ$KrBB z4@_tfP5m50(T--qv1BJcD&aiH#b-QC>8#7Fx@3yXlonJI#aEIi=8&ChiVpc#N=5le zM*?rDIdcpawoc5kizv$GEjnveyrp3sY>+5_R5;>`>erS%JolimF=A^EIsAK zsPoVyyUHCgf0aYr&alx`<)eb6Be$m&`JYSuBu=p8j%QlNNp$-5C{b4#RubPb|CAIS zGE=9OFLP7?Hgc{?k45)84biT0k&-C6C%Q}aI~q<(7BL`C#<6HyxaR%!dFx7*o^laG z=!GBF^cwK$IA(sn9y6>60Rw{mYRYkp%$jH z*xQM~+bp)G$_RhtFPYx2HTsWk80+p(uqv9@I9)y{b$7NK53rYL$ezbmRjdXS?V}fj zWxX_feWoLFNm3MG7pMUuFPs$qrQWO9!l2B(SIuy2}S|lHNbHzoE+M2|Zxhjq9+Ws8c{*}x^VAib7SbxJ*Q3EnY5lgI9 z=U^f3IW6T=TWaVj+2N%K3<%Un;CF(wUp`TC&Y|ZjyFu6co^uqDDB#EP?DV5v_dw~E zIRK*BoY9y-G_ToU2V_XCX4nJ32~`czdjT!zwme zGgJ0nOk3U4@IE5JwtM}pwimLjk{ln^*4HMU%Fl4~n(cnsLB}Ja-jUM>xIB%aY;Nq8 z)Fp8dv1tkqKanv<68o@cN|%thj$+f;zGSO7H#b+eMAV8xH$hLggtt?O?;oYEgbq@= zV(u9bbd12^%;?nyk6&$GPI%|+<_mEpJGNfl*`!KV;VfmZWw{n{rnZ51?}FDh8we_L z8OI9nE31skDqJ5Oa_ybn7|5@ui>aC`s34p4ZEu6-s!%{uU45$Zd1=p$^^dZBh zu<*pDDPLW+c>iWO$&Z_*{VSQKg7=YEpS3PssPn1U!lSm6eZIho*{@&20e4Y_lRklKDTUCKI%o4Pc<|G^Xgu$J^Q|B87U;`c1zGwf^-zH*VQ^x+i^OUWE0yd z;{FJq)2w!%`x7yg@>uGFFf-XJl4H`YtUG%0slGKOlXV`q?RP>AEWg#x!b{0RicxGhS!3$p7 zij;{gm!_u@D4$Ox%>>bPtLJ> zwKtYz?T_DR1jN>DkkfGU^<#6sGz|~p*I{y`aZ>^Di#TC|Z!7j_O1=Wo8thuit?WxR zh9_S>kw^{V^|g}HRUF=dcq>?q(pHxw!8rx4dC6vbQVmIhmICF#zU!HkHpQ>9S%Uo( zMw{eC+`&pb=GZRou|3;Po1}m46H6NGd$t<2mQh}kaK-WFfmj_66_17BX0|j-E2fe3Jat}ijpc53 zJV$$;PC<5aW`{*^Z6e5##^`Ed#a0nwJDT#Qq~^e8^JTA=z^Kl>La|(UQ!bI@#ge{Dzz@61p-I)kc2?ZxFt^QQ}f%ldLjO*GPj(5)V9IyuUakJX=~GnTgZ4$5!3E=V#t`yOG4U z(gphZB6u2zsj=qNFLYShhg$}lNpO`P9xOSnO*$@@UdMYES*{jJVj|9z-}F^riksLK zbsU+4-{281P9e2UjY6tse^&a)WM1MFw;p#_dHhWI7p&U*9TR0zKdVuQed%6{otTsq z$f~S!;wg#Bd9kez=Br{m|66Wv z#g1xMup<0)H;c2ZO6su_ii&m8j&+jJz4iKnGZ&wxoQX|5a>v&_e#6WA!MB_4asTxLRGQCC5cI(em z%$ZfeqP>!*q5kU>a+BO&ln=4Jm>Ef(QE8o&RgLkk%2}4Tf}U%IFP&uS7}&|Q-)`5< z+e>;s#4cJ-z%&-^&!xsYx777Wt(wZY9(3(avmr|gRe4cD+a8&!LY`1^T?7x{E<=kdY9NYw>A;FtTvQ=Y&1M%lyZPl$ss1oY^Sl8we}n}Aob#6 zl4jERwnt9BlSoWb@3HxYgga(752Vu6Y)k4yk9u~Kw>cA5&LHcrvn1Y-HoIuFWg~}4 zEw4bR`mXZQIyOAzo)FYqg?$5W<;^+XX%Uz61{-L6@eP|lLH%|w?g=rFc;OvEW;^qh z&iYXGhVt(G-q<+_j}CTbPS_=K>RKN0&;dubh0NxJyDOHFF;<1k!{k#7b{|Qok9hac z;gHz}6>H6C6RnB`Tt#oaSrX0p-j-oRJ;_WvS-qS--P*8}V943RT6kou-G=A+7QPGQ z!ze^UGxtW3FC0$|(lY9^L!Lx^?Q8cny(rR`es5U;-xBhphF%_WNu|aO<+e9%6LuZq zt(0PoagJG<%hyuf;te}n+qIl_Ej;czWdc{LX^pS>77s9t*2b4s5dvP_!L^3cwlc)E!(!kGrg~FescVT zZCLeua3f4;d;Tk4iXzt}g}O@nlK3?_o91_~@UMIl?@77Qc$IAlLE95#Z=TES>2E%z zxUKpK{_HvGF;5%Q7n&vA?`{%8ohlYT_?(3A$cZSi)MvIJygXD}TS-3UwyUxGLGiJP znblO~G|*uA^|ac8E-w#}uBtg|s_~s&t>-g0X%zIZ@;o_wNMr_;{KDg^O=rg`fhDZu zFp(VKd1Edj%F zWHPl+)FGj%J1BO3bOHVfH^3d1F{)*PL&sRX`~(-Zy3&9UQX)Z;c51tvaI2E*E7!)q zcz|{vpK7bjxix(k&6=OEIBJC!9lTkUbgg?4-yE{9+pFS)$Ar@vrIf`D0Bnsed(Cf? zObt2CJ>BKOl>q8PyFO6w)+6Iz`LW%T5^R`U_NIW0r1dWv6OY=TVF?N=EfA(k(~7VBW(S;Tu5m4Lg8emDG-(mOSSs=M9Q&N8jc^Y4&9RqIsk(yO_P(mcCr}rCs%1MW1VBrn=0-oQN(Xj!k%iKV zb%ricBF3G4S1;+8lzg5PbZ|$Se$)I=PwiK=cDpHYdov2QO1_a-*dL4KUi|g&oh>(* zq$<`dQ^fat`+VW?m)?_KLn&mp^-@d=&7yGDt<=XwZZC=1scwxO2^RRI7n@g-1o8ps z)&+et_~)vr8aIF1VY1Qrq~Xe``KJrQSnAZ{CSq3yP;V*JC;mmCT6oRLSs7=GA?@6g zUooM}@tKtx(^|aKK8vbaHlUQqwE0}>j&~YlN3H#vKGm@u)xxS?n9XrOWUfCRa< z`20Fld2f&;gg7zpo{Adh+mqNntMc-D$N^yWZAZRI+u1T1zWHPxk{+?vcS1D>08>@6 zLhE@`gt1Y9mAK6Z4p|u(5I%EkfU7rKFSM=E4?VG9tI;a*@?6!ey{lzN5=Y-!$WFSe z&2dtO>^0@V4WRc#L&P%R(?@KfSblMS+N+?xUN$u3K4Ys%OmEh+tq}fnU}i>6YHM?< zlnL2gl~sF!j!Y4E;j3eIU-lfa`RsOL*Tt<%EFC0gPzoHfNWAfKFIKZN8}w~(Yi~=q z>=VNLO2|CjkxP}RkutxjV#4fWYR1KNrPYq5ha9Wl+u>ipsk*I(HS@iLnmGH9MFlTU zaFZ*KSR0px>o+pL7BbhB2EC1%PJ{67_ z#kY&#O4@P=OV#-79y_W>Gv2dxL*@G7%LksNSqgId9v;2xJ zrh8uR!F-eU$NMx@S*+sk=C~Dxr9Qn7TfWnTupuHKuQ$;gGiBcU>GF5sWx(~4IP3`f zWE;YFO*?jGwYh%C3X<>RKHC-DZ!*r;cIr}GLOno^3U4tFSSoJp%oHPiSa%nh=Zgn% z14+8v@ygy0>UgEN1bczD6wK45%M>psM)y^)IfG*>3ItX|TzV*0i%@>L(VN!zdKb8S?Qf7BhjNpziA zR}?={-eu>9JDcl*R=OP9B8N$IcCETXah9SUDhr{yrld{G;PnCWRsPD7!eOOFBTWUQ=LrA_~)mFf&!zJX!Oc-_=kT<}m|K52 z)M=G#;p;Rdb@~h5D{q^K;^fX-m5V}L%!wVC2iZ1uu401Ll}#rocTeK|7FAeBRhNdQ zCc2d^aQnQp=MpOmak60N$OgS}a;p(l9CL`o4r(e-nN}mQ?M&isv-P&d$!8|1D1I(3-z!wi zTgoo)*Mv`gC?~bm?S|@}I|m-E2yqPEvYybiD5azInexpK8?9q*$9Yy9-t%5jU8~ym zgZDx>!@ujQ=|HJnwp^wv-FdD{RtzO9SnyfB{mH_(c!jHL*$>0o-(h(eqe*ZwF6Lvu z{7rkk%PEqaA>o+f{H02tzZ@TWy&su?VNw43! z-X+rN`6llvpUms3ZiSt)JMeztB~>9{J8SPmYs&qohxdYFi!ra8KR$35Zp9oR)eFC4 zE;P31#3V)n`w$fZ|4X-|%MX`xZDM~gJyl2W;O$H25*=+1S#%|53>|LyH za@yh+;325%Gq3;J&a)?%7X%t@WXcWL*BaaR*7UEZad4I8iDt7^R_Fd`XeUo256;sAo2F!HcIQKk;h})QxEsPE5BcKc7WyerTchgKmrfRX z!x#H_%cL#B9TWAqkA4I$R^8{%do3Y*&(;WFmJ zU7Dih{t1<{($VtJRl9|&EB?|cJ)xse!;}>6mSO$o5XIx@V|AA8ZcoD88ZM?C*;{|f zZVmf94_l1OmaICt`2sTyG!$^UeTHx9YuUP!omj(r|7zpm5475|yXI=rR>>fteLI+| z)MoiGho0oEt=*J(;?VY0QzwCqw@cVm?d7Y!z0A@u#H?sCJ*ecvyhj& z-F77lO;SH^dmf?L>3i>?Z*U}Em4ZYV_CjgfvzYsRZ+1B!Uo6H6mbS<-FFL`ytqvb& zE7+)2ahv-~dz(Hs+f})z{*4|{)b=2!RZK;PWwOnO=hG7xG`JU5>bAvUbdYd_CjvtHBHgtGdlO+s^9ca^Bv3`t@VRX2_AD$Ckg36OcQRF zXD6QtGfHdw*hx~V(MV-;;ZZF#dJ-piEF+s27z4X1qi5$!o~xBnvf=uopcn7ftfsZc zy@(PuOk`4GL_n(H9(E2)VUjqRCk9kR?w)v@xO6Jm_Mx})&WGEl=GS0#)0FAq^J*o! zAClhvoTsNP*-b~rN{8Yym3g{01}Ep^^Omf=SKqvN?{Q*C4HNNAcrowIa^mf+3PRy! z*_G-|3i8a;+q;iP@~Of_$(vtFkB8yOyWt2*K)vAn9El>=D;A$CEx6b*XF@4y_6M+2 zpeW`RHoI_p(B{%(&jTHI->hmNmZjHUj<@;7w0mx3&koy!2$@cfX{sN19Y}euYJFn& z1?)+?HCkD0MRI$~uB2UWri})0bru_B;klFdwsLc!ne4YUE;t41JqfG# zZJq6%vbsdx!wYeE<~?>o4V`A3?lN%MnKQ`z=uUivQN^vzJ|C;sdQ37Qn?;lpzg})y z)_2~rUdH}zNwX;Tp0tJ78+&I=IwOQ-fl30R79O8@?Ub8IIA(6I`yHn%lARVL`%b8+ z4$8D-|MZZWxc_)vu6@VZN!HsI$*2NOV&uMxBNzIbRgy%ob_ zhwEH{J9r$!dEix9XM7n&c{S(h>nGm?el;gaX0@|QnzFD@bne`el^CO$yXC?BDJ|Qg z+y$GRoR`?ST1z^e*>;!IS@5Ovb7*RlN>BV_UC!7E_F;N#ky%1J{+iixp(dUJj93aK zzHNN>R-oN7>kykHClPnoPTIj7zc6KM(Pnlb(|s??)SMb)4!sMHU^-ntJwY5Big7xv zb1Ew`Xj;|D2kzGja*C$eS44(d&RMU~c_Y14V9_TLTz0J#uHlsx`S6{nhsA0dWZ#cG zJ?`fO50E>*X4TQLv#nl%3GOk*UkAgt=IY+u0LNXqeln3Z zv$~&Li`ZJOKkFuS)dJRA>)b_Da%Q~axwA_8zNK{BH{#}#m}zGcuckz}riDE-z_Ms> zR8-EqAMcfyGJCtvTpaUVQtajhUS%c@Yj}&6Zz;-M7MZzqv3kA7{SuW$oW#=0az2wQ zg-WG@Vb4|D`pl~Il54N7Hmsauc_ne-a!o5#j3WaBBh@Wuefb!QJIOn5;d)%A#s+5% zuD$H=VNux9bE-}1&bcYGZ+>1Fo;3Z@e&zX^n!?JK*adSbONm$XW9z;Q^L>9U!}Toj2WdafJ%oL#h|yWWwyAGxzfrAWdDTtaKl zK4`5tDpPg5>z$MNv=X0LZ0d6l%D{(D8oT@+w0?ce$DZ6pv>{1&Ok67Ix1 zH}3=IEhPJEhItCC8E=`T`N5(k?G=B4+xzZ?<4!~ ze~z6Wk9!CHTI(0rLJ4{JU?E-puc;xusR?>G?;4vt;q~iI9=kDL=z0Rr%O$vU`30X$ zDZRFyZ`(omOy@u|i6h;wtJlP;+}$|Ak|k2dea7n?U1*$T!sXqqOjq^NxLPMmk~&qI zYg0W?yK8T(6+Ea+$YyspKK?kP$+B`~t3^Pib_`!6xCs32!i@pqXfFV6PmBIR<-QW= zN8L{pt0Vap0x`Gzn#E@zh@H)0FfVfA_Iu4fjYZ+umO1LXIbVc$pY+E234u)ttcrl$ z>s92z4vT%n6cMb>=XT6;l0+9e(|CZG)$@C7t7Z7Ez@a)h)!hyuV&B5K%%)P5?Lk|C zZZSVzdXp{@OXSP0hoU-gF8s8Um(#xzjP2Vem zec#-^JqTa&Y#QJ>-FBxd7tf`XB6e^JPUgagB8iBSEps;92KG`!#mvVcPQ5yNC-GEG zTiHEDYfH+0O15}r^+ z#jxj=@x8iNHWALe!P3R67TwmhItn**0JwnzSV2O&KE8KcT+0hWH^OPD1pwiuyx=b@ zNf5Jh0{9X)8;~Es)$t@%(3!OnbY+`@?i{mGX7Yy}8T_*0a6g;kaFPq;*=px5EhO{Cp%1kI<0?*|h8v!6WnO3cCJRF2-CRrU3JiLJnj@6;L)!0kWYAc_}F{2P))3HmCrz zQ&N&gE70;`!6*eJ4^1IR{f6j4(-l&X!tjHxkbHA^Zhrnhr9g{exN|xrS`5Pq=#Xf& zG%P=#ra-TyVFfgW%cZo5OSIwFL9WtXAlFOa+ubmI5t*3=g#Y zF%;70p5;{ZeFL}&}yOY1N1*Q;*<(kTB!7vM$QokF)yr2FlIU@$Ph58$Bz z0J?xQG=MlS4L6jA22eS42g|9*9pX@$#*sUeM(z+t?hr@r5J&D1rx}2pW&m*_`VDCW zUYY@v-;bAO0HqoAgbbiGGC<=ryf96}3pouhy3XJrX+!!u*O_>Si38V{uJmQ&USptX zKp#l(?>%^7;2%h(q@YWS#9;a!JhKlkR#Vd)ERILlgu!Hr@jA@V;sk4BJ-H#p*4EqC zDGjC*tl=@3Oi6)Bn^QwFpul18fpkbpg0+peH$xyPBqb%`$OUhPKyWb32o7clB*9Z< zN=i~NLjavrLtwgJ01bufP+>p-jR2I95|TpmKpQL2!oV>g(4RvS2pK4*ou%m(h6r3A zX#s&`9LU1ZG&;{CkOK!4fLDTnBys`M!vuz>Q&9OZ0hGQl!~!jSDg|~s*w52opC{sB ze|Cf2luD(*G13LcOAGA!s2FjSK8&IE5#W%J25w!vM0^VyQM!t)inj&RTiJ!wXzFgz z3^IqzB7I0L$llljsGq})thBy9UOyjtFO_*hYM_sgcMk>44jeH0V1FDyELc{S1F-;A zS;T^k^~4biG&V*Irq}O;e}j$$+E_#G?HKIn05iP3j|87TkGK~SqG!-KBg5+mN(aLm z8ybhIM`%C19UX$H$KY6JgXbY$0AT%rEpHC;u`rQ$Y=rxUdsc5*Kvc8jaYaO$^)cI6){P6K0r)I6DY4Wr4&B zLQUBraey#0HV|&c4v7PVo3n$zHj99(TZO^3?Ly%C4nYvJTL9eLBLHsM3WKKD>5!B` zQ=BsR3aR6PD(Fa>327E2HAu5TM~Wusc!)>~(gM)+3~m;92Jd;FnSib=M5d6;;5{%R zb4V7DEJ0V!CP-F*oU?gkc>ksUtAYP&V4ND5J>J2^jt*vcFflQWCrB&fLdT%O59PVJ zhid#toR=FNgD!q3&r8#wEBr`!wzvQu5zX?Q>nlSJ4i@WC*CN*-xU66F^V5crWevQ9gsq$I@z1o(a=k7LL~ z7m_~`o;_Ozha1$8Q}{WBehvAlO4EL60y5}8GDrZ< zXh&F}71JbW2A~8KfEWj&UWV#4+Z4p`b{uAj4&WC zha`}X@3~+Iz^WRlOHU&KngK>#j}+_o@LdBC1H-`gT+krWX3-;!)6?{FBp~%20a}FL zFP9%Emqcwa#(`=G>BBZ0qZDQhmZKJg_g8<=bBFKWr!dyg(YkpE+|R*SGpDVU!+VlU zFC54^DLv}`qa%49T>nNiA9Q7Ips#!Xx90tCU2gvK`(F+GPcL=J^>No{)~we#o@&mUb6c$ zCc*<|NJBk-#+{j9xkQ&ujB zI~`#kN~7W!f*-}wkG~Ld!JqZ@tK}eeSnsS5J1fMFXm|`LJx&}5`@dK3W^7#Wnm+_P zBZkp&j1fa2Y=eIjJ0}gh85jt43kaIXXv?xmo@eHrka!Z|vQv12HN#+!I5E z`(fbuW>gFiJL|uXJ!vKt#z3e3HlVdboH7;e#i3(2<)Fg-I@BR!qY#eof3MFZ&*Y@l zI|KJf&ge@p2Dq09Vu$$Qxb7!}{m-iRk@!)%KL)txi3;~Z4Pb}u@GsW;ELiWeG9V51 znX#}B&4Y2E7-H=OpNE@q{%hFLxwIpBF2t{vPREa8_{linXT;#1vMRWjOzLOP$-hf( z>=?$0;~~PnkqY;~K{EM6Vo-T(0K{A0}VUGmu*hR z{tw3hvBN%N3G3Yw`X5Te+F{J`(3w1s3-+1EbnFQKcrgrX1Jqvs@ADGe%M0s$EbK$$ zK)=y=upBc6SjGYAACCcI=Y*6Fi8_jgwZlLxD26fnQfJmb8^gHRN5(TemhX@0e=vr> zg`W}6U>x6VhoA3DqsGGD9uL1DhB3!OXO=k}59TqD@(0Nb{)Ut_luTioK_>7wjc!5C zIr@w}b`Fez3)0wQfKl&bae7;PcTA7%?f2xucM0G)wt_KO!Ewx>F~;=BI0j=Fb4>pp zv}0R^xM4eti~+^+gE$6b81p(kwzuDti(-K9bc|?+pJEl@H+jSYuxZQV8rl8 zjp@M{#%qItIUFN~KcO9Hed*`$5A-2~pAo~K&<-Q+`9`$CK>rzqAI4w~$F%vs9s{~x zg4BP%Gy*@m?;D6=SRX?888Q6peF@_4Z->8wAH~Cn!R$|Hhq2cIzFYqT_+cDourHbY z0qroxJnrZ4Gh+Ay+F`_c%+KRT>y3qw{)89?=hJ@=KO=@ep)aBJ$c!JHfBMJpsP*3G za7|)VJJ8B;4?n{~ldJF7%jmb`-ftIvNd~ekoufG(`K(3=LNc;HBY& z(lp#q8XAD#cIf}k49zX_i`*fO+#!zKA&%T3j@%)R+#yag067CU%yUEe47>wzGU8^` z1EXFT^@I!{J!F8!X?S6ph8J=gUi5tl93*W>7}_uR<2N2~e}FaG?}KPyugQ=-OGEZs z!GBoyYY+H*ANn4?Z)X4l+7H%`17i5~zRlRIX?t)6_eu=g2Q`3WBhxSUeea+M-S?RL zX9oBGKn%a!H+*hx4d2(I!gsi+@SQK%<{X22M~2tMulJoa)0*+z9=-YO+;DFEm5eE1U9b^B(Z}2^9!Qk`!A$wUE z7$Ar5?NRg2&G!AZqnmE64eh^Anss3i!{}%6@Et+4rr!=}!SBF8eZ2*J3ujCWbl;3; z48H~goPSv(8X61fKKdpP!Z7$88NL^Z?j`!^*I?-P4X^pMxyWz~@$(UeAcTSDd(`vO z{~rc;9|GfMJcApU3k}22a!&)k4{CU!e_ny^Y3cO;tOvOMKEyWz!vG(Kp*;hB?d|R3`2X~=5a6#^o5@qn?J-bI8Ppip{-yG z!k|VcGsq!jF~}7DMr49Wap-s&>o=U^T0!Lcy}!(bhtYsPQy z4|EJe{12QL#=c(suQ89Mhw9<`bui%nx7Nep`C&*M3~vMEACmcRYYRGtANq$F%zh&V zc)cEVeHz*Z1N)L7k-(k3np#{GcDh2Q@ya0YHl*n7fl*ZPAsbU-a94MYYtA#&!c`xGIaV;yzsmrjfieTEtqB_WgZp2*NplHx=$O{M~2#i_vJ{ps-NgK zQsxKK_CBM2PP_je+Xft`(vYfXXgIUr{=PA=7a8`2EHk)Ym2QKIforz# tySWtj{oF3N9@_;i*Fv5S)9x^z=nlWP>jpp-9)52ZmLVA=i*%6g{{fxOO~wEK diff --git a/client_mobile/windows/runner/runner.exe.manifest b/client_mobile/windows/runner/runner.exe.manifest deleted file mode 100644 index a42ea76..0000000 --- a/client_mobile/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - - - - - - - diff --git a/client_mobile/windows/runner/utils.cpp b/client_mobile/windows/runner/utils.cpp deleted file mode 100644 index 3a0b465..0000000 --- a/client_mobile/windows/runner/utils.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - unsigned int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); - std::string utf8_string; - if (target_length == 0 || target_length > utf8_string.max_size()) { - return utf8_string; - } - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} diff --git a/client_mobile/windows/runner/utils.h b/client_mobile/windows/runner/utils.h deleted file mode 100644 index 3879d54..0000000 --- a/client_mobile/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/client_mobile/windows/runner/win32_window.cpp b/client_mobile/windows/runner/win32_window.cpp deleted file mode 100644 index 60608d0..0000000 --- a/client_mobile/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "win32_window.h" - -#include -#include - -#include "resource.h" - -namespace { - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registrar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - UpdateTheme(window); - - return OnCreate(); -} - -bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, - RRF_RT_REG_DWORD, nullptr, &light_mode, - &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} diff --git a/client_mobile/windows/runner/win32_window.h b/client_mobile/windows/runner/win32_window.h deleted file mode 100644 index e901dde..0000000 --- a/client_mobile/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates a win32 window with |title| that is positioned and sized using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_ From 2c605299ba407683a2238f9a3216f5680a3c8151 Mon Sep 17 00:00:00 2001 From: Mael-RABOT Date: Mon, 9 Dec 2024 16:10:42 +0100 Subject: [PATCH 14/26] evol(about): add ids to paylaod Signed-off-by: Mael-RABOT --- client_web/src/App.tsx | 4 ++- client_web/src/Context/CombinedProviders.tsx | 2 ++ client_web/src/Context/ContextHooks.ts | 15 ++++++++-- .../src/Context/Scopes/ErrorContext.tsx | 23 +++++++++++++++ client_web/src/Pages/Errors/CustomError.tsx | 28 +++++++++++++++++++ .../src/Pages/Workflows/CreateWorkflow.tsx | 28 ++++++++++++++----- 6 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 client_web/src/Context/Scopes/ErrorContext.tsx create mode 100644 client_web/src/Pages/Errors/CustomError.tsx diff --git a/client_web/src/App.tsx b/client_web/src/App.tsx index 8a0b4d9..0a7ffcd 100644 --- a/client_web/src/App.tsx +++ b/client_web/src/App.tsx @@ -16,7 +16,8 @@ import { loadSlim } from "@tsparticles/slim"; import Home from './Pages/Home'; import NotFound from './Pages/Errors/NotFound'; import ApiNotConnected from "@/Pages/Errors/ApiNotConnected"; -// @ts-ignore +import CustomError from "@/Pages/Errors/CustomError"; + import Layout from '@/Components/Layout/Layout'; import Login from './Pages/Auth/Forms/Login'; import Register from './Pages/Auth/Forms/Register'; @@ -162,6 +163,7 @@ const App = () => { } /> } /> + } /> } /> diff --git a/client_web/src/Context/CombinedProviders.tsx b/client_web/src/Context/CombinedProviders.tsx index c058035..df53de9 100644 --- a/client_web/src/Context/CombinedProviders.tsx +++ b/client_web/src/Context/CombinedProviders.tsx @@ -2,11 +2,13 @@ import { ReactNode } from 'react'; import { UserProvider } from './Scopes/UserContext'; import { ThemeProvider } from './Scopes/ThemeContext'; import { AuthProvider } from './Scopes/AuthContext'; +import { ErrorProvider } from './Scopes/ErrorContext'; const providers = [ UserProvider, ThemeProvider, AuthProvider, + ErrorProvider // Add more providers here ]; diff --git a/client_web/src/Context/ContextHooks.ts b/client_web/src/Context/ContextHooks.ts index 7eedb9e..3a8622b 100644 --- a/client_web/src/Context/ContextHooks.ts +++ b/client_web/src/Context/ContextHooks.ts @@ -1,7 +1,8 @@ import { useContext } from 'react'; -import { UserContext } from './Scopes/UserContext'; -import { ThemeContext } from './Scopes/ThemeContext'; -import { AuthContext } from './Scopes/AuthContext'; +import { UserContext } from '@/Context/Scopes/UserContext'; +import { ThemeContext } from '@/Context/Scopes/ThemeContext'; +import { AuthContext } from '@/Context/Scopes/AuthContext'; +import { ErrorContext } from "@/Context/Scopes/ErrorContext"; export const useUser = () => { const context = useContext(UserContext); @@ -26,3 +27,11 @@ export const useAuth = () => { } return context; }; + +export const useError = () => { + const context = useContext(ErrorContext); + if (!context) { + throw new Error('useError must be used within an ErrorProvider'); + } + return context; +} diff --git a/client_web/src/Context/Scopes/ErrorContext.tsx b/client_web/src/Context/Scopes/ErrorContext.tsx new file mode 100644 index 0000000..283b44d --- /dev/null +++ b/client_web/src/Context/Scopes/ErrorContext.tsx @@ -0,0 +1,23 @@ +import { createContext, useState, ReactNode } from 'react'; + +interface Error { + error: string; + errorDescription: string; +} + +export interface ErrorContextType { + error: Error | null; + setError: (error: Error | null) => void; +} + +export const ErrorContext = createContext(undefined); + +export const ErrorProvider = ({ children }: { children: ReactNode }) => { + const [error, setError] = useState(null); + + return ( + + {children} + + ); +} diff --git a/client_web/src/Pages/Errors/CustomError.tsx b/client_web/src/Pages/Errors/CustomError.tsx new file mode 100644 index 0000000..59e5d81 --- /dev/null +++ b/client_web/src/Pages/Errors/CustomError.tsx @@ -0,0 +1,28 @@ +import { Result } from 'antd'; +import LinkButton from "@/Components/LinkButton"; +import React from 'react'; +import {ErrorContext} from "@/Context/Scopes/ErrorContext"; +import { useError } from "@/Context/ContextHooks"; +import { useAuth } from "@/Context/ContextHooks"; + +const CustomError: () => React.ReactElement = () => { + const { error } = useError(); + const { isAuthenticated } = useAuth(); + + return ( + + } + style={{ zIndex: 1, position: 'relative' }} + /> + ); +}; + +export default CustomError; diff --git a/client_web/src/Pages/Workflows/CreateWorkflow.tsx b/client_web/src/Pages/Workflows/CreateWorkflow.tsx index baf68db..a9a4f28 100644 --- a/client_web/src/Pages/Workflows/CreateWorkflow.tsx +++ b/client_web/src/Pages/Workflows/CreateWorkflow.tsx @@ -6,7 +6,9 @@ import { normalizeName } from "@/Pages/Workflows/CreateWorkflow.utils"; import { About, Service, Action, Reaction, Workflow, Parameter, SelectedAction, SelectedReaction } from "@/types"; import { toast } from "react-toastify"; -import { instanceWithAuth, workflow as workflowRoute } from "@/Config/backend.routes"; +import { instanceWithAuth, workflow as workflowRoute, root } from "@/Config/backend.routes"; +import { useError } from "@/Context/ContextHooks"; +import { useNavigate } from "react-router-dom"; const { Title, Text } = Typography; const { Panel } = Collapse; @@ -180,9 +182,21 @@ const CreateWorkflow: React.FC = () => { const [activeActionKeys, setActiveActionKeys] = useState([]); const [activeReactionKeys, setActiveReactionKeys] = useState([]); + const { setError } = useError(); + + const navigate = useNavigate(); + React.useEffect(() => { setLoading(true); - setAbout(_data); // TODO: Fetch workflow from the server + instanceWithAuth.get(root.about) + .then((response) => { + setAbout(response?.data); + }) + .catch((error) => { + console.error(error); + setError({ error: "API Error", errorDescription: "Could not fetch server information" }); + navigate('/error/fetch'); + }); setLoading(false); }, []); @@ -335,7 +349,7 @@ const CreateWorkflow: React.FC = () => { return ( -
+
Create Workflow @@ -376,9 +390,9 @@ const CreateWorkflow: React.FC = () => { - + - + @@ -422,7 +436,7 @@ const CreateWorkflow: React.FC = () => { - +
{ - + From dc4980b973a731f148e48964ae0e122cdc656508 Mon Sep 17 00:00:00 2001 From: Mael-RABOT Date: Mon, 9 Dec 2024 17:00:54 +0100 Subject: [PATCH 15/26] feat(user): add user page Signed-off-by: Mael-RABOT --- client_web/src/App.tsx | 6 +- .../src/Components/Auth/OAuthButtons.tsx | 30 +++-- client_web/src/Components/Layout/Header.tsx | 75 +++++++++--- client_web/src/Components/Layout/Layout.tsx | 2 +- client_web/src/Context/Scopes/UserContext.tsx | 2 +- client_web/src/Pages/Account/UserPage.tsx | 115 ++++++++++++++++++ client_web/src/Pages/Home.tsx | 40 +----- 7 files changed, 208 insertions(+), 62 deletions(-) create mode 100644 client_web/src/Pages/Account/UserPage.tsx diff --git a/client_web/src/App.tsx b/client_web/src/App.tsx index 0a7ffcd..f37429f 100644 --- a/client_web/src/App.tsx +++ b/client_web/src/App.tsx @@ -18,6 +18,8 @@ import NotFound from './Pages/Errors/NotFound'; import ApiNotConnected from "@/Pages/Errors/ApiNotConnected"; import CustomError from "@/Pages/Errors/CustomError"; +import UserPage from "@/Pages/Account/UserPage"; + import Layout from '@/Components/Layout/Layout'; import Login from './Pages/Auth/Forms/Login'; import Register from './Pages/Auth/Forms/Register'; @@ -148,7 +150,7 @@ const App = () => { - } /> + } /> } /> } /> } /> @@ -162,6 +164,8 @@ const App = () => { } /> } /> + } /> + } /> } /> } /> diff --git a/client_web/src/Components/Auth/OAuthButtons.tsx b/client_web/src/Components/Auth/OAuthButtons.tsx index b6379e4..ff3189e 100644 --- a/client_web/src/Components/Auth/OAuthButtons.tsx +++ b/client_web/src/Components/Auth/OAuthButtons.tsx @@ -6,7 +6,7 @@ import DiscordAuth from './Buttons/DiscordAuth'; import { Divider } from 'antd'; interface OAuthButtonsProps { - mode: 'signin' | 'signup'; + mode: 'signin' | 'signup' | 'connect'; onGoogleSuccess: (response: unknown) => void; onGoogleError: () => void; onMicrosoftSuccess: (response: unknown) => void; @@ -24,34 +24,50 @@ const OAuthButtons = ({ onLinkedinSuccess, onLinkedinError }: OAuthButtonsProps) => { + let withText = ""; + switch (mode) { + case 'signin': + withText = "Sign in with"; + break; + case 'signup': + withText = "Sign up with"; + break; + case 'connect': + withText = "Connect with"; + break; + default: + withText = "Use"; + break; + } + return ( <> - Or + {mode !== 'connect' && Or} ); diff --git a/client_web/src/Components/Layout/Header.tsx b/client_web/src/Components/Layout/Header.tsx index 6aefa9e..c726a16 100644 --- a/client_web/src/Components/Layout/Header.tsx +++ b/client_web/src/Components/Layout/Header.tsx @@ -1,26 +1,33 @@ -import { Layout, Menu, Button } from 'antd'; +import { Layout, Menu, Button, Dropdown, Avatar } from 'antd'; import { Link, useLocation } from 'react-router-dom'; import { useAuth, useTheme } from '@/Context/ContextHooks'; import React, { useEffect, useState } from "react"; +import { UserOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; const { Header: AntHeader } = Layout; interface MenuItems { key: string; label: React.ReactNode; + auth: boolean; } const menuItems: MenuItems[] = [ - { key: '/', label: Home }, - { key: '/dashboard', label: Dashboard }, + { key: '/', label: Home, auth: false }, + { key: '/login', label: Login, auth: false }, + { key: '/register', label: Register, auth: false }, + { key: '/dashboard', label: Dashboard, auth: true }, + { key: '/workflow/create', label: Create Workflow, auth: true }, ]; const Header: React.FC = () => { const { theme } = useTheme(); + const navigate = useNavigate(); const location = useLocation(); const [selectedKey, setSelectedKey] = useState(location.pathname); - const { setIsAuthenticated, setJsonWebToken } = useAuth(); + const { isAuthenticated, setIsAuthenticated, setJsonWebToken } = useAuth(); useEffect(() => { setSelectedKey(location.pathname); @@ -33,6 +40,27 @@ const Header: React.FC = () => { window.location.href = '/'; } + const profileMenuItems = [ + { + key: 'profile', + label: 'Profile Settings' + }, + { + key: 'logout', + label: 'Logout', + danger: true + } + ]; + + const handleMenuClick = ({ key }: { key: string }) => { + if (key === 'logout') { + handleLogout(); + } + if (key === 'profile') { + navigate('/account/me'); + } + }; + return (
@@ -40,22 +68,37 @@ const Header: React.FC = () => { theme={theme} mode="horizontal" style={{ flex: 1 }} - items={menuItems} + items={menuItems.filter(item => !item.auth || isAuthenticated)} selectedKeys={[selectedKey]} /> - + +
); diff --git a/client_web/src/Components/Layout/Layout.tsx b/client_web/src/Components/Layout/Layout.tsx index f5d1934..26e582f 100644 --- a/client_web/src/Components/Layout/Layout.tsx +++ b/client_web/src/Components/Layout/Layout.tsx @@ -9,7 +9,7 @@ interface LayoutProps { const Layout: React.FC = ({ children }) => { const location = useLocation(); - const headerIncludedPaths: (string | RegExp)[] = ['/', '/dashboard', '/workflow/create', '/workflows']; + const headerIncludedPaths: (string | RegExp)[] = ['/', '/dashboard', '/account/me', '/workflow/create', '/workflows']; const footerIncludedPaths: (string | RegExp)[] = ['/', '/dashboard']; const isHeaderIncluded = headerIncludedPaths.some(path => diff --git a/client_web/src/Context/Scopes/UserContext.tsx b/client_web/src/Context/Scopes/UserContext.tsx index 6981a53..22fd3f8 100644 --- a/client_web/src/Context/Scopes/UserContext.tsx +++ b/client_web/src/Context/Scopes/UserContext.tsx @@ -2,7 +2,7 @@ import { createContext, useState, ReactNode } from 'react'; interface User { id: string; - name: string; + username: string; } export interface UserContextType { diff --git a/client_web/src/Pages/Account/UserPage.tsx b/client_web/src/Pages/Account/UserPage.tsx new file mode 100644 index 0000000..29796b4 --- /dev/null +++ b/client_web/src/Pages/Account/UserPage.tsx @@ -0,0 +1,115 @@ +import React, { useState } from "react"; +import { Layout, Button, Typography, Space, Card, Row, Col } from "antd"; +import { useNavigate } from "react-router-dom"; +// @ts-expect-error +import { BlockPicker } from "react-color"; +import { useAuth, useUser } from "@/Context/ContextHooks"; +import OAuthButtons from "@/Components/Auth/OAuthButtons"; +import {toast} from "react-toastify"; +import Security from "@/Components/Security"; + +const { Header, Content } = Layout; +const { Title, Text } = Typography; + +interface UserPageProps { + backgroundColor: string; + setBackgroundColor: (color: string) => void; +} + +const UserPage: React.FC = ({ backgroundColor, setBackgroundColor }) => { + const navigate = useNavigate(); + const [tempColor, setTempColor] = useState(backgroundColor); + + const { setJsonWebToken, setIsAuthenticated } = useAuth(); + const { user } = useUser(); + + const handleLogout = () => { + localStorage.removeItem("JsonWebToken"); + setJsonWebToken(""); + setIsAuthenticated(false); + navigate("/"); + }; + + const handleColorChange = (color: any) => { + setTempColor(color.hex); + sessionStorage.setItem('backgroundColor', color.hex); + setBackgroundColor(color.hex); + }; + + const handleDefaultColor = () => { + const defaultColor = "#FFA500"; + setTempColor(defaultColor); + sessionStorage.setItem('backgroundColor', defaultColor); + setBackgroundColor(defaultColor); + }; + + const handleNotImplemented = () => { + toast.error("This feature is not implemented yet"); + }; + + return ( + +
+ + Create Workflow + + + + + + + + Welcome, {user?.username || 'User'}! + Manage your account settings and connected services here. + + + + + + + + Customize your background color +
+ +
+ +
+
+ + + + + + Connect your accounts to enhance your experience + + + + +
+ + + + Ready to leave? + + + +
+
+
+
+ ); +}; + +export default UserPage; diff --git a/client_web/src/Pages/Home.tsx b/client_web/src/Pages/Home.tsx index a730599..f4e3cc4 100644 --- a/client_web/src/Pages/Home.tsx +++ b/client_web/src/Pages/Home.tsx @@ -1,9 +1,7 @@ import React from 'react'; -import { Layout, Typography, Row, Col, Button, Card, Modal } from 'antd'; +import { Layout, Typography, Row, Col, Button, Card } from 'antd'; import { ThunderboltOutlined, ApiOutlined, SafetyCertificateOutlined } from '@ant-design/icons'; import { motion } from 'framer-motion'; -// @ts-ignore -import { BlockPicker } from 'react-color'; import { instance, root } from "@Config/backend.routes"; import { toast } from "react-toastify"; import { useNavigate } from "react-router-dom"; @@ -14,40 +12,18 @@ const { Title, Paragraph } = Typography; interface HomeProps { backgroundColor: string; - setBackgroundColor: (color: string) => void; } -const Home: React.FC = ({ backgroundColor, setBackgroundColor }) => { - const [isModalVisible, setIsModalVisible] = React.useState(false); - const [tempColor, setTempColor] = React.useState(backgroundColor); +const Home: React.FC = ({ backgroundColor }) => { const [pingResponse, setPingResponse] = React.useState(false); const hasPinged = React.useRef(false); const navigate = useNavigate(); const { isAuthenticated } = useAuth(); - const handleColorChange = (color: { hex: any; }) => { - setTempColor(color.hex); - }; - - const showModal = () => { - setTempColor(backgroundColor); - setIsModalVisible(true); - }; - - const handleOk = () => { - setBackgroundColor(tempColor); - sessionStorage.setItem('backgroundColor', tempColor); - setIsModalVisible(false); - }; - - const handleCancel = () => { - setIsModalVisible(false); - }; - const ping = () => { - const response = instance.get(root.ping) - .then((response) => { + instance.get(root.ping) + .then((_) => { setPingResponse(true); }) .catch((error) => { @@ -174,15 +150,7 @@ const Home: React.FC = ({ backgroundColor, setBackgroundColor }) => { ))}
-
- -
- - - ); }; From 354428e0b73d2e8b2b24ef7848bca102e0e00381 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Thu, 5 Dec 2024 23:07:23 +0100 Subject: [PATCH 16/26] feat(token): add token model Signed-off-by: Mathieu --- client_web/src/Config/backend.routes.ts | 5 ++ client_web/src/Pages/Auth/Forms/Login.tsx | 28 +++++-- client_web/src/Pages/Auth/Forms/Register.tsx | 26 +++++- server/build/rabbit-mq/rabbitmq.conf | 1 - server/internal/controllers/auth.go | 2 +- server/internal/controllers/oauth.go | 84 ++++++++++++++++++- .../internal/controllers/oauth/microsoft.go | 77 +++++++++++++++++ server/internal/models/event.go | 1 + server/internal/models/service.go | 5 +- server/internal/models/token.go | 11 +++ server/internal/models/user.go | 8 +- server/internal/models/workflow.go | 17 +++- server/internal/pkg/db.go | 3 + server/internal/pkg/token.go | 9 ++ server/internal/routers/routers.go | 12 +-- 15 files changed, 259 insertions(+), 30 deletions(-) delete mode 100644 server/build/rabbit-mq/rabbitmq.conf create mode 100644 server/internal/controllers/oauth/microsoft.go create mode 100644 server/internal/models/token.go diff --git a/client_web/src/Config/backend.routes.ts b/client_web/src/Config/backend.routes.ts index f1165bc..8e1b7e4 100644 --- a/client_web/src/Config/backend.routes.ts +++ b/client_web/src/Config/backend.routes.ts @@ -42,6 +42,10 @@ const auth = { health: `${endpoint}/auth/health`, } +const oauth = { + microsoft: `${endpoint}/oauth/microsoft`, +} + const workflow = { create: `${endpoint}/workflow/create`, } @@ -51,5 +55,6 @@ export { instanceWithAuth, root, auth, + oauth, workflow } diff --git a/client_web/src/Pages/Auth/Forms/Login.tsx b/client_web/src/Pages/Auth/Forms/Login.tsx index eda0da3..41ac89c 100644 --- a/client_web/src/Pages/Auth/Forms/Login.tsx +++ b/client_web/src/Pages/Auth/Forms/Login.tsx @@ -1,10 +1,10 @@ import {Form, Input, Button, Card } from 'antd'; import { Link } from 'react-router-dom'; import OAuthButtons from '../../../Components/Auth/OAuthButtons'; -import { instance, auth } from "@Config/backend.routes"; +import { instance, auth, oauth } from "@Config/backend.routes"; import { useAuth } from "@/Context/ContextHooks"; import { useNavigate } from 'react-router-dom'; -import {toast} from "react-toastify"; +import { toast } from "react-toastify"; const Login = () => { const { setJsonWebToken, isAuthenticated, setIsAuthenticated } = useAuth(); @@ -43,10 +43,28 @@ const Login = () => { }; const handleMicrosoftSuccess = (response: unknown) => { - console.log('Microsoft Login Success:', response); - // Call your API to verify the Microsoft token and log in the user + // @ts-expect-error response isn't typed + instance.post(oauth.microsoft, { "token": response?.accessToken }, { + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((response) => { + if (!response?.data?.jwt) { + console.error('JWT not found in response'); + return; + } + localStorage.setItem('jsonWebToken', response?.data?.jwt); + setJsonWebToken(response?.data?.jwt); + setIsAuthenticated(true); + navigate('/dashboard'); + }) + .catch((error) => { + console.error('Failed:', error); + toast.error('Failed to register: ' + (error?.response?.data?.error || 'Network error')); + }); }; - + const handleMicrosoftError = (error: unknown) => { console.error('Microsoft Login Failed:', error); }; diff --git a/client_web/src/Pages/Auth/Forms/Register.tsx b/client_web/src/Pages/Auth/Forms/Register.tsx index 528aab9..721b807 100644 --- a/client_web/src/Pages/Auth/Forms/Register.tsx +++ b/client_web/src/Pages/Auth/Forms/Register.tsx @@ -1,7 +1,7 @@ import { Form, Input, Button, Card } from 'antd'; import { Link } from 'react-router-dom'; import OAuthButtons from '@/Components/Auth/OAuthButtons'; -import { instance, auth } from "@Config/backend.routes"; +import { instance, auth, oauth } from "@Config/backend.routes"; import { useAuth } from "@/Context/ContextHooks"; import { useNavigate } from 'react-router-dom'; import {toast} from "react-toastify"; @@ -45,12 +45,32 @@ const Register = () => { }; const handleMicrosoftSuccess = (response: unknown) => { - console.log('Microsoft Register Success:', response); - // Call your API to verify the Microsoft token and register the user + // @ts-expect-error response isn't typed + instance.post(oauth.microsoft, { "token": response?.accessToken }, { + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((response) => { + if (!response?.data?.jwt) { + console.error('JWT not found in response'); + return; + } + localStorage.setItem('jsonWebToken', response?.data?.jwt); + setJsonWebToken(response?.data?.jwt); + setIsAuthenticated(true); + navigate('/dashboard'); + }) + .catch((error) => { + console.error('Failed:', error); + toast.error('Failed to register: ' + (error?.response?.data?.error || 'Network error')); + }); }; const handleMicrosoftError = (error: unknown) => { console.error('Microsoft Register Failed:', error); + // @ts-expect-error error isn't typed + toast.error('Failed to register: ' + error?.response?.data?.error); }; const handleLinkedinSuccess = (response: unknown) => { diff --git a/server/build/rabbit-mq/rabbitmq.conf b/server/build/rabbit-mq/rabbitmq.conf deleted file mode 100644 index 3b1a462..0000000 --- a/server/build/rabbit-mq/rabbitmq.conf +++ /dev/null @@ -1 +0,0 @@ -listeners.tcp.default = 5673 \ No newline at end of file diff --git a/server/internal/controllers/auth.go b/server/internal/controllers/auth.go index 61acd6e..b418048 100644 --- a/server/internal/controllers/auth.go +++ b/server/internal/controllers/auth.go @@ -40,7 +40,7 @@ func Login(c *gin.Context) { } tokenString := utils.NewToken(c, LoginData.Email) db.DB.Model(&user).Update("token", tokenString) - c.JSON(http.StatusOK, gin.H{"jwt": tokenString}) + c.JSON(http.StatusOK, gin.H{"jwt": tokenString}) } // Register godoc diff --git a/server/internal/controllers/oauth.go b/server/internal/controllers/oauth.go index 1d1e75d..1d16b50 100644 --- a/server/internal/controllers/oauth.go +++ b/server/internal/controllers/oauth.go @@ -1,11 +1,87 @@ package controllers import ( + "AREA/internal/controllers/oauth" + "AREA/internal/models" + "AREA/internal/pkg" + "errors" + "net/http" + "github.com/gin-gonic/gin" ) -func Google(c *gin.Context) { - c.JSON(200, gin.H{ - "message": "Google", - }) +type Token struct { + Token string `json:"token" binding:"required"` +} + +type OAuthCallback func(c *gin.Context, token *models.Token) (*models.User, error) + +var OAuthCallbacks = map[string]OAuthCallback { + //"google": googleCallback, + "microsoft": oauth.MicrosoftCallback, +} + +func getServiceID(c *gin.Context) (uint, error) { + serviceId, err := pkg.GetServiceFromName(c.Param("service")) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H { + "message": "Service not found", + }) + return 0, errors.New("Invalid request") + } + return serviceId, nil +} + +func OAuth(c *gin.Context) { + var token Token + if err := c.ShouldBindJSON(&token); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H { + "message": "Invalid request", + }) + } + + + serviceId, err := getServiceID(c) + if err != nil { + return + } + + var dbToken models.Token + dbToken.Value = token.Token + dbToken.ServiceID = serviceId + user, err := OAuthCallbacks[c.Param("service")](c, &dbToken) + dbToken.UserID = user.ID + if err != nil { + return + } + + c.JSON(http.StatusOK, gin.H{"username": user.Username, "email": user.Email, "jwt": user.Token}) } + +/*func OAuthBind(c *gin.Context) { + var token Token + if err := c.ShouldBindJSON(&token); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H { + "message": "Invalid request", + }) + return + } + userId, err := pkg.GetUserFromToken(c) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H { + "message": "User not found", + }) + } + serviceId, err := pkg.GetServiceFromName(c.Param("service")) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H { + "message": "Service not found", + }) + return + } + + var dbToken models.Token + dbToken.Value = token.Token + dbToken.UserID = userId + dbToken.ServiceID = serviceId +}*/ diff --git a/server/internal/controllers/oauth/microsoft.go b/server/internal/controllers/oauth/microsoft.go new file mode 100644 index 0000000..00384f1 --- /dev/null +++ b/server/internal/controllers/oauth/microsoft.go @@ -0,0 +1,77 @@ +package oauth + +import ( + "AREA/internal/models" + "AREA/internal/pkg" + "AREA/internal/utils" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type MicrosoftResponse struct { + Mail string `json:"mail"` + DisplayName string `json:"displayName"` +} + +func createAccount(c *gin.Context, response MicrosoftResponse, token *models.Token) (*models.User) { + var user models.User + user.Email = token.Email + user.Username = response.DisplayName + + user.Token = utils.NewToken(c, user.Email) + + pkg.DB.Create(&user) + token.UserID = user.ID + pkg.DB.Create(&token) + return &user +} + +func getBindedAccount(c *gin.Context, response MicrosoftResponse, token *models.Token) (*models.User, error) { + serviceId, _ := pkg.GetServiceFromName(c.Param("service")) + err := pkg.DB.Where("email = ? AND service_id = 3", token.Email, serviceId).First(token).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + return createAccount(c, response, token), nil + } else { + var user models.User + pkg.DB.Where("id = ?", token.UserID).First(&user) + pkg.DB.Where("email = ?", token.Email).First(token).Update("value", token.Value) + user.Token = utils.NewToken(c, user.Email) + pkg.DB.UpdateColumns(&user) + return &user, nil + } +} + +func MicrosoftCallback(c *gin.Context, token *models.Token) (*models.User, error) { + httpRequestUrl := "https://graph.microsoft.com/v1.0/me" + req, err := http.NewRequest("GET", httpRequestUrl, nil) + if err != nil { + fmt.Println(err) + err := errors.New("Error creating request") + return nil, err + + } + req.Header.Set("Authorization", "Bearer " + token.Value) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println(err) + return nil, errors.New("Error executing request") + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println(err) + return nil, errors.New("Error reading request") + } + var response MicrosoftResponse + json.Unmarshal([]byte(b), &response) + token.Email = response.Mail + return getBindedAccount(c, response, token) +} diff --git a/server/internal/models/event.go b/server/internal/models/event.go index b5e9545..6bf6c17 100644 --- a/server/internal/models/event.go +++ b/server/internal/models/event.go @@ -11,6 +11,7 @@ type Event struct { ServiceID uint `gorm:"foreignKey:ServiceID" json:"service_id"` Parameters []Parameters `json:"parameters"` Type EventType `gorm:"type:enum('action', 'reaction');not null" json:"type"` + WorkflowEvents []WorkflowEvent `gorm:"constraint:OnDelete:CASCADE;" json:"workflow_events"` } const ( diff --git a/server/internal/models/service.go b/server/internal/models/service.go index e0c3ed7..98d127b 100644 --- a/server/internal/models/service.go +++ b/server/internal/models/service.go @@ -4,6 +4,7 @@ import "gorm.io/gorm" type Service struct { gorm.Model - Name string `json:"name"` - Events []Event `gorm:"constraint:OnDelete:CASCADE;" json:"events"` + Name string `gorm:"unique;not null" json:"name"` + Events []Event `gorm:"constraint:OnDelete:CASCADE;" json:"events"` + Tokens []Token `gorm:"constraint:OnDelete:CASCADE;" json:"tokens"` } diff --git a/server/internal/models/token.go b/server/internal/models/token.go new file mode 100644 index 0000000..9120075 --- /dev/null +++ b/server/internal/models/token.go @@ -0,0 +1,11 @@ +package models + +import "gorm.io/gorm" + +type Token struct { + gorm.Model + Value string + Email string + ServiceID uint + UserID uint +} diff --git a/server/internal/models/user.go b/server/internal/models/user.go index 4a6c48e..f132a38 100644 --- a/server/internal/models/user.go +++ b/server/internal/models/user.go @@ -6,9 +6,11 @@ import ( type User struct { gorm.Model - Email string `gorm:"unique;not null" json:"email" binding:"required"` - Username string `gorm:"unique;not null" json:"username" binding:"required"` + Email string `json:"email" binding:"required"` + Username string `gorm:"unique" json:"username" binding:"required"` Password string `gorm:"not null" json:"password" binding:"required"` Salt string `gorm:"not null" json:"salt"` - Token string `gorm:"not null" json:"token"` + Token string `gorm:"not null" json:"token"` + Workflows []Workflow `gorm:"constraint:OnDelete:CASCADE;"` + Tokens []Token `gorm:"constraint:OnDelete:CASCADE;"` } diff --git a/server/internal/models/workflow.go b/server/internal/models/workflow.go index 8cdc29c..8567e09 100644 --- a/server/internal/models/workflow.go +++ b/server/internal/models/workflow.go @@ -15,6 +15,14 @@ type Parameters struct { Description string `json:"description"` Type string `json:"type"` EventID uint `gorm:"foreignKey:EventID" json:"event_id"` + ParametersValues []ParametersValue `gorm:"constraint:OnDelete:CASCADE;" json:"parameters_values"` +} + +type ParametersValue struct { + gorm.Model + ParametersID uint `gorm:"foreignKey:ParametersID" json:"parameters_id"` + WorkflowEventID uint `gorm:"foreignKey:WorkflowEventID" json:"workflow_event_id"` + Value string `gorm:"not null" json:"value"` } type Workflow struct { @@ -24,7 +32,14 @@ type Workflow struct { Description string `json:"description"` Status WorkflowStatus `gorm:"type:enum('pending', 'processed', 'failed')" json:"status"` IsActive bool `json:"is_active"` - Events []Event `gorm:"many2many:workflow_events" json:"events"` + WorkflowEvents []WorkflowEvent `gorm:"constraint:OnDelete:CASCADE;" json:"workflow_events"` +} + +type WorkflowEvent struct { + gorm.Model + WorkflowID uint `gorm:"foreignKey:WorkflowID" json:"workflow_id"` + EventID uint `gorm:"foreignKey:EventID" json:"event_id"` + ParametersValues []ParametersValue `gorm:"constraint:OnDelete:CASCADE;" json:"parameters_values"` } func (w *Workflow) BeforeCreate(tx *gorm.DB) (err error) { diff --git a/server/internal/pkg/db.go b/server/internal/pkg/db.go index 56ec36e..d080559 100644 --- a/server/internal/pkg/db.go +++ b/server/internal/pkg/db.go @@ -18,6 +18,9 @@ func migrateDB() error { &models.Service{}, &models.Event{}, &models.Parameters{}, + &models.Token{}, + &models.WorkflowEvent{}, + &models.ParametersValue{}, ) if err != nil { log.Fatalf("Failed to migrate DB: %v", err) diff --git a/server/internal/pkg/token.go b/server/internal/pkg/token.go index 1767c74..89909f1 100644 --- a/server/internal/pkg/token.go +++ b/server/internal/pkg/token.go @@ -4,6 +4,7 @@ import ( "AREA/internal/models" "AREA/internal/utils" "errors" + "github.com/gin-gonic/gin" ) @@ -19,3 +20,11 @@ func GetUserFromToken(c *gin.Context) (uint, error) { } return user.ID, nil } + +func GetServiceFromName(serviceName string) (uint, error) { + var service models.Service + if err := DB.Where("name = ?", serviceName).First(&service).Error; err != nil { + return 0, errors.New("Service not found") + } + return service.ID, nil +} diff --git a/server/internal/routers/routers.go b/server/internal/routers/routers.go index 2536527..458810a 100644 --- a/server/internal/routers/routers.go +++ b/server/internal/routers/routers.go @@ -11,16 +11,8 @@ import ( ) func setUpOauthGroup(router *gin.Engine) { - oauth := router.Group("/oauth") - { - - oauth.POST("/google", controllers.Google) - /*oauth.POST("/spotify", controllers.Spotify) - oauth.POST("/github", controllers.Github) - oauth.POST("/linkedin", controllers.Linkedin) - oauth.POST("/discord", controllers.Discord) - auth.POST("/twitch", controllers.Twitch)*/ - } + router.POST("/oauth/:service", controllers.OAuth) + //router.POST("/oauth/bind/:service", controllers.OAuthBind) } func setUpAuthGroup(router *gin.Engine) { From 9ab856f69e8b8e9778746bf5842702f59d74a640 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Mon, 9 Dec 2024 18:10:32 +0100 Subject: [PATCH 17/26] hotfix(rabbitmq): fix rabbitmq build Signed-off-by: Mathieu --- server/build/rabbit-mq/rabbitmq.conf | 1 + 1 file changed, 1 insertion(+) create mode 100644 server/build/rabbit-mq/rabbitmq.conf diff --git a/server/build/rabbit-mq/rabbitmq.conf b/server/build/rabbit-mq/rabbitmq.conf new file mode 100644 index 0000000..bf17b36 --- /dev/null +++ b/server/build/rabbit-mq/rabbitmq.conf @@ -0,0 +1 @@ +listeners.tcp.default = 5673 From f46de8f4f2e674169cedcb1eada74e2b62044ba8 Mon Sep 17 00:00:00 2001 From: Mael-RABOT Date: Mon, 9 Dec 2024 20:42:59 +0100 Subject: [PATCH 18/26] feat(workflows): add table Signed-off-by: Mael-RABOT --- client_web/.env.local.example | 4 +- client_web/package-lock.json | 57 +++++++ client_web/package.json | 2 + client_web/src/App.tsx | 2 - .../Components/LoadingDots/LoadingDots.css | 26 +++ .../Components/LoadingDots/LoadingDots.tsx | 46 ++++++ client_web/src/Components/Security.tsx | 5 +- .../Components/Workflow/WorkflowsTable.tsx | 153 ++++++++++++++++++ client_web/src/Config/backend.routes.ts | 2 + client_web/src/Pages/Dashboard/Dashboard.tsx | 143 +++++++++++++--- .../src/Pages/Workflows/CreateWorkflow.tsx | 2 +- .../src/Pages/Workflows/WorkflowsTable.tsx | 51 ------ client_web/src/types.ts | 15 ++ 13 files changed, 425 insertions(+), 83 deletions(-) create mode 100644 client_web/src/Components/LoadingDots/LoadingDots.css create mode 100644 client_web/src/Components/LoadingDots/LoadingDots.tsx create mode 100644 client_web/src/Components/Workflow/WorkflowsTable.tsx delete mode 100644 client_web/src/Pages/Workflows/WorkflowsTable.tsx diff --git a/client_web/.env.local.example b/client_web/.env.local.example index 965d1d9..d62db11 100644 --- a/client_web/.env.local.example +++ b/client_web/.env.local.example @@ -12,6 +12,8 @@ VITE_LINKEDIN_CLIENT_SECRET= VITE_SPOTIFY_CLIENT_ID= VITE_SPOTIFY_CLIENT_SECRET= +VITE_DISCORD_CLIENT_ID= + # Server URLs API_URL=http://localhost:8080 WEB_CLIENT_URL=http://localhost:8081 @@ -21,4 +23,4 @@ MOBILE_CLIENT_URL=http://localhost:8082 GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret -# Add other environment variables as needed \ No newline at end of file +# Add other environment variables as needed diff --git a/client_web/package-lock.json b/client_web/package-lock.json index 3b8bfca..1d8555f 100644 --- a/client_web/package-lock.json +++ b/client_web/package-lock.json @@ -22,6 +22,7 @@ "react": "^18.3.1", "react-color": "^2.19.3", "react-dom": "^18.3.1", + "react-responsive": "^10.0.0", "react-router-dom": "^6.28.0", "react-toastify": "^10.0.6" }, @@ -30,6 +31,7 @@ "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "@types/react-responsive": "^8.0.8", "@vitejs/plugin-react": "^4.3.3", "electron": "^33.2.0", "eslint": "^9.13.0", @@ -2247,6 +2249,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-responsive": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/@types/react-responsive/-/react-responsive-8.0.8.tgz", + "integrity": "sha512-HDUZtoeFRHrShCGaND23HmXAB9evOOTjkghd2wAasLkuorYYitm5A1XLeKkhXKZppcMBxqB/8V4Snl6hRUTA8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -2939,6 +2951,12 @@ "node": ">= 8" } }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "license": "BSD" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3932,6 +3950,12 @@ "node": ">=10.19.0" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4216,6 +4240,15 @@ "node": ">=10" } }, + "node_modules/matchmediaquery": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz", + "integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==", + "license": "MIT", + "dependencies": { + "css-mediaquery": "^0.1.2" + } + }, "node_modules/material-colors": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", @@ -5284,6 +5317,24 @@ "node": ">=0.10.0" } }, + "node_modules/react-responsive": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.0.tgz", + "integrity": "sha512-N6/UiRLGQyGUqrarhBZmrSmHi2FXSD++N5VbSKsBBvWfG0ZV7asvUBluSv5lSzdMyEVjzZ6Y8DL4OHABiztDOg==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.4.2", + "prop-types": "^15.6.1", + "shallow-equal": "^3.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-router": { "version": "6.28.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", @@ -5525,6 +5576,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/client_web/package.json b/client_web/package.json index a2002d1..2ec4ecc 100644 --- a/client_web/package.json +++ b/client_web/package.json @@ -28,6 +28,7 @@ "react": "^18.3.1", "react-color": "^2.19.3", "react-dom": "^18.3.1", + "react-responsive": "^10.0.0", "react-router-dom": "^6.28.0", "react-toastify": "^10.0.6" }, @@ -36,6 +37,7 @@ "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "@types/react-responsive": "^8.0.8", "@vitejs/plugin-react": "^4.3.3", "electron": "^33.2.0", "eslint": "^9.13.0", diff --git a/client_web/src/App.tsx b/client_web/src/App.tsx index f37429f..7112680 100644 --- a/client_web/src/App.tsx +++ b/client_web/src/App.tsx @@ -30,7 +30,6 @@ import MicrosoftCallback from './Pages/Auth/Callback/MicrosoftCallback'; import DiscordCallback from './Pages/Auth/Callback/DiscordCallback'; import CreateWorkflow from "./Pages/Workflows/CreateWorkflow"; -import WorkflowsTable from "./Pages/Workflows/WorkflowsTable"; import Dashboard from './Pages/Dashboard/Dashboard'; @@ -161,7 +160,6 @@ const App = () => { } /> } /> - } /> } /> } /> diff --git a/client_web/src/Components/LoadingDots/LoadingDots.css b/client_web/src/Components/LoadingDots/LoadingDots.css new file mode 100644 index 0000000..dde1448 --- /dev/null +++ b/client_web/src/Components/LoadingDots/LoadingDots.css @@ -0,0 +1,26 @@ +.loading-dots { + display: inline-flex; + align-items: center; +} + +.dot { + border-radius: 50%; + animation: jump 1.5s ease-in-out infinite; +} + +.dot:nth-child(2) { + animation-delay: 0.2s; +} + +.dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes jump { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-6px); + } +} \ No newline at end of file diff --git a/client_web/src/Components/LoadingDots/LoadingDots.tsx b/client_web/src/Components/LoadingDots/LoadingDots.tsx new file mode 100644 index 0000000..7ac0ef6 --- /dev/null +++ b/client_web/src/Components/LoadingDots/LoadingDots.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import './LoadingDots.css'; + +interface LoadingDotsProps { + color?: string; + size?: number; +} + +const LoadingDots: React.FC = ({ + color = '#000000', + size = 4 +}) => { + return ( + + + + + + ); +}; + +export default LoadingDots; \ No newline at end of file diff --git a/client_web/src/Components/Security.tsx b/client_web/src/Components/Security.tsx index cc3f63d..e800a33 100644 --- a/client_web/src/Components/Security.tsx +++ b/client_web/src/Components/Security.tsx @@ -2,8 +2,7 @@ import React, { useState, useEffect } from "react"; import { useAuth } from "@/Context/ContextHooks"; import { useNavigate } from "react-router-dom"; import { Spin } from 'antd'; -import { instance, instanceWithAuth, root, auth } from "@Config/backend.routes"; -import {toast} from "react-toastify"; +import { instanceWithAuth, auth } from "@Config/backend.routes"; type SecurityProps = { children: React.ReactNode; @@ -13,8 +12,6 @@ const Security = ({ children }: SecurityProps) => { const { isAuthenticated, jsonWebToken, setIsAuthenticated, setJsonWebToken } = useAuth(); const navigate = useNavigate(); const [loading, setLoading] = useState(true); - const [_, setPingResponse] = React.useState(false); - const hasPinged = React.useRef(false); useEffect(() => { const checkAuth = () => { diff --git a/client_web/src/Components/Workflow/WorkflowsTable.tsx b/client_web/src/Components/Workflow/WorkflowsTable.tsx new file mode 100644 index 0000000..8b45959 --- /dev/null +++ b/client_web/src/Components/Workflow/WorkflowsTable.tsx @@ -0,0 +1,153 @@ +import React from "react"; +import { Table, Button, Space, Tooltip } from 'antd'; +import { EditOutlined, DeleteOutlined, PoweroffOutlined } from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import type { WorkflowTableDetail } from '@/types'; +import dayjs from 'dayjs'; +import { instanceWithAuth, workflow } from "@Config/backend.routes"; +import { toast } from "react-toastify"; +import { useMediaQuery } from 'react-responsive'; +import Security from "@/Components/Security"; + +interface WorkflowsTableProps { + workflows: WorkflowTableDetail[]; + setNeedReload: (value: boolean) => void; + loading: boolean; +} + +const WorkflowsTable: React.FC = ({ workflows, setNeedReload, loading }) => { + const isSmallScreen = useMediaQuery({ maxWidth: 767 }); + + const handleEdit = (record: WorkflowTableDetail) => { + console.log('Edit workflow:', record); + // TODO: Add edit logic here + }; + + const handleToggleActive = (record: WorkflowTableDetail) => { + console.log('Toggle active status:', record); + // TODO: Add toggle active logic here + }; + + const handleDelete = (record: WorkflowTableDetail) => { + instanceWithAuth.delete(workflow.delete + `/${record.ID}`) + .then(() => { + toast.success('Workflow deleted'); + }) + .catch((error) => { + console.error(error) + toast.error('Failed to delete workflow'); + }) + .finally(() => { + setNeedReload(true); + }) + }; + + const columns: ColumnsType = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + fixed: isSmallScreen ? undefined : 'left', + width: 200, + sorter: (a, b) => a.name.localeCompare(b.name), + }, + { + title: 'Description', + dataIndex: 'description', + key: 'description', + ellipsis: true, + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + filters: [...new Set(workflows.map(w => w.status))].map(status => ({ + text: status, + value: status, + })), + onFilter: (value, record) => record.status === value, + }, + { + title: 'Active', + dataIndex: 'is_active', + key: 'is_active', + render: (active: boolean) => active ? 'Yes' : 'No', + filters: [ + { text: 'Active', value: true }, + { text: 'Inactive', value: false }, + ], + onFilter: (value, record) => record.is_active === value, + }, + { + title: 'Created At', + dataIndex: 'CreatedAt', + key: 'CreatedAt', + render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'), + sorter: (a, b) => dayjs(a.CreatedAt).unix() - dayjs(b.CreatedAt).unix(), + }, + { + title: 'Updated At', + dataIndex: 'UpdatedAt', + key: 'UpdatedAt', + render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'), + sorter: (a, b) => dayjs(a.UpdatedAt).unix() - dayjs(b.UpdatedAt).unix(), + }, + { + title: 'Actions', + key: 'actions', + fixed: isSmallScreen ? undefined : 'right', + width: 150, + render: (_, record) => ( + + +