Merge pull request #45 from Oloodi/38-admin-scaffold-the-admin-console-web-application

38 admin scaffold the admin console web application
This commit is contained in:
Boris-Wilfried
2025-11-16 17:04:58 -05:00
committed by GitHub
40 changed files with 6685 additions and 108 deletions

View File

@@ -0,0 +1,80 @@
# Base44 Entity Schemas Reference
This document serves as a developer reference for the original data schemas from the Base44 backend. It is used to ensure feature parity and data integrity during the migration to the new GCP/Firebase backend.
---
## User Schema
```
role (text, required)
- The role of the user in the app
- Options: admin, user
email (text, required)
- The email of the user
full_name (text, required)
- Full name of the user
user_role (text)
- User's role in the system
- Options: admin, procurement, operator, sector, client, vendor, workforce
company_name (text)
- Company or organization name
profile_picture (text)
- URL to profile picture
phone (text)
- Phone number
address (text)
- Address
preferred_vendor_id (text)
- ID of the client's preferred/default vendor
preferred_vendor_name (text)
- Name of the client's preferred vendor
backup_vendor_ids (array)
- List of backup vendor IDs
dashboard_layout (object)
- User's customized dashboard layout preferences (legacy, kept for backward compatibility)
- widgets (array): Ordered list of visible widgets
- hidden_widgets (array): List of hidden widgets
- layout_version (text): Layout version for migration
dashboard_layout_client (object)
- Client dashboard layout
- widgets (array)
- hidden_widgets (array)
- layout_version (text)
dashboard_layout_vendor (object)
- Vendor dashboard layout
- widgets (array)
- hidden_widgets (array)
- layout_version (text)
dashboard_layout_operator (object)
- Operator dashboard layout
- widgets (array)
- hidden_widgets (array)
- layout_version (text)
dashboard_layout_workforce (object)
- Workforce dashboard layout
- widgets (array)
- hidden_widgets (array)
- layout_version (text)
preferences (object)
- User preferences and settings
- theme (text): Default: "light", Options: light, dark
- notifications_enabled (boolean): Default: true
- email_notifications (boolean): Default: true
```

278
Makefile
View File

