From f00ecb8155af1a86611c734c51e917bf01f54ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Benouarets?= Date: Mon, 11 Aug 2025 22:15:32 +0200 Subject: [PATCH] feat: Create collection file for routes --- .idea/dictionaries/project.xml | 8 ++ .idea/sqldialects.xml | 8 ++ .idea/tenante-api.iml | 4 + .idea/vcs.xml | 6 ++ .idea/workspace.xml | 151 ++++++++++++++++++++++++++++++ database/connection.go | 30 ++++++ go.mod | 5 + go.sum | 2 + main.go | 15 +++ server/api.go | 39 ++++++++ server/response.go | 13 +++ server/route.go | 24 +++++ sql/secnex_support.sql | 12 +++ sql/tenante_admin.sql | 165 +++++++++++++++++++++++++++++++++ 14 files changed, 482 insertions(+) create mode 100644 .idea/dictionaries/project.xml create mode 100644 .idea/sqldialects.xml create mode 100644 .idea/tenante-api.iml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 database/connection.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 server/api.go create mode 100644 server/response.go create mode 100644 server/route.go create mode 100644 sql/secnex_support.sql create mode 100644 sql/tenante_admin.sql diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 0000000..c52f068 --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,8 @@ + + + + ossp + secnex + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..992b820 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tenante-api.iml b/.idea/tenante-api.iml new file mode 100644 index 0000000..7ee078d --- /dev/null +++ b/.idea/tenante-api.iml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..ee545c5 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + {} + { + "isMigrated": true +} + { + "associatedIndex": 5 +} + + + + + + + { + "keyToString": { + "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", + "DefaultGoTemplateProperty": "Go File", + "Go Build.go build github.com/tenante/api.executor": "Run", + "Go Build.go build main.go.executor": "Run", + "Go Build.go build tenante-api main.go.executor": "Run", + "ModuleVcsDetector.initialDetectionPerformed": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", + "RunOnceActivity.git.unshallow": "true", + "RunOnceActivity.go.formatter.settings.were.checked": "true", + "RunOnceActivity.go.migrated.go.modules.settings": "true", + "RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true", + "SHARE_PROJECT_CONFIGURATION_FILES": "true", + "git-widget-placeholder": "main", + "go.import.settings.migrated": "true", + "go.sdk.automatically.set": "true", + "last_opened_file_path": "/Users/bbenouarets/Projects/secnex/tenante-api", + "node.js.detected.package.eslint": "true", + "node.js.detected.package.tslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", + "ts.external.directory.path": "/Users/bbenouarets/Projects/secnex/tenante-api/node_modules/typescript/lib", + "vue.rearranger.settings.migration": "true" + }, + "keyToStringList": { + "DatabaseDriversLRU": [ + "postgresql" + ] + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1754674133452 + + + + + + + + + + true + + \ No newline at end of file diff --git a/database/connection.go b/database/connection.go new file mode 100644 index 0000000..ed863ae --- /dev/null +++ b/database/connection.go @@ -0,0 +1,30 @@ +package database + +import ( + "database/sql" + "fmt" + + _ "github.com/lib/pq" +) + +type Connection struct { + Connection *sql.DB +} + +func NewConnection(host string, port int, user string, password string, dbName string, sslMode string) *Connection { + psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", host, port, user, password, dbName, sslMode) + db, err := sql.Open("postgres", psqlInfo) + if err != nil { + panic(err) + } + return &Connection{Connection: db} +} + +func (db *Connection) Close() error { + return db.Connection.Close() +} + +func (db *Connection) Ping() bool { + fmt.Println("🔥 Pinging database...") + return db.Connection.Ping() == nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e0ae418 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/tenante/api + +go 1.24.5 + +require github.com/lib/pq v1.10.9 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aeddeae --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4d71f69 --- /dev/null +++ b/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/tenante/api/database" + "github.com/tenante/api/server" +) + +func main() { + db := database.NewConnection("localhost", 5432, "postgres", "postgres", "tenante_admin_PRD", "disable") + api := server.NewApiServer(8081, db) + err := api.Start() + if err != nil { + panic(err) + } +} diff --git a/server/api.go b/server/api.go new file mode 100644 index 0000000..81b751a --- /dev/null +++ b/server/api.go @@ -0,0 +1,39 @@ +package server + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/tenante/api/database" +) + +type ApiServer struct { + Port int + DatabaseConnection *database.Connection +} + +func NewApiServer(port int, database *database.Connection) *ApiServer { + return &ApiServer{Port: port, DatabaseConnection: database} +} + +func (api *ApiServer) Start() error { + mux := http.NewServeMux() + + mux.HandleFunc("/_/health", api.HealthCheckRoute) + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", api.Port), + Handler: mux, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + fmt.Printf("🚀 Server starting on %s...\n", srv.Addr) + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} diff --git a/server/response.go b/server/response.go new file mode 100644 index 0000000..3f00577 --- /dev/null +++ b/server/response.go @@ -0,0 +1,13 @@ +package server + +type HealthCheckResponse struct { + Database bool `json:"database"` + API bool `json:"api"` + Status string `json:"status"` +} + +type InternalServerErrorResponse struct { + Status string `json:"status"` + Code int `json:"code"` + Message string `json:"message"` +} diff --git a/server/route.go b/server/route.go new file mode 100644 index 0000000..bd86be0 --- /dev/null +++ b/server/route.go @@ -0,0 +1,24 @@ +package server + +import ( + "encoding/json" + "net/http" +) + +func (api *ApiServer) HealthCheckRoute(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + response := HealthCheckResponse{ + Database: api.DatabaseConnection.Ping(), + API: true, + Status: "OK", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte("Internal Server Error")) + if err != nil { + return + } + } +} diff --git a/sql/secnex_support.sql b/sql/secnex_support.sql new file mode 100644 index 0000000..dc78467 --- /dev/null +++ b/sql/secnex_support.sql @@ -0,0 +1,12 @@ +-- CREATE DATABASE "secnex_PRD"; + +CREATE SCHEMA IF NOT EXISTS "support"; + +CREATE TABLE IF NOT EXISTS "public"."user" ( + "id" SERIAL PRIMARY KEY, + "name" VARCHAR(255) NOT NULL, + "email" VARCHAR(255) NOT NULL, + "password" VARCHAR(255) NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/sql/tenante_admin.sql b/sql/tenante_admin.sql new file mode 100644 index 0000000..76b8bfb --- /dev/null +++ b/sql/tenante_admin.sql @@ -0,0 +1,165 @@ +-- CREATE DATABASE "tenante_admin_PRD"; + +-- Drop all tables +DO $$ +DECLARE + tabName text; +BEGIN + FOR tabName IN + SELECT tablename + FROM pg_tables + WHERE schemaname = 'public' + LOOP + EXECUTE 'DROP TABLE IF EXISTS public."' || tabName || '" CASCADE'; + END LOOP; +END $$; + +-- Install uuid extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Create the ORGANIZATION table +CREATE TABLE IF NOT EXISTS "organization" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL UNIQUE, + slug TEXT NOT NULL UNIQUE, + "ssoConfigurationId" UUID, + "ssoDomain" UUID, + "ssoEnabled" BOOLEAN NOT NULL DEFAULT FALSE, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMPTZ +); + +-- Create the PROJECT table +CREATE TABLE IF NOT EXISTS "project" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMPTZ, + "organizationId" UUID REFERENCES organization(id) ON DELETE CASCADE, + CONSTRAINT unique_name_per_org UNIQUE ("organizationId", name), + CONSTRAINT unique_slug_per_org UNIQUE ("organizationId", slug) +); + +-- Create the USER table +CREATE TABLE IF NOT EXISTS "user" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "displayName" TEXT NOT NULL, + "username" TEXT NOT NULL UNIQUE, + "email" TEXT NOT NULL UNIQUE, + "password" TEXT NOT NULL, + "verified" BOOLEAN NOT NULL DEFAULT FALSE, + "verifiedAt" TIMESTAMPTZ, + "enabled" BOOLEAN NOT NULL DEFAULT TRUE, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMPTZ +); + +-- Create the ACCOUNT table +CREATE TABLE IF NOT EXISTS "account" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "userId" UUID REFERENCES "user"(id) ON DELETE CASCADE, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refreshToken" TEXT, + "accessToken" TEXT NOT NULL, + "accessTokenExpires" TIMESTAMPTZ NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMPTZ, + CONSTRAINT unique_provider_per_user UNIQUE ("userId", "provider"), + CONSTRAINT unique_provider_account_per_user UNIQUE ("userId", "provider", "providerAccountId") +); + +-- Create the MFA table to store MFA data +CREATE TABLE IF NOT EXISTS "mfa" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "userId" UUID REFERENCES "user"(id) ON DELETE CASCADE, + "secret" TEXT NOT NULL UNIQUE, + "enabled" BOOLEAN NOT NULL DEFAULT TRUE, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create ENUM for user roles +DROP TYPE IF EXISTS "user_role" CASCADE; +CREATE TYPE "user_role" AS ENUM ('owner', 'admin', 'member', 'guest'); + +-- Create the PROJECT_USER table to link users to projects and assign roles +CREATE TABLE IF NOT EXISTS "project_user" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "userId" UUID REFERENCES "user"(id) ON DELETE CASCADE, + "projectId" UUID REFERENCES "project"(id) ON DELETE CASCADE, + "role" "user_role" NOT NULL DEFAULT 'guest', + "createdBy" UUID REFERENCES "user"(id) ON DELETE SET NULL, + "updatedBy" UUID REFERENCES "user"(id) ON DELETE SET NULL, + "deletedBy" UUID REFERENCES "user"(id) ON DELETE SET NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMPTZ, + CONSTRAINT unique_user_per_project UNIQUE ("userId", "projectId"), + CONSTRAINT unique_role_per_user_per_project UNIQUE ("userId", "projectId", "role") +); + +-- Create the APP table to link apps to projects and assign roles +CREATE TABLE IF NOT EXISTS "app" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT, + "projectId" UUID REFERENCES "project"(id) ON DELETE CASCADE, + "createdBy" UUID REFERENCES "user"(id) ON DELETE SET NULL, + "updatedBy" UUID REFERENCES "user"(id) ON DELETE SET NULL, + "deletedBy" UUID REFERENCES "user"(id) ON DELETE SET NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMPTZ, + CONSTRAINT unique_name_per_project UNIQUE ("projectId", name), + CONSTRAINT unique_slug_per_project UNIQUE ("projectId", slug) +); + +DROP TYPE IF EXISTS "user_audit_action" CASCADE; +CREATE TYPE "user_audit_action" AS ENUM ('login', 'password_change', 'reset_password', '2fa_enable', '2fa_disable', '2fa_generate_backup', 'request_deletion', 'stop_deletion'); + +-- Create USER_AUDIT table (to log any user changes and activities e.g., login attempts, password changes, 2FA changes +CREATE TABLE IF NOT EXISTS "user_audit" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "action" "user_audit_action" NOT NULL DEFAULT 'login', + "ipAddress" TEXT NOT NULL, + "userAgent" TEXT NOT NULL, + "appId" UUID REFERENCES "app"(id) ON DELETE SET NULL, + "userId" UUID REFERENCES "user"(id) ON DELETE CASCADE, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMPTZ +); + +-- Create the SESSION table to save session data +CREATE TABLE IF NOT EXISTS "session" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "userId" UUID REFERENCES "user"(id) ON DELETE CASCADE, + "ipAddress" TEXT NOT NULL, + "userAgent" TEXT NOT NULL, + "refreshToken" TEXT NOT NULL, + "appId" UUID REFERENCES "app"(id) ON DELETE SET NULL, + "expiresAt" TIMESTAMPTZ NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create the EVENT table to save webhook data and more +CREATE TABLE IF NOT EXISTS "event" ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type TEXT NOT NULL, + payload JSONB NOT NULL DEFAULT '{}', + "appId" UUID REFERENCES "app"(id) ON DELETE SET NULL, + "userId" UUID REFERENCES "user"(id) ON DELETE SET NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); \ No newline at end of file