@@ -4,7 +4,7 @@
# It is designed to be the main entry point for developers. # It is designed to be the main entry point for developers.
# Use .PHONY to declare targets that are not files, to avoid conflicts. # Use .PHONY to declare targets that are not files, to avoid conflicts.
.PHONY: install dev build prepare-export help deploy-launchpad deploy-app .PHONY: help install dev build integrate-export prepare-export deploy-launchpad deploy-launchpad-full deploy-app admin-install admin-dev admin-build deploy-admin deploy-admin-full configure-iap-launchpad configure-iap-admin list-iap-users remove-iap-user setup-labels export-issues create-issues-from-file install-git-hooks
# The default command to run if no target is specified (e.g., just 'make'). # The default command to run if no target is specified (e.g., just 'make').
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
@@ -12,9 +12,20 @@
# --- Firebase & GCP Configuration --- # --- Firebase & GCP Configuration ---
GCP_DEV_PROJECT_ID := krow-workforce-dev GCP_DEV_PROJECT_ID := krow-workforce-dev
GCP_STAGING_PROJECT_ID := krow-workforce-staging GCP_STAGING_PROJECT_ID := krow-workforce-staging
IAP_SERVICE_ACCOUNT := service-933560802882@gcp-sa-iap.iam.gserviceaccount.com
# --- Cloud Run Configuration ---
CR_LAUNCHPAD_SERVICE_NAME := internal-launchpad
CR_LAUNCHPAD_REGION := us-central1
CR_LAUNCHPAD_IMAGE_URI := us-docker.pkg.dev/$(GCP_DEV_PROJECT_ID)/gcr-io/$(CR_LAUNCHPAD_SERVICE_NAME)
CR_ADMIN_SERVICE_NAME := admin-console
CR_ADMIN_REGION := us-central1
CR_ADMIN_IMAGE_URI = us-docker.pkg.dev/$(GCP_PROJECT_ID)/gcr-io/$(CR_ADMIN_SERVICE_NAME)
# --- Environment Detection --- # --- Environment Detection ---
ENV ?= dev ENV ?= dev
SERVICE ?= launchpad # Default service for IAP commands: 'launchpad' or 'admin'
# --- Conditional Variables by Environment --- # --- Conditional Variables by Environment ---
ifeq ($(ENV),staging) ifeq ($(ENV),staging)
@@ -27,26 +38,230 @@ else
HOSTING_TARGET := app-dev HOSTING_TARGET := app-dev
endif endif
# Installs all project dependencies using npm. # --- Conditional Variables by Service for IAP commands ---
ifeq ($(SERVICE),admin)
IAP_SERVICE_NAME := $(CR_ADMIN_SERVICE_NAME)
IAP_SERVICE_REGION := $(CR_ADMIN_REGION)
IAP_PROJECT_ID := $(GCP_PROJECT_ID) # Admin console is env-specific
else
IAP_SERVICE_NAME := $(CR_LAUNCHPAD_SERVICE_NAME)
IAP_SERVICE_REGION := $(CR_LAUNCHPAD_REGION)
IAP_PROJECT_ID := $(GCP_DEV_PROJECT_ID) # Launchpad is dev-only
endif
# Shows this help message.
help:
@echo "--------------------------------------------------"
@echo " KROW Workforce - Available Makefile Commands"
@echo "--------------------------------------------------"
@echo ""
@echo " --- CORE DEVELOPMENT ---"
@echo " make install - Installs web frontend dependencies."
@echo " make dev - Starts the local web frontend server."
@echo " make build - Builds the web frontend for production."
@echo ""
@echo " --- DEPLOYMENT ---"
@echo " make deploy-launchpad-full - Deploys internal launchpad to Cloud Run (dev only) with IAP."
@echo " make deploy-admin-full [ENV=staging] - Deploys Admin Console to Cloud Run with IAP (default: dev)."
@echo " make deploy-app [ENV=staging] - Builds and deploys the main web app via Firebase Hosting (default: dev)."
@echo ""
@echo " --- CLOUD IAP MANAGEMENT ---"
@echo " make list-iap-users [SERVICE=admin] - Lists IAP users for a service (default: launchpad)."
@echo " make remove-iap-user USER=... [SERVICE=admin] - Removes an IAP user from a service."
@echo ""
@echo " --- PROJECT MANAGEMENT & TOOLS ---"
@echo " make setup-labels - Creates/updates GitHub labels from labels.yml."
@echo " make export-issues [ARGS="--state=all --label=bug"] - Exports GitHub issues to a markdown file. See scripts/export_issues.sh for options."
@echo " make create-issues-from-file - Bulk creates GitHub issues from a markdown file."
@echo " make install-git-hooks - Installs git pre-push hook to protect main/dev branches."
@echo ""
@echo " --- DATA CONNECT MANAGEMENT ---"
@echo " make dataconnect-enable-apis - Enables required GCP APIs for Data Connect."
@echo " make dataconnect-init - Initializes Firebase Data Connect (interactive wizard)."
@echo " make dataconnect-deploy - Deploys Data Connect schemas (GraphQL -> Cloud SQL)."
@echo ""
@echo " --- BASE44 EXPORT WORKFLOW ---"
@echo " make integrate-export - Integrates a new Base44 export from '../krow-workforce-export-latest'."
@echo " make prepare-export - Prepares a fresh Base44 export for local use."
@echo ""
@echo " make help - Shows this help message."
@echo "--------------------------------------------------"
# --- Core Development ---
install: install:
@echo "--> Installing web frontend dependencies..." @echo "--> Installing web frontend dependencies..."
@cd frontend-web && npm install @cd frontend-web && npm install
# Starts the local development server.
dev: dev:
@echo "--> Ensuring web frontend dependencies are installed..." @echo "--> Ensuring web frontend dependencies are installed..."
@cd frontend-web && npm install @cd frontend-web && npm install
@echo "--> Starting web frontend development server on http://localhost:5173 ..." @echo "--> Starting web frontend development server on http://localhost:5173 ..."
@cd frontend-web && npm run dev @cd frontend-web && npm run dev
# Builds the application for production.
build: build:
@echo "--> Building web frontend for production..." @echo "--> Building web frontend for production..."
@cd frontend-web && VITE_APP_ENV=$(ENV) npm run build @cd frontend-web && VITE_APP_ENV=$(ENV) npm run build
# Integrates a new Base44 export into the current project. # --- Deployment ---
# It replaces the src directory and the index.html file in the frontend-web directory. deploy-launchpad:
# Prerequisite: The new export must be in a folder named '../krow-workforce-export-latest'. @echo "--> Building and deploying Internal Launchpad to Cloud Run..."
@echo " - Step 1: Building container image..."
@cd firebase/internal-launchpad && gcloud builds submit \
--tag $(CR_LAUNCHPAD_IMAGE_URI) \
--project=$(GCP_DEV_PROJECT_ID)
@echo " - Step 2: Deploying to Cloud Run..."
@gcloud run deploy $(CR_LAUNCHPAD_SERVICE_NAME) \
--image $(CR_LAUNCHPAD_IMAGE_URI) \
--platform managed \
--region $(CR_LAUNCHPAD_REGION) \
--no-allow-unauthenticated \
--project=$(GCP_DEV_PROJECT_ID)
@echo " - Step 3: Enabling IAP on the service..."
@gcloud beta run services update $(CR_LAUNCHPAD_SERVICE_NAME) \
--region=$(CR_LAUNCHPAD_REGION) \
--project=$(GCP_DEV_PROJECT_ID) \
--iap
@echo "--> ✅ Deployment to Cloud Run successful."
deploy-launchpad-full: deploy-launchpad configure-iap-launchpad
@echo "✅ Launchpad deployed and IAP configured successfully!"
deploy-app: build
@echo "--> Deploying Frontend Web App to [$(ENV)] environment..."
@firebase deploy --only hosting:$(HOSTING_TARGET) --project=$(FIREBASE_ALIAS)
# --- Admin Console ---
admin-install:
@echo "--> Installing admin console dependencies..."
@cd admin-web && npm install
admin-dev:
@echo "--> Starting admin console development server on http://localhost:5174 ..."
@cd admin-web && npm run dev -- --port 5174
admin-build:
@echo "--> Building admin console for production..."
@node scripts/patch-admin-layout-for-env-label.js
@cd admin-web && VITE_APP_ENV=$(ENV) npm run build
deploy-admin: admin-build
@echo "--> Building and deploying Admin Console to Cloud Run [$(ENV)]..."
@echo " - Step 1: Building container image..."
@cd admin-web && gcloud builds submit \
--tag $(CR_ADMIN_IMAGE_URI) \
--project=$(GCP_PROJECT_ID)
@echo " - Step 2: Deploying to Cloud Run..."
@gcloud run deploy $(CR_ADMIN_SERVICE_NAME) \
--image $(CR_ADMIN_IMAGE_URI) \
--platform managed \
--region $(CR_ADMIN_REGION) \
--no-allow-unauthenticated \
--project=$(GCP_PROJECT_ID)
@echo " - Step 3: Enabling IAP on the service..."
@gcloud beta run services update $(CR_ADMIN_SERVICE_NAME) \
--region=$(CR_ADMIN_REGION) \
--project=$(GCP_PROJECT_ID) \
--iap
@echo "--> ✅ Admin Console deployment to Cloud Run successful."
deploy-admin-full: deploy-admin configure-iap-admin
@echo "✅ Admin Console deployed and IAP configured successfully!"
# --- Cloud IAP Configuration ---
configure-iap-launchpad:
@echo "--> Configuring IAP for Cloud Run service [$(CR_LAUNCHPAD_SERVICE_NAME)]..."
@echo " - Granting Cloud Run Invoker role to IAP Service Account..."
@gcloud run services add-iam-policy-binding $(CR_LAUNCHPAD_SERVICE_NAME) \
--region=$(CR_LAUNCHPAD_REGION) \
--project=$(GCP_DEV_PROJECT_ID) \
--member="serviceAccount:$(IAP_SERVICE_ACCOUNT)" \
--role='roles/run.invoker' \
--quiet
@echo " - Adding users from iap-users.txt..."
@cd firebase/internal-launchpad && \
grep -v '^#' iap-users.txt | grep -v '^$$' | while read -r member; do \
echo " Adding $$member as IAP-secured Web App User..."; \
gcloud beta iap web add-iam-policy-binding \
--project=$(GCP_DEV_PROJECT_ID) \
--resource-type=cloud-run \
--service=$(CR_LAUNCHPAD_SERVICE_NAME) \
--region=$(CR_LAUNCHPAD_REGION) \
--member="$$member" \
--role='roles/iap.httpsResourceAccessor' \
--quiet; \
done
@echo "✅ IAP configuration for Launchpad complete."
configure-iap-admin:
@echo "--> Configuring IAP for Cloud Run service [$(CR_ADMIN_SERVICE_NAME)] in [$(ENV)]..."
@echo " - Granting Cloud Run Invoker role to IAP Service Account..."
@gcloud run services add-iam-policy-binding $(CR_ADMIN_SERVICE_NAME) \
--region=$(CR_ADMIN_REGION) \
--project=$(GCP_PROJECT_ID) \
--member="serviceAccount:$(IAP_SERVICE_ACCOUNT)" \
--role='roles/run.invoker' \
--quiet
@echo " - Adding users from iap-users.txt..."
@cd admin-web && \
grep -v '^#' iap-users.txt | grep -v '^$$' | while read -r member; do \
echo " Adding $$member as IAP-secured Web App User..."; \
gcloud beta iap web add-iam-policy-binding \
--project=$(GCP_PROJECT_ID) \
--resource-type=cloud-run \
--service=$(CR_ADMIN_SERVICE_NAME) \
--region=$(CR_ADMIN_REGION) \
--member="$$member" \
--role='roles/iap.httpsResourceAccessor' \
--quiet; \
done
@echo "✅ IAP configuration for Admin Console complete."
list-iap-users:
@echo "--> Current IAP users for Cloud Run service [$(IAP_SERVICE_NAME)]:"
@gcloud beta iap web get-iam-policy \
--project=$(IAP_PROJECT_ID) \
--resource-type=cloud-run \
--service=$(IAP_SERVICE_NAME) \
--region=$(IAP_SERVICE_REGION)
remove-iap-user:
@if [ -z "$(USER)" ]; then \
echo "❌ Error: Please specify USER=user:email@example.com"; \
exit 1; \
fi
@echo "--> Removing IAP access for $(USER) from Cloud Run service [$(IAP_SERVICE_NAME)]..."
@gcloud beta iap web remove-iam-policy-binding \
--project=$(IAP_PROJECT_ID) \
--resource-type=cloud-run \
--service=$(IAP_SERVICE_NAME) \
--region=$(IAP_SERVICE_REGION) \
--member="$(USER)" \
--role='roles/iap.httpsResourceAccessor' \
--quiet
@echo "✅ User removed from IAP."
# --- Project Management ---
setup-labels:
@echo "--> Setting up GitHub labels..."
@./scripts/setup-github-labels.sh
export-issues:
@echo "--> Exporting GitHub issues to documentation..."
@./scripts/export_issues.sh $(ARGS)
create-issues-from-file:
@echo "--> Creating GitHub issues from file..."
@./scripts/create_issues.py
# --- Development Tools ---
install-git-hooks:
@echo "--> Installing Git hooks..."
@ln -sf ../../scripts/git-hooks/pre-push .git/hooks/pre-push
@echo "✅ pre-push hook installed successfully. Direct pushes to 'main' and 'dev' are now blocked."
# --- Base44 Export Workflow ---
integrate-export: integrate-export:
@echo "--> Integrating new Base44 export into frontend-web/..." @echo "--> Integrating new Base44 export into frontend-web/..."
@if [ ! -d "../krow-workforce-export-latest" ]; then \ @if [ ! -d "../krow-workforce-export-latest" ]; then \
@@ -69,8 +284,6 @@ integrate-export:
@node scripts/patch-index-html.js @node scripts/patch-index-html.js
@echo "--> Integration complete. Next step: 'make prepare-export'." @echo "--> Integration complete. Next step: 'make prepare-export'."
# Applies all necessary patches to a fresh Base44 export to run it locally.
# This is the main command for the hybrid workflow.
prepare-export: prepare-export:
@echo "--> Preparing fresh Base44 export for local development..." @echo "--> Preparing fresh Base44 export for local development..."
@node scripts/prepare-export.js @node scripts/prepare-export.js
@@ -100,50 +313,3 @@ dataconnect-deploy:
@echo "--> Deploying Firebase Data Connect schemas to [$(ENV)] (project: $(FIREBASE_ALIAS))..." @echo "--> Deploying Firebase Data Connect schemas to [$(ENV)] (project: $(FIREBASE_ALIAS))..."
@firebase deploy --only dataconnect --project=$(FIREBASE_ALIAS) @firebase deploy --only dataconnect --project=$(FIREBASE_ALIAS)
@echo "✅ Data Connect deployment completed for [$(ENV)]." @echo "✅ Data Connect deployment completed for [$(ENV)]."
# --- Firebase Deployment ---
deploy-launchpad:
@echo "--> Deploying Internal Launchpad to DEV project..."
@firebase deploy --only hosting:launchpad --project=dev
deploy-app: build
@echo "--> Deploying Frontend Web App to [$(ENV)] environment..."
@firebase deploy --only hosting:$(HOSTING_TARGET) --project=$(FIREBASE_ALIAS)
# Shows this help message.
help:
@echo "--------------------------------------------------"
@echo " KROW Workforce - Available Makefile Commands"
@echo "--------------------------------------------------"
@echo " make install - Installs web frontend dependencies."
@echo " make dev - Starts the local web frontend server."
@echo " make build - Builds the web frontend for production."
@echo " make integrate-export - Integrates a new Base44 export from '../krow-workforce-export-latest'."
@echo " make prepare-export - Prepares a fresh Base44 export for local use."
@echo ""
@echo " --- DEPLOYMENT ---"
@echo " make deploy-launchpad - Deploys the internal launchpad (always to dev)."
@echo " make deploy-app [ENV=staging] - Builds and deploys the main web app (default: dev)."
@echo ""
@echo " make help - Shows this help message."
@echo "--------------------------------------------------"
# --- Project Management ---
setup-labels:
@echo "--> Setting up GitHub labels..."
@./scripts/setup-github-labels.sh
export-issues:
@echo "--> Exporting GitHub issues to documentation..."
@./scripts/export_issues.sh
create-issues-from-file:
@echo "--> Creating GitHub issues from file..."
@./scripts/create_issues.py
# --- Development Tools ---
install-git-hooks:
@echo "--> Installing Git hooks..."
@ln -sf ../../scripts/git-hooks/pre-push .git/hooks/pre-push
@echo "✅ pre-push hook installed successfully. Direct pushes to 'main' and 'dev' are now blocked."

View File

@@ -1,43 +1,38 @@
# KROW Workforce Platform # Krow Workforce Management Platform
This monorepo contains the complete source code for the KROW Workforce platform, including the web frontend, mobile applications, and backend services. Krow is a comprehensive monorepo platform designed to streamline workforce management for events, hospitality, and large-scale enterprise operations. It connects clients, operators, vendors, and staff in a unified ecosystem, leveraging modern technology to optimize every step from procurement to payroll.
## 🚀 What's in this Monorepo? ## 🚀 What's in this Monorepo?
- **/firebase/**: Contains the Firebase Data Connect configuration (GraphQL schema, queries, mutations) and Firebase Hosting configuration. - **/firebase/**: This directory is the backbone of our backend infrastructure. It contains:
- **/frontend-web/**: The React/Vite web application used by administrators and managers. - **Firestore Rules (`firestore.rules`):** Defines the security and access control logic for our NoSQL database, ensuring that users can only read and write data they are authorized to access.
- **/mobile-apps/**: Contains the two Flutter-based mobile applications: - **Cloud Storage Rules (`storage.rules`):** Secures file uploads and downloads (like compliance documents and user avatars).
- `client-app`: For clients managing events. - **Data Connect (`dataconnect/`):** Houses the configuration for Firebase Data Connect, including the GraphQL schema, connectors, queries, and mutations that define our API layer.
- `staff-app`: For staff members managing their shifts and profile.
- **/docs/**: All project documentation (vision, roadmaps, architecture, guides).
- **/scripts/**: Automation scripts used by the `Makefile`.
- **/secrets/**: Contains sensitive credentials (ignored by Git).
## ▶️ Getting Started - **/admin-web/**: The central "mission control" for platform administrators. This React/Vite web application provides a secure interface for:
- **User Management:** Inviting new users (the very first client or operator) and managing roles/permissions across the entire ecosystem.
- **Platform Analytics:** Monitoring application usage, top pages, and user activity.
- **System Logs:** A log explorer for diagnosing issues and monitoring the platform's health.
This project uses a central `Makefile` to orchestrate all common tasks. - **/frontend-web/**: The primary web application for core business operations, used by procurement managers, operators, and clients. This React/Vite application includes modules for:
- **Vendor Management:** Onboarding, compliance tracking, and performance scorecards.
- **Event & Order Management:** Creating and managing staffing orders.
- **Invoicing & Payroll:** Financial workflows for clients and staff.
- **Dashboards:** Role-specific dashboards for different user types.
1. **Install Dependencies:** - **/mobile-apps/**: This directory is planned for our future mobile applications. It is structured to contain two distinct apps:
```bash - `client-app`: A dedicated application for clients to create orders, track events, and manage billing on the go.
make install - `staff-app`: An essential tool for workforce members to view schedules, clock in/out, manage their profiles, and track earnings.
```
*(This will install dependencies for the web frontend. Mobile dependency installation is handled within their respective directories.)*
2. **Run a Service:** - **/docs/**: The single source of truth for all project documentation. This includes:
- To run the web frontend: `make dev` - **Vision & Roadmaps:** High-level strategy, product goals, and technical direction.
- *(Additional commands for mobile and backend will be added as development progresses.)* - **Architecture:** Detailed diagrams and explanations of the system architecture.
- **API Specifications:** Documentation for our GraphQL API.
- **Development Guides:** Conventions, setup instructions, and maintenance procedures.
3. **See All Commands:** - **/scripts/**: A collection of automation scripts to streamline development and operational tasks. These scripts are primarily executed via the `Makefile` and handle tasks like database patching, environment setup, and code generation.
For a full list of available commands, run:
```bash
make help
```
## 🤝 Contributing - **/secrets/**: A Git-ignored directory for storing sensitive credentials, API keys, and environment-specific configuration files. This ensures that no confidential information is ever committed to the repository.
New to the KROW team? Start here to set up your environment and understand our development practices: **[CONTRIBUTING.md](./CONTRIBUTING.md)**
---
## 📚 Documentation Overview ## 📚 Documentation Overview
@@ -52,3 +47,35 @@ This section provides a quick guide to the most important documentation files in
- **[06-maintenance-guide.md](./docs/06-maintenance-guide.md)**: The operational manual for integrating updates from the Base44 visual builder. - **[06-maintenance-guide.md](./docs/06-maintenance-guide.md)**: The operational manual for integrating updates from the Base44 visual builder.
- **[07-reference-base44-api-export.md](./docs/07-reference-base44-api-export.md)**: The raw API documentation exported from Base44, used as a reference. - **[07-reference-base44-api-export.md](./docs/07-reference-base44-api-export.md)**: The raw API documentation exported from Base44, used as a reference.
- **[08-reference-base44-prompts.md](./docs/08-reference-base44-prompts.md)**: A collection of standardized prompts for interacting with the Base44 AI. - **[08-reference-base44-prompts.md](./docs/08-reference-base44-prompts.md)**: A collection of standardized prompts for interacting with the Base44 AI.
## 🤝 Contributing
New to the KROW team? Start here to set up your environment and understand our development practices: **[CONTRIBUTING.md](./CONTRIBUTING.md)**
## 🛠️ Tech Stack
- **Frontend:** React with Vite (JavaScript)
- **Styling:** Tailwind CSS
- **Backend:** Firebase (Firestore, Cloud Storage, Authentication, Data Connect)
- **Mobile:** Flutter (planned)
## 📦 Getting Started
1. **Clone the repository:**
```bash
git clone https://github.com/Oloodi/krow.git
cd krow
```
2. **Install dependencies for the main web app:**
```bash
cd frontend-web
npm install
```
3. **Run the development server:**
```bash
npm run dev
```
The main application will be available at `http://localhost:5173`. For other packages, refer to their respective `README.md` files.

17
admin-web/.gcloudignore Normal file
View File

@@ -0,0 +1,17 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Node.js dependencies:
node_modules/

24
admin-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

29
admin-web/Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
# STAGE 1: Build the React application
FROM node:20-alpine AS build
WORKDIR /app
# Copy package files and install dependencies
COPY package.json package-lock.json ./
RUN npm install
# Copy the rest of the application source code
COPY . .
# Build the application for production
RUN npm run build
# STAGE 2: Serve the static files with Nginx
FROM nginx:alpine
# Copy the built files from the build stage
COPY --from=build /app/dist /usr/share/nginx/html
# Copy the custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose the port Nginx is listening on
EXPOSE 8080
# Command to run Nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]

35
admin-web/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Krow Admin Console
This is the central administration web application for the Krow platform, built with React and Vite.
## Overview
The Krow Admin Console provides a secure and centralized interface for platform administrators to perform high-level management tasks. Its primary functions are:
- **User Ecosystem Management:** Invite, view, and manage access for all users across the platform (Clients, Operators, Vendors, etc.).
- **Platform Health Monitoring:** Get a high-level overview of system activity, usage, and performance.
- **Diagnostics and Troubleshooting:** Access system logs for maintenance and issue resolution.
## Features
- **Dashboard:** A central hub displaying key platform metrics like total users, active vendors, and pending invitations.
- **User Management:** A complete interface to invite new users and manage existing ones, including role assignments.
- **Analytics:** A view of application usage statistics, such as top users and most visited pages.
- **Logs Explorer:** A simple tool to monitor and filter system logs for diagnostics.
## Getting Started
1. Navigate to the `admin-web` directory:
```bash
cd admin-web
```
2. Install the dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npm run dev
```
The application will be available at `http://localhost:5173/admin-web/`.

21
admin-web/components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

18
admin-web/favicon.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="200 180 280 140" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#24303B;}
.st1{fill:#002FE3;}
</style>
<g>
<path class="st1" d="M459.81,202.55c-5.03,0.59-9.08,4.49-10.36,9.38l-15.99,59.71l-16.24-56.3
c-1.68-5.92-6.22-10.86-12.19-12.34c-1.58-0.39-3.11-0.54-4.64-0.49h-0.15c-1.53-0.05-3.11,0.1-4.64,0.49
c-5.97,1.48-10.51,6.42-12.24,12.34l-3.6,12.53l-11.35,39.38l-7.9-27.54c-10.76-37.5-48.56-62.23-88.38-55.32
c-33.26,5.82-57.05,35.68-56.99,69.48v0.79c0,4.34,0.39,8.73,1.13,13.18c0.18,1.02,0.37,2.03,0.6,3.03
c1.84,8.31,10.93,12.73,18.49,8.8v0c5.36-2.79,7.84-8.89,6.42-14.77c-0.85-3.54-1.28-7.23-1.23-11.03
c0-25.02,20.48-45.5,45.55-45.2c7.6,0.1,15.59,2.07,23.59,6.37c13.52,7.3,23.15,20.18,27.34,34.94l13.32,46.34
c1.73,5.97,6.22,11,12.24,12.58c9.62,2.62,19-3.06,21.51-12.04l16.09-56.7l0.2-0.1l16.09,56.85c1.63,5.68,5.87,10.41,11.55,11.99
c9.13,2.57,18.11-2.66,20.67-11.2l24.13-79.6C475.35,209.85,468.64,201.56,459.81,202.55z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

8
admin-web/iap-users.txt Normal file
View File

@@ -0,0 +1,8 @@
# List of authorized users for the Admin Console
# Format: one email per line, lines starting with # are comments
#
# IMPORTANT: These users must belong to the 'krowwithus.com' organization.
# This is a known limitation of enabling IAP directly on Cloud Run.
# See: https://docs.cloud.google.com/run/docs/securing/identity-aware-proxy-cloud-run#known_limitations
user:admin@krowwithus.com

13
admin-web/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KROW Admin Console</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

10
admin-web/jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "react-jsx"
},
"include": ["src/**/*.js", "src/**/*.jsx"]
}

18
admin-web/logo.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="200 180 280 140" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#24303B;}
.st1{fill:#002FE3;}
</style>
<g>
<path class="st1" d="M459.81,202.55c-5.03,0.59-9.08,4.49-10.36,9.38l-15.99,59.71l-16.24-56.3
c-1.68-5.92-6.22-10.86-12.19-12.34c-1.58-0.39-3.11-0.54-4.64-0.49h-0.15c-1.53-0.05-3.11,0.1-4.64,0.49
c-5.97,1.48-10.51,6.42-12.24,12.34l-3.6,12.53l-11.35,39.38l-7.9-27.54c-10.76-37.5-48.56-62.23-88.38-55.32
c-33.26,5.82-57.05,35.68-56.99,69.48v0.79c0,4.34,0.39,8.73,1.13,13.18c0.18,1.02,0.37,2.03,0.6,3.03
c1.84,8.31,10.93,12.73,18.49,8.8v0c5.36-2.79,7.84-8.89,6.42-14.77c-0.85-3.54-1.28-7.23-1.23-11.03
c0-25.02,20.48-45.5,45.55-45.2c7.6,0.1,15.59,2.07,23.59,6.37c13.52,7.3,23.15,20.18,27.34,34.94l13.32,46.34
c1.73,5.97,6.22,11,12.24,12.58c9.62,2.62,19-3.06,21.51-12.04l16.09-56.7l0.2-0.1l16.09,56.85c1.63,5.68,5.87,10.41,11.55,11.99
c9.13,2.57,18.11-2.66,20.67-11.2l24.13-79.6C475.35,209.85,468.64,201.56,459.81,202.55z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

13
admin-web/nginx.conf Normal file
View File

@@ -0,0 +1,13 @@
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ /index.html;
}
}

4967
admin-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
admin-web/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "admin-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"vite": "^7.2.2"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
admin-web/src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

24
admin-web/src/App.jsx Normal file
View File

@@ -0,0 +1,24 @@
import React from "react";
import Layout from "./pages/Layout";
import Dashboard from "./pages/Dashboard";
import UserManagement from "./pages/UserManagement";
import Analytics from "./pages/Analytics";
import Logs from "./pages/Logs";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
function App() {
return (
<Router>
<Layout>
<Routes>
<Route path="/admin-web/" element={<Dashboard />} />
<Route path="/admin-web/user-management" element={<UserManagement />} />
<Route path="/admin-web/analytics" element={<Analytics />} />
<Route path="/admin-web/logs" element={<Logs />} />
</Routes>
</Layout>
</Router>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

58
admin-web/src/index.css Normal file
View File

@@ -0,0 +1,58 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,6 @@
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs))
}

10
admin-web/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,61 @@
import React from "react";
const topUsers = [
{ email: "ian.gomez@example.com", visits: 6950 },
{ email: "admin@krow.com", visits: 193 },
{ email: "paula.orte@example.com", visits: 81 },
{ email: "test.user@example.com", visits: 50 },
];
const topPages = [
{ name: "VendorManagement", visits: 718 },
{ name: "Dashboard", visits: 600 },
{ name: "unknown", visits: 587 },
{ name: "ClientDashboard", visits: 475 },
{ name: "Events", visits: 456 },
];
export default function Analytics() {
return (
<div>
<h1 className="text-3xl font-bold text-slate-800">Analytics</h1>
<p className="mt-2 text-base text-slate-600">
An overview of your application's data.
</p>
<div className="mt-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white shadow-lg rounded-2xl p-6">
<h3 className="text-lg font-medium text-gray-900">Total Unique Users</h3>
<p className="mt-2 text-4xl font-bold text-blue-600">3</p>
</div>
</div>
</div>
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h3 className="text-xl font-semibold text-gray-900">Top Users</h3>
<ul className="mt-4 space-y-4">
{topUsers.map((user) => (
<li key={user.email} className="bg-white shadow-lg rounded-2xl p-4">
<p className="font-semibold text-slate-800">{user.email}</p>
<p className="text-slate-500">{user.visits.toLocaleString()} visits</p>
</li>
))}
</ul>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-900">Top Pages</h3>
<ul className="mt-4 space-y-4">
{topPages.map((page) => (
<li key={page.name} className="bg-white shadow-lg rounded-2xl p-4 flex justify-between items-center">
<p className="font-semibold text-slate-800">{page.name}</p>
<p className="text-slate-500 font-medium">{page.visits.toLocaleString()}</p>
</li>
))}
</ul>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import React from "react";
import { Users, Building2, Mail, FileText } from "lucide-react";
const stats = [
{ name: "Total Users", stat: "15", icon: Users, color: "bg-blue-500" },
{ name: "Active Vendors", stat: "8", icon: Building2, color: "bg-green-500" },
{ name: "Pending Invitations", stat: "3", icon: Mail, color: "bg-yellow-500" },
{ name: "Open Orders", stat: "12", icon: FileText, color: "bg-purple-500" },
];
export default function Dashboard() {
return (
<div>
<h1 className="text-3xl font-bold text-slate-800">Admin Dashboard</h1>
<p className="mt-2 text-base text-slate-600">
Welcome to the Krow Admin Console. Here is a quick overview of the platform.
</p>
<div className="mt-8">
<dl className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((item) => (
<div key={item.name} className="relative overflow-hidden rounded-2xl bg-white px-4 py-5 shadow-lg sm:px-6 sm:py-6">
<dt>
<div className={`absolute rounded-xl p-3 ${item.color}`}>
<item.icon className="h-8 w-8 text-white" aria-hidden="true" />
</div>
<p className="ml-20 truncate text-sm font-medium text-gray-500">{item.name}</p>
</dt>
<dd className="ml-20 flex items-baseline">
<p className="text-3xl font-semibold text-gray-900">{item.stat}</p>
</dd>
</div>
))}
</dl>
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { Building2, Users, BarChart3, Terminal, LayoutDashboard, LogOut } from "lucide-react";
const navigation = [
{ name: "Dashboard", href: "/admin-web/", icon: LayoutDashboard },
{ name: "User Management", href: "/admin-web/user-management", icon: Users },
{ name: "Analytics", href: "/admin-web/analytics", icon: BarChart3 },
{ name: "Logs Explorer", href: "/admin-web/logs", icon: Terminal },
];
export default function Layout({ children }) {
const location = useLocation();
return (
<div className="flex h-screen bg-slate-50">
{/* Sidebar */}
<div className="hidden md:flex md:flex-shrink-0">
<div className="flex flex-col w-64">
{/* Sidebar header */}
<div className="flex items-center h-16 flex-shrink-0 px-4 bg-white border-b border-slate-200">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg flex items-center justify-center">
<img src="/logo.svg" alt="Krow Logo" className="h-full w-full" />
</div>
<div>
<h1 className="text-lg font-bold text-slate-800">KROW</h1>
<p className="text-xs text-slate-500">Admin Console</p>
</div>
</div>
{import.meta.env.VITE_APP_ENV && (
<span className={`ml-auto inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
import.meta.env.VITE_APP_ENV === 'staging' ? 'bg-amber-100 text-amber-800' : 'bg-blue-100 text-blue-800'
}`}>
{import.meta.env.VITE_APP_ENV === 'staging' ? 'Staging' : 'Dev'}
</span>
)}
</div>
{/* Sidebar content */}
<div className="flex-1 flex flex-col overflow-y-auto bg-white">
<nav className="flex-1 px-4 py-4">
{navigation.map((item) => (
<Link
key={item.name}
to={item.href}
className={`
${
location.pathname === item.href
? "bg-blue-50 text-blue-600"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900"
}
group flex items-center px-3 py-3 text-sm font-medium rounded-lg
`}
>
<item.icon
className="mr-3 h-6 w-6"
aria-hidden="true"
/>
{item.name}
</Link>
))}
</nav>
<div className="px-4 py-4 border-t border-slate-200">
<Link
to="#"
className="group flex items-center px-3 py-3 text-sm font-medium rounded-lg text-slate-600 hover:bg-slate-100 hover:text-slate-900"
>
<LogOut className="mr-3 h-6 w-6" />
Logout
</Link>
</div>
</div>
</div>
</div>
{/* Main content */}
<div className="flex flex-col w-0 flex-1 overflow-hidden">
<main className="flex-1 relative z-0 overflow-y-auto focus:outline-none">
<div className="py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
{children}
</div>
</div>
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
import React from "react";
const logs = [
{ level: "INFO", message: "User admin@krow.com logged in.", timestamp: "2024-05-21T10:00:00Z" },
{ level: "WARN", message: "API response time is slow: 2500ms for /api/vendors", timestamp: "2024-05-21T10:01:00Z" },
{ level: "ERROR", message: "Failed to process payment for invoice INV-12345.", timestamp: "2024-05-21T10:02:00Z" },
{ level: "INFO", message: "New vendor 'New Staffing Co' invited by admin@krow.com.", timestamp: "2024-05-21T10:05:00Z" },
];
export default function Logs() {
return (
<div>
<h1 className="text-3xl font-bold text-slate-800">Logs Explorer</h1>
<p className="mt-2 text-base text-slate-600">
Monitor system logs across your application.
</p>
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-2xl">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-slate-50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Level</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Message</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Timestamp</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{logs.map((log, index) => (
<tr key={index}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
log.level === 'ERROR' ? 'bg-red-100 text-red-800' :
log.level === 'WARN' ? 'bg-yellow-100 text-yellow-800' :
'bg-green-100 text-green-800'
}`}>
{log.level}
</span>
</td>
<td className="px-3 py-4 text-sm text-gray-500 font-mono">{log.message}</td>
<td className="px-3 py-4 text-sm text-gray-500">{new Date(log.timestamp).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,146 @@
import React, { useState } from "react";
import { Plus, Send } from "lucide-react";
const users = [
{ name: "Alex Johnson", email: "alex.j@krow.com", role: "Admin", status: "Active", avatar: "https://randomuser.me/api/portraits/men/1.jpg" },
{ name: "Samantha Lee", email: "samantha.lee@vendor.com", role: "Vendor", status: "Active", avatar: "https://randomuser.me/api/portraits/women/2.jpg" },
{ name: "Michael Chen", email: "michael.chen@client.com", role: "Client", status: "Pending", avatar: "https://randomuser.me/api/portraits/men/3.jpg" },
];
function InviteUserModal({ isOpen, onClose }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity">
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div className="relative transform overflow-hidden rounded-2xl bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<h3 className="text-xl font-semibold leading-6 text-slate-900">Send Invitation</h3>
<p className="mt-2 text-sm text-slate-500">The user will receive an email with a link to set up their account.</p>
<div className="mt-6 space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email address
</label>
<input
type="email"
name="email"
id="email"
className="mt-2 block w-full rounded-lg border-0 py-2.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
placeholder="colleague@company.com"
/>
</div>
<div>
<label htmlFor="role" className="block text-sm font-medium text-gray-700">
Access level
</label>
<select
id="role"
name="role"
className="mt-2 block w-full rounded-lg border-0 py-2.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
defaultValue="User"
>
<option>Admin</option>
<option>Vendor</option>
<option>Client</option>
<option>User</option>
</select>
</div>
</div>
</div>
<div className="bg-slate-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button
type="button"
className="inline-flex w-full items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 sm:ml-3 sm:w-auto"
onClick={onClose}
>
<Send className="h-5 w-5 mr-2" />
Send Invitation
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={onClose}
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
);
}
export default function UserManagement() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-3xl font-bold text-slate-800">User Management</h1>
<p className="mt-2 text-base text-slate-600">
A list of all the users in your account including their name, email and role.
</p>
</div>
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
className="inline-flex items-center justify-center rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:w-auto"
onClick={() => setIsModalOpen(true)}
>
<Plus className="h-5 w-5 mr-2" />
Invite User
</button>
</div>
</div>
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-2xl">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-slate-50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Name</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Email</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Role</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{users.map((user) => (
<tr key={user.email}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div className="flex items-center">
<div className="h-11 w-11 flex-shrink-0">
<img className="h-11 w-11 rounded-full" src={user.avatar} alt="" />
</div>
<div className="ml-4">
<div className="font-medium text-gray-900">{user.name}</div>
</div>
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.email}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.role}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
user.status === 'Active' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{user.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
<InviteUserModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div>
);
}

View File

@@ -0,0 +1,89 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
theme: {
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [require("tailwindcss-animate")],
}

13
admin-web/vite.config.js Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})

200
docs/issues/template.md Normal file
View File

@@ -0,0 +1,200 @@
# [Auth] Implement Firebase Authentication via krowSDK Facade
Labels: feature, infra, platform:web, platform:backend, priority:high, sred-eligible
Milestone: Foundation & Dev Environment Setup
### 🎯 Objective
Replace the Base44 authentication client with a new internal SDK module, `krowSDK.auth`, backed by Firebase Authentication. This foundational task will unblock all future backend development and align the web application with the new GCP-based architecture.
### 🔬 SR&ED Justification
- **Technological Uncertainty:** What is the optimal way to create a seamless abstraction layer (`krowSDK`) that perfectly mimics the existing `base44` SDK's interface to minimize frontend refactoring, while integrating a completely different authentication provider (Firebase Auth)? A key uncertainty is how this facade will handle the significant differences in user session management and data retrieval (e.g., custom user fields like `user_role` which are not native to the Firebase Auth user object) between the two systems.
- **Systematic Investigation:** We will conduct experimental development to build a `krowSDK.auth` module. This involves systematically mapping each `base44.auth` method (`me`, `logout`, `isAuthenticated`) to its Firebase Auth equivalent. We will investigate and prototype a solution for fetching supplementary user data (like `user_role`) from our Firestore database and merging it with the core Firebase Auth user object. This will establish a clean, reusable, and scalable architectural pattern for all future SDK modules.
### Details
This task is the most critical prerequisite for migrating our backend. As defined in the `03-backend-api-specification.md`, every request to our new Data Connect and Cloud Functions API will require a `Bearer <Firebase-Auth-Token>`. Without this, no backend work can proceed.
#### The Strategy: A Facade SDK (`krowSDK`)
Instead of replacing `base44` calls with Firebase calls directly throughout the codebase, we will create an abstraction layer, or "Facade". This approach has three major benefits:
1. **Simplified Migration:** The new `krowSDK.auth` will expose the *exact same methods* as the old `base44.auth`. This means we can swap out the authentication logic with minimal changes to the UI components, drastically reducing the scope of refactoring.
2. **High Maintainability:** All authentication logic will be centralized in one place. If we ever need to change providers again, we only modify the SDK, not the entire application.
3. **Clear Separation of Concerns:** The UI components remain agnostic about the authentication provider. They just need to call `krowSDK.auth.me()`, not worry about the underlying implementation details.
#### Implementation Plan
The developer should create two new files:
**1. Firebase Configuration (`frontend-web/src/firebase/config.js`)**
This file will initialize the Firebase app and export the necessary services. It's crucial to use environment variables for the configuration keys to keep them secure and environment-specific.
```javascript
// frontend-web/src/firebase/config.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
// Your web app's Firebase configuration
// IMPORTANT: Use environment variables for these values
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Export Firebase services
export const auth = getAuth(app);
export const db = getFirestore(app);
export default app;
```
**2. Krow SDK (`frontend-web/src/lib/krowSDK.js`)**
This is the core of the task. This file will implement the facade, importing the Firebase `auth` service and recreating the `base44.auth` interface.
```javascript
// frontend-web/src/lib/krowSDK.js
import { auth, db } from '../firebase/config';
import {
onAuthStateChanged,
signOut,
updateProfile,
// Import other necessary auth functions like signInWithEmailAndPassword, createUserWithEmailAndPassword, etc.
} from "firebase/auth";
import { doc, getDoc } from "firebase/firestore";
/**
* A promise-based wrapper for onAuthStateChanged to check the current auth state.
* @returns {Promise<boolean>} - A promise that resolves to true if authenticated, false otherwise.
*/
const isAuthenticated = () => {
return new Promise((resolve) => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
unsubscribe();
resolve(!!user);
});
});
};
/**
* Fetches the current authenticated user's profile.
* This function mimics `base44.auth.me()` by combining the Firebase Auth user
* with custom data from our Firestore database (e.g., user_role).
* @returns {Promise<object|null>} - A promise that resolves with the user object or null.
*/
const me = () => {
return new Promise((resolve, reject) => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
unsubscribe();
if (user) {
try {
// 1. Get the core user from Firebase Auth
const { uid, email, displayName } = user;
const baseProfile = {
id: uid,
email: email,
full_name: displayName,
};
// 2. Get custom fields from Firestore
// We assume a 'users' collection where the document ID is the user's UID.
const userDocRef = doc(db, "users", uid);
const userDoc = await getDoc(userDocRef);
if (userDoc.exists()) {
const customData = userDoc.data();
// 3. Merge the two data sources
resolve({
...baseProfile,
user_role: customData.user_role, // Example custom field
// ... other custom fields from Firestore
});
} else {
// User exists in Auth but not in Firestore. This can happen during sign-up.
// Resolve with base profile; the app should handle creating the Firestore doc.
resolve(baseProfile);
}
} catch (error) {
console.error("Error fetching user profile from Firestore:", error);
reject(error);
}
} else {
// No user is signed in.
resolve(null);
}
});
});
};
/**
* Updates the current user's profile.
* This mimics `base44.auth.updateMe()`.
* @param {object} profileData - Data to update, e.g., { full_name: "New Name" }.
*/
const updateMe = async (profileData) => {
if (auth.currentUser) {
// Firebase Auth's updateProfile only supports displayName and photoURL.
if (profileData.full_name) {
await updateProfile(auth.currentUser, {
displayName: profileData.full_name,
});
}
// For other custom fields, you would need to update the user's document in Firestore.
// const userDocRef = doc(db, "users", auth.currentUser.uid);
// await updateDoc(userDocRef, { custom_field: profileData.custom_field });
} else {
throw new Error("No authenticated user to update.");
}
};
/**
* Logs the user out and redirects them.
* @param {string} [redirectUrl='/'] - The URL to redirect to after logout.
*/
const logout = (redirectUrl = '/') => {
signOut(auth).then(() => {
// Redirect after sign-out.
window.location.href = redirectUrl;
}).catch((error) => {
console.error("Logout failed:", error);
});
};
// The krowSDK object that mimics the Base44 SDK structure
export const krowSDK = {
auth: {
isAuthenticated,
me,
updateMe,
logout,
// Note: redirectToLogin is not implemented as it's a concern of the routing library (e.g., React Router),
// which should protect routes and redirect based on the authentication state.
},
// Future modules will be added here, e.g., krowSDK.entities.Event
};
```
### ✅ Acceptance Criteria
- [ ] A `frontend-web/src/firebase/config.js` file is created, correctly initializing the Firebase app using environment variables.
- [ ] A `frontend-web/src/lib/krowSDK.js` file is created and implements the `krowSDK.auth` facade.
- [ ] The `krowSDK.auth` module exports `isAuthenticated`, `me`, `updateMe`, and `logout` functions with interfaces identical to their `base44.auth` counterparts.
- [ ] Key parts of the application (e.g., login pages, user profile components, auth checks) are refactored to import and use `krowSDK.auth` instead of `base44.auth`.
- [ ] A new user can sign up using a Firebase-powered form.
- [ ] An existing user can log in using a Firebase-powered form.
- [ ] The application UI correctly updates to reflect the user's authenticated state (e.g., showing user name, hiding login button).
- [ ] After logging in, the user's Firebase Auth ID Token can be retrieved and is ready to be sent in an `Authorization: Bearer` header for API calls.

View File

@@ -0,0 +1,18 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Node.js dependencies:
node_modules/
*.log

View File

@@ -0,0 +1,28 @@
# Utiliser nginx pour servir les fichiers statiques
FROM nginx:alpine
# Copier les fichiers statiques
COPY index.html /usr/share/nginx/html/
COPY assets /usr/share/nginx/html/assets/
COPY favicon.svg /usr/share/nginx/html/
COPY logo.svg /usr/share/nginx/html/
# Configuration nginx pour le routing SPA
RUN echo 'server { \
listen 8080; \
server_name _; \
root /usr/share/nginx/html; \
index index.html; \
location / { \
try_files $uri $uri/ /index.html; \
} \
# Headers de sécurité \
add_header X-Frame-Options "SAMEORIGIN" always; \
add_header X-Content-Type-Options "nosniff" always; \
add_header X-XSS-Protection "1; mode=block" always; \
}' > /etc/nginx/conf.d/default.conf
# Nginx écoute sur le port 8080 (requis par Cloud Run)
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,9 @@
# List of authorized users for the Internal Launchpad
# Format: one email per line, lines starting with # are comments
#
# IMPORTANT: These users must belong to the 'krowwithus.com' organization.
# This is a known limitation of enabling IAP directly on Cloud Run.
# See: https://docs.cloud.google.com/run/docs/securing/identity-aware-proxy-cloud-run#known_limitations
user:admin@krowwithus.com
# user:boris@oloodi.com # External users are not supported with this IAP method

View File

@@ -273,7 +273,7 @@
</div> </div>
<div> <div>
<h1 class="text-xl font-bold text-gray-900">KROW</h1> <h1 class="text-xl font-bold text-gray-900">KROW</h1>
<p class="text-xs text-gray-500">Workforce Platform</p> <p class="text-xs text-gray-500">Launchpad</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -20,6 +20,9 @@
- name: "refactor" - name: "refactor"
description: "Code changes that neither fix a bug nor add a feature" description: "Code changes that neither fix a bug nor add a feature"
color: "f29513" color: "f29513"
- name: "security"
description: "Tasks related to security enhancements, audits, or fixes"
color: "000000" # Black for security, to make it stand out
# By Platform # By Platform
- name: "platform:web" - name: "platform:web"
@@ -31,6 +34,9 @@
- name: "platform:backend" - name: "platform:backend"
description: "Tasks for Data Connect or Cloud Functions" description: "Tasks for Data Connect or Cloud Functions"
color: "5319e7" color: "5319e7"
- name: "platform:admin"
description: "Tasks specific to the Admin Console web app"
color: "5319e7"
# For Project Management # For Project Management
- name: "sred-eligible" - name: "sred-eligible"

132
memo-iap.txt Normal file
View File

@@ -0,0 +1,132 @@
# ===================================================================
# MEMO : Sécurisation d'un service Cloud Run avec IAP (Identity-Aware Proxy)
# ===================================================================
#
# Objectif : Ce document résume les étapes et les permissions nécessaires
# pour sécuriser un service Cloud Run avec IAP, en se basant sur
# l'expérience acquise avec le service 'internal-launchpad'.
#
# Date : 2025-11-16
# ---
# 1. PRINCIPE FONDAMENTAL ET CONTRAINTE MAJEURE
# ---
#
# L'enseignement le plus important est le suivant :
#
# > L'activation d'IAP directement sur un service Cloud Run (sans Load Balancer)
# > restreint l'accès aux utilisateurs qui font partie de la MÊME organisation
# > Google Workspace que le projet Google Cloud.
#
# Dans notre cas, le projet 'krow-workforce-dev' appartient à l'organisation
# 'krowwithus.com'. C'est pourquoi :
# - 'admin@krowwithus.com' A PU ACCÉDER au service.
# - 'boris@oloodi.com' N'A PAS PU ACCÉDER au service, même avec les bonnes permissions.
#
# Solution adoptée : Créer des comptes utilisateurs via Google Workspace ou
# Cloud Identity (qui est gratuit) dans le domaine 'krowwithus.com' pour
# tous les développeurs et administrateurs qui ont besoin d'accéder aux
# services internes.
# ---
# 2. PRÉREQUIS INDISPENSABLES
# ---
#
# Avant de commencer, les éléments suivants doivent être configurés pour le projet :
#
# a) APIs Google Cloud activées :
# - Cloud Run API (`run.googleapis.com`)
# - Identity-Aware Proxy API (`iap.googleapis.com`)
# - Cloud Build API (`cloudbuild.googleapis.com`)
# - Artifact Registry API (`artifactregistry.googleapis.com`)
#
# Commande pour les activer :
# gcloud services enable run.googleapis.com iap.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com --project=krow-workforce-dev
#
# b) Écran de consentement OAuth configuré :
# - IAP utilise OAuth2 pour identifier les utilisateurs. L'écran de consentement est obligatoire.
# - URL : https://console.cloud.google.com/apis/credentials/consent?project=krow-workforce-dev
# - Type d'utilisateur : "Interne" est préférable si disponible.
# - Nom de l'application : Utiliser un nom général (ex: "Krow Workforce Platform").
#
# c) Compte de service IAP existant :
# - Parfois, ce compte n'est pas créé automatiquement. Il est plus sûr de forcer sa création.
#
# Commande pour le créer :
# gcloud beta services identity create --service=iap.googleapis.com --project=krow-workforce-dev
# ---
# 3. LOGIQUE DE CONFIGURATION EN 3 ÉTAPES (AUTOMATISÉE DANS LE MAKEFILE)
# ---
#
# Le processus complet pour sécuriser un service se déroule en 3 étapes séquentielles.
#
# ÉTAPE A : DÉPLOIEMENT SÉCURISÉ DU SERVICE
# -----------------------------------------
# Le service doit être déployé en mode privé, puis IAP doit être activé dessus.
#
# 1. Déployer le service avec l'accès public désactivé :
# gcloud run deploy <SERVICE_NAME> --image <IMAGE_URI> --no-allow-unauthenticated ...
#
# 2. Activer IAP sur le service :
# gcloud beta run services update <SERVICE_NAME> --iap ...
#
#
# ÉTAPE B : AUTORISER IAP À APPELER LE SERVICE
# -------------------------------------------
# Une fois IAP activé, le proxy IAP a besoin de la permission d'invoquer (d'appeler)
# votre service Cloud Run. Cette permission est accordée au compte de service IAP.
#
# Commande :
# gcloud run services add-iam-policy-binding <SERVICE_NAME> \
# --member="serviceAccount:service-<PROJECT_NUMBER>@gcp-sa-iap.iam.gserviceaccount.com" \
# --role="roles/run.invoker"
#
#
# ÉTAPE C : AUTORISER LES UTILISATEURS À PASSER LE PROXY IAP
# -------------------------------------------------------
# C'est ici qu'on ajoute les utilisateurs finaux (qui doivent être dans l'organisation).
# Cette commande est spécifique à IAP pour Cloud Run.
#
# Commande :
# gcloud beta iap web add-iam-policy-binding \
# --resource-type=cloud-run \
# --service=<SERVICE_NAME> \
# --member="user:email@krowwithus.com" \
# --role="roles/iap.httpsResourceAccessor"
# ---
# 4. AUTOMATISATION VIA LE MAKEFILE
# ---
#
# Tout ce processus est géré par la commande `make deploy-launchpad-full`.
#
# a) `deploy-launchpad` s'occupe de l'ÉTAPE A.
#
# b) `configure-iap-launchpad` s'occupe des ÉTAPES B et C.
#
# - Il lit les utilisateurs depuis `firebase/internal-launchpad/iap-users.txt`.
# - Il exécute la commande de l'ÉTAPE B pour le compte de service IAP.
# - Il exécute la commande de l'ÉTAPE C pour chaque utilisateur du fichier.
#
# Pour adapter cela à 'admin-web', il faudra :
# 1. Créer des variables similaires à `CR_LAUNCHPAD_...` pour `admin-web`.
# 2. Créer de nouvelles cibles `deploy-admin-web-full`, `configure-iap-admin-web`, etc.,
# en copiant la logique de celles du launchpad et en adaptant les noms de service.
# ---
# 5. VÉRIFICATION ET DÉPANNAGE
# ---
#
# a) Lister les utilisateurs autorisés :
# La commande `make list-iap-users` est le meilleur moyen de vérifier qui a accès.
#
# b) Erreur "You don't have access" :
# - Cause 1 : L'utilisateur n'est pas dans la bonne organisation (le plus probable).
# - Cause 2 : L'utilisateur n'est pas dans le fichier `iap-users.txt` et `make configure-iap-launchpad` n'a pas été lancé.
# - Cause 3 : Problème de cache de navigateur. Toujours tester dans une nouvelle fenêtre de navigation privée.
# - Cause 4 : Délai de propagation des permissions IAM (attendre 2-3 minutes).
#
# c) Erreur "Forbidden" :
# - Cela signifie que l'authentification a réussi mais que la permission d'invoquer le service est manquante.
# - La cause la plus probable est que l'ÉTAPE B (donner la permission au compte de service IAP) a échoué ou a été oubliée.

View File

@@ -1,17 +1,49 @@
#!/bin/bash #!/bin/bash
# ==================================================================================== # ====================================================================================
# SCRIPT TO EXPORT SR&ED-ELIGIBLE GITHUB ISSUES TO A MARKDOWN FILE # SCRIPT TO EXPORT GITHUB ISSUES TO A MARKDOWN FILE
# ==================================================================================== # ====================================================================================
set -e # Exit script if a command fails set -e # Exit script if a command fails
# --- Configuration --- # --- Default Configuration ---
OUTPUT_FILE="sred-issues-export.md"
# This is the label we will use to identify SR&ED-eligible tasks
SRED_LABEL="sred-eligible"
ISSUE_LIMIT=1000 ISSUE_LIMIT=1000
DEFAULT_LABEL="sred-eligible"
DEFAULT_STATE="open"
echo "🚀 Starting export of SR&ED-eligible issues to '${OUTPUT_FILE}'..." # --- Parse Command Line Arguments ---
STATE=""
LABEL=""
# If no arguments are provided, run in legacy SR&ED mode
if [ $# -eq 0 ]; then
STATE=$DEFAULT_STATE
LABEL=$DEFAULT_LABEL
else
while [[ "$#" -gt 0 ]]; do
case $1 in
--state=*) STATE="${1#*=}" ;;
--state) STATE="$2"; shift ;;
--label=*) LABEL="${1#*=}" ;;
--label) LABEL="$2"; shift ;;
*) echo "Unknown parameter passed: $1"; exit 1 ;;
esac
shift
done
fi
# --- Dynamic Configuration ---
STATE_FOR_CMD=${STATE:-"open"} # Default to open if state is empty
LABEL_FOR_FILENAME=${LABEL:-"all"}
STATE_FOR_FILENAME=${STATE:-"open"}
OUTPUT_FILE="export-issues-${STATE_FOR_FILENAME}-${LABEL_FOR_FILENAME}.md"
TITLE="Export of GitHub Issues (State: ${STATE_FOR_FILENAME}, Label: ${LABEL_FOR_FILENAME})"
FETCH_MESSAGE="Fetching issues with state '${STATE_FOR_CMD}'"
if [ -n "$LABEL" ]; then
FETCH_MESSAGE="${FETCH_MESSAGE} and label '${LABEL}'"
fi
echo "🚀 Starting export of GitHub issues to '${OUTPUT_FILE}'..."
# --- Step 1: Dependency Check --- # --- Step 1: Dependency Check ---
echo "1. Checking for 'gh' CLI dependency..." echo "1. Checking for 'gh' CLI dependency..."
@@ -22,27 +54,30 @@ fi
echo "✅ 'gh' CLI found." echo "✅ 'gh' CLI found."
# --- Step 2: Initialize Output File --- # --- Step 2: Initialize Output File ---
echo "# Export of SR&ED-Eligible Issues" > "$OUTPUT_FILE" echo "# ${TITLE}" > "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE"
echo "*This document lists the systematic investigations and experimental development tasks undertaken during this period. Export generated on $(date)*." >> "$OUTPUT_FILE" echo "*Export generated on $(date)*." >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE"
# --- Step 3: Fetch SR&ED-Eligible Issues --- # --- Step 3: Build 'gh' command and Fetch Issues ---
echo "2. Fetching open issues with the '${SRED_LABEL}' label..." echo "2. ${FETCH_MESSAGE}..."
# We use 'gh issue list' with a JSON output and parse it with 'jq' for robustness. GH_COMMAND=("gh" "issue" "list" "--state" "${STATE_FOR_CMD}" "--limit" "$ISSUE_LIMIT" "--json" "number")
# This is more reliable than parsing text output. if [ -n "$LABEL" ]; then
issue_numbers=$(gh issue list --state open --label "${SRED_LABEL}" --limit $ISSUE_LIMIT --json number | jq -r '.[].number') GH_COMMAND+=("--label" "${LABEL}")
fi
issue_numbers=$("${GH_COMMAND[@]}" | jq -r '.[].number')
if [ -z "$issue_numbers" ]; then if [ -z "$issue_numbers" ]; then
echo "⚠️ No open issues found with the label '${SRED_LABEL}'. The export file will be minimal." echo "⚠️ No issues found matching the criteria."
echo "" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE"
echo "**No SR&ED-eligible issues found for this period.**" >> "$OUTPUT_FILE" echo "**No issues found.**" >> "$OUTPUT_FILE"
exit 0 exit 0
fi fi
total_issues=$(echo "$issue_numbers" | wc -l | xargs) total_issues=$(echo "$issue_numbers" | wc -l | xargs)
echo "✅ Found ${total_issues} SR&ED-eligible issue(s)." echo "✅ Found ${total_issues} issue(s)."
# --- Step 4: Loop Through Each Issue and Format the Output --- # --- Step 4: Loop Through Each Issue and Format the Output ---
echo "3. Formatting details for each issue..." echo "3. Formatting details for each issue..."
@@ -53,11 +88,10 @@ for number in $issue_numbers; do
echo " -> Processing issue #${number} (${current_issue}/${total_issues})" echo " -> Processing issue #${number} (${current_issue}/${total_issues})"
# Use 'gh issue view' with a template to format the output for each issue # Use 'gh issue view' with a template to format the output for each issue
# and append it to the output file. gh issue view "$number" --json number,title,body,author,createdAt,state --template \
gh issue view "$number" --json number,title,body,author,createdAt --template \ '\n### [#{{.number}}] {{.title}} ({{.state}})\n\n{{if .body}}{{.body}}{{else}}*No description provided.*{{end}}\n\n**Meta:** {{.author.login}} | **Created:** {{timefmt "2006-01-02" .createdAt}}\n***\n' >> "$OUTPUT_FILE"
'\n### Task: [#{{.number}}] {{.title}}\n\n**Hypothesis/Goal:** \n> *(Briefly describe the technological uncertainty this task addresses. What was the technical challenge or question?)*\n\n**Systematic Investigation:**\n{{if .body}}\n{{.body}}\n{{else}}\n*No detailed description provided in the issue.*\n{{end}}\n\n**Team:** {{.author.login}} | **Date Initiated:** {{timefmt "2006-01-02" .createdAt}}\n***\n' >> "$OUTPUT_FILE"
done done
echo "" echo ""
echo "🎉 Export complete!" echo "🎉 Export complete!"
echo "Your SR&ED-ready markdown file is ready: ${OUTPUT_FILE}" echo "Your markdown file is ready: ${OUTPUT_FILE}"