first test

This commit is contained in:
Tomas Dvorak
2026-02-08 14:14:55 +01:00
parent 18aa702174
commit d27cf14110
372 changed files with 98089 additions and 2585 deletions
+26
View File
@@ -0,0 +1,26 @@
# OAuth Service Configuration
OAUTH_SERVICE_PORT=9090
OAUTH_GIN_MODE=debug
OAUTH_CORS_ALLOWED_ORIGINS=*
# GitHub OAuth Configuration
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_REDIRECT_URL=http://localhost:9090/auth/github/callback
# Production URLs (update these for your deployment)
DEFAULT_CLIENT_URL=https://yourdomain.com
SERVICE_DOMAIN=https://oauth.yourdomain.com
# JWT Configuration for OAuth Service
OAUTH_JWT_SECRET=your_oauth_jwt_secret_here
OAUTH_JWT_EXPIRES_IN=24h
# Database Configuration (if using separate database for OAuth)
OAUTH_DB_TYPE=postgres
OAUTH_DB_HOST=localhost
OAUTH_DB_PORT=5432
OAUTH_DB_USER=oauth_user
OAUTH_DB_PASSWORD=your_oauth_password
OAUTH_DB_NAME=oauth_db
OAUTH_DB_SSL_MODE=disable
+56
View File
@@ -0,0 +1,56 @@
# OAuth Service Configuration Changes
## Summary of Changes
### 1. CORS Configuration Updated
- **Before**: Restricted to specific origins (`http://localhost:5173,http://localhost:8080`)
- **After**: Allows all origins (`*`) for maximum flexibility
- **Implementation**: Updated CORS middleware to handle wildcard origins properly
### 2. Dynamic Client URL Detection
- **Before**: Hardcoded default client URL (`http://localhost:5173`)
- **After**: Dynamically determines client URL from:
- Query parameter `redirect_uri` (highest priority)
- Request `Origin` header
- Request `Referer` header
- Fallback to `DEFAULT_CLIENT_URL` environment variable
- **Implementation**: Enhanced `initiateGitHubOAuth` function with URL parsing logic
### 3. Service Domain Configuration
- **Added**: New `SERVICE_DOMAIN` environment variable
- **Purpose**: Identifies the OAuth service domain in logs and webhook responses
- **Current Value**: `https://oauth.tdvorak.dev`
### 4. Enhanced Webhook Handling
- **Before**: Basic webhook processing with minimal logging
- **After**:
- Proper webhook secret configuration check
- Enhanced logging with service domain identification
- Detailed event type handling with better payload logging
- Response includes service domain information
### 5. Environment Files Updated
- **`.env`**: Updated with new configuration values
- **`.env.example`**: Updated to reflect the new structure for other deployments
## Key Benefits
1. **Multi-domain Support**: Service can now handle requests from any domain
2. **Dynamic Client Detection**: Automatically redirects users back to their originating domain
3. **Better Debugging**: Enhanced logging makes troubleshooting easier
4. **Production Ready**: Configuration is more flexible for different deployment scenarios
## Security Considerations
- While CORS is set to allow all origins, the OAuth flow itself remains secure
- State parameter validation prevents CSRF attacks
- JWT tokens are still properly validated
- Webhook signature validation is in place (though secret needs to be configured)
## Usage
The service will now:
1. Accept OAuth requests from any domain
2. Automatically detect the client's origin for proper redirects
3. Handle webhooks with better logging and domain identification
4. Work seamlessly with the user's domain (`tdvorak.dev`) and any other domains
+50
View File
@@ -0,0 +1,50 @@
FROM golang:1.21-alpine AS builder
# Set the working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy the source code
COPY . .
# Build the binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o oauth-service main.go
# Final stage
FROM alpine:latest
# Install ca-certificates for HTTPS requests
RUN apk --no-cache add ca-certificates
# Create a non-root user
RUN addgroup -g 1001 -S oauth && \
adduser -u 1001 -S oauth -G oauth
WORKDIR /app
# Copy the binary from builder stage
COPY --from=builder /app/oauth-service .
# Copy .env file if it exists
COPY --from=builder /app/.env.example .env
# Change ownership to non-root user
RUN chown -R oauth:oauth /app
# Switch to non-root user
USER oauth
# Expose port
EXPOSE 9090
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:9090/health || exit 1
# Run the binary
CMD ["./oauth-service"]
+66
View File
@@ -0,0 +1,66 @@
# TSX Integration Fixes Summary
## ✅ All Errors Fixed Successfully
### **TypeScript Configuration Fixed:**
- ✅ Removed problematic `solid-js/env` type from tsconfig.json
- ✅ Fixed all event handler type annotations
- ✅ Resolved null safety issues with event.currentTarget
### **Event Handler Fixes:**
- ✅ Added proper `MouseEvent` typing for onClick handlers
- ✅ Fixed HTMLElement casting for DOM queries
- ✅ Added null safety checks with optional chaining
### **Build System Fixed:**
- ✅ Renamed `.js` config files to `.cjs` for ES module compatibility
- ✅ Fixed PostCSS and TailwindCSS configuration
- ✅ All builds now pass without errors
### **Component Structure:**
- ✅ All TSX components properly typed with TypeScript
- ✅ SolidJS reactive signals working correctly
- ✅ Event handlers properly typed and functional
## 🚀 Final Status
**✅ TypeScript Check:** `npx tsc --noEmit` - No errors
**✅ Build:** `npm run build` - Successful
**✅ Dev Server:** `npm run dev` - Working
**✅ Backend:** `go run main.go` - Running successfully
**✅ Integration:** Full-stack system operational
## 📁 Project Structure
```
oauth-service/
├── src/
│ ├── components/
│ │ ├── Dashboard.tsx ✅ Fixed
│ │ ├── CourseManagement.tsx ✅ Fixed
│ │ └── InstanceManagement.tsx ✅ Fixed
│ ├── App.tsx ✅ Working
│ ├── index.tsx ✅ Working
│ └── styles.css ✅ Working
├── static/ ✅ Built frontend
├── main.go ✅ Backend running
├── tsconfig.json ✅ Fixed config
├── package.json ✅ Dependencies installed
└── dev.sh ✅ Development script
```
## 🎯 Ready to Use
**Development:**
```bash
./dev.sh # Starts both frontend (5174) and backend (9090)
```
**Production:**
```bash
npm run build && go run main.go
```
**Access:** http://localhost:9090/dashboard
All TypeScript errors have been resolved and the system is fully functional! 🎉
+283
View File
@@ -0,0 +1,283 @@
# Centralized OAuth Service
This is a **standalone OAuth service** that handles GitHub authentication and email verification for all users. Users never need to set up their own OAuth applications - everything is centralized.
## 🎯 **How It Works**
### **For Users:**
1. **GitHub OAuth**: Click "Connect GitHub" → GitHub authorization → Automatic login with GitHub profile
2. **Email Verification**: Enter email → Receive verification code → Verify email for 2FA
### **For Developers:**
1. **Zero setup** - No OAuth app creation needed
2. **Simple integration** - Just redirect to our service
3. **Secure authentication** - We handle all the complexity
4. **User management** - Centralized user database
## 🚀 **Quick Start**
### **1. Setup the OAuth Service**
```bash
# Navigate to the OAuth service
cd oauth-service
# Run the setup script
./setup.sh
# Edit the .env file with your GitHub OAuth credentials
nano .env
# Start the service
go run main.go
```
### **2. GitHub OAuth App Setup (One Time)**
1. Go to GitHub Settings → Developer settings → OAuth Apps
2. Create a new OAuth app with:
- **Application name**: Trackeep OAuth Service
- **Homepage URL**: `http://localhost:9090`
- **Authorization callback URL**: `http://localhost:9090/auth/github/callback`
3. Copy the Client ID and Client Secret to `.env`
### **3. Email Verification Setup (One Time)**
1. Configure smtp.purelymail.com for sending verification emails:
- **SMTP Host**: `smtp.purelymail.com`
- **SMTP Port**: `587`
- **Username**: Your purelymail SMTP username
- **Password**: Your purelymail SMTP password
2. Add SMTP credentials to `.env` file
3. The service will send 6-digit verification codes for 2FA
### **4. Integration in Your App**
```javascript
// Redirect to GitHub OAuth
const connectGitHub = () => {
window.location.href = 'http://localhost:9090/auth/github?redirect_uri=' +
encodeURIComponent(window.location.origin);
};
// Send email verification code
const sendEmailVerification = (email) => {
fetch('http://localhost:9090/api/v1/email/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
}).then(response => response.json())
.then(data => {
if (data.demo_code) {
console.log('Demo verification code:', data.demo_code);
}
});
};
// Verify email code
const verifyEmailCode = (email, code) => {
fetch('http://localhost:9090/api/v1/email/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, code })
}).then(response => response.json())
.then(data => {
if (data.verified) {
console.log('Email verified successfully!');
}
});
};
// Handle callback (works for both GitHub and Email)
const handleCallback = () => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const username = urlParams.get('user');
if (token) {
localStorage.setItem('token', token);
localStorage.setItem('username', username);
// Redirect to dashboard
window.location.href = '/app';
}
};
```
## 📡 **API Endpoints**
### **OAuth Endpoints:**
- `GET /auth/github` - Initiate GitHub OAuth flow
- `GET /auth/github/callback` - Handle GitHub callback
### **Email Verification Endpoints:**
- `POST /api/v1/email/send` - Send verification code to email
- `POST /api/v1/email/verify` - Verify email code for 2FA
### **API Endpoints:**
- `GET /api/v1/user/me` - Get current user info
- `GET /api/v1/user/:username/repos` - Get user repositories
- `POST /api/v1/webhook/github` - GitHub webhook handler
- `POST /api/v1/email/verify` - Verify email code
### **Utility:**
- `GET /health` - Service health check
## 🔧 **Configuration**
### **Environment Variables:**
```bash
# GitHub OAuth (Admin Only)
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_REDIRECT_URL=http://localhost:9090/auth/github/callback
# Email Verification Configuration (Admin Only)
SMTP_HOST=smtp.purelymail.com
SMTP_PORT=587
SMTP_USERNAME=your_purelymail_username
SMTP_PASSWORD=your_purelymail_password
# Service Configuration
PORT=9090
JWT_SECRET=your-super-secret-jwt-key
DEFAULT_CLIENT_URL=http://localhost:5173
# CORS
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:8080
```
## 🏗️ **Architecture**
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ User App │ │ OAuth Service │ │ GitHub │
│ │ │ │ │ │
│ Connect GitHub ─┼───>│ /auth/github ────>│ OAuth Flow │
│ │ │ │ │ │
│ Handle Callback │<───>│ /auth/callback │<───>│ Return Token │
│ │ │ │ │ │
│ Store Token │ │ Generate JWT │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
```
## 🔒 **Security Features**
- **CSRF Protection**: State parameter validation
- **Secure JWT**: Signed tokens with expiration
- **CORS Support**: Configurable allowed origins
- **Webhook Support**: Optional webhook secret validation
- **Rate Limiting**: GitHub API rate limit awareness
## 📊 **User Management**
The service maintains a centralized user database:
```go
type User struct {
ID int `json:"id"`
GitHubID int `json:"github_id"`
Username string `json:"username"`
Email string `json:"email"`
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"`
LastLogin time.Time `json:"last_login"`
}
```
## 🔄 **Multi-Application Support**
The same OAuth service can serve multiple applications:
```javascript
// App 1
window.location.href = 'http://localhost:9090/auth/github?redirect_uri=http://app1.com';
// App 2
window.location.href = 'http://localhost:9090/auth/github?redirect_uri=http://app2.com';
// App 3
window.location.href = 'http://localhost:9090/auth/github?redirect_uri=http://app3.com';
```
## 🚀 **Production Deployment**
### **Docker Deployment:**
```dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download && go build -o oauth-service
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/oauth-service .
COPY .env .
EXPOSE 9090
CMD ["./oauth-service"]
```
### **Docker Compose:**
```yaml
version: '3.8'
services:
oauth-service:
build: ./oauth-service
ports:
- "9090:9090"
environment:
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- JWT_SECRET=${JWT_SECRET}
restart: unless-stopped
```
## 🛠️ **Development**
```bash
# Install dependencies
go mod tidy
# Run in development
go run main.go
# Build for production
go build -o oauth-service main.go
# Run tests
go test ./...
```
## 📝 **Benefits**
### **For Users:**
-**Zero configuration** - No OAuth app setup
-**Single sign-on** - One GitHub account for all apps
-**Secure** - Enterprise-grade security
-**Fast** - Instant authentication
### **For Developers:**
-**Easy integration** - Just redirect to our service
-**No OAuth management** - We handle everything
-**Centralized users** - Shared user database
-**Scalable** - Serve unlimited applications
### **For Administrators:**
-**Single control point** - Manage all OAuth in one place
-**Security oversight** - Monitor all authentication
-**Easy updates** - Update OAuth settings once
-**Cost effective** - One OAuth app for all services
## 🎯 **Use Cases**
- **SaaS platforms** - Multiple products, one authentication
- **Development teams** - Internal tools with GitHub login
- **Open source projects** - Contributor authentication
- **Enterprise** - Internal service authentication
- **API services** - Secure API access with GitHub OAuth
This service completely abstracts away OAuth complexity while providing enterprise-grade authentication for all your applications!
+308
View File
@@ -0,0 +1,308 @@
# Trackeep Main Controller
The **Trackeep Main Controller** is a centralized service that handles authentication, user management, and learning content management for all Trackeep instances. It transforms the original OAuth service into a comprehensive learning management system with a beautiful dashboard interface.
## 🛠️ **Tech Stack**
### **Backend:**
- **Go** - High-performance API server
- **Gin** - HTTP web framework
- **JWT** - Authentication tokens
- **OAuth2** - GitHub integration
### **Frontend:**
- **SolidJS** - Reactive UI framework
- **TypeScript** - Type-safe development
- **TailwindCSS** - Utility-first styling
- **Vite** - Fast build tool
### **Features:**
- **🔐 Centralized Authentication** - GitHub OAuth and email verification for all users
- **📚 Learning Management** - Create and manage free courses with YouTube, ZTM, GitHub, and Fireship resources
- **🖥️ Instance Management** - Register and monitor Trackeep instances
- **📊 Visual Dashboard** - Beautiful Trackeep-inspired UI for management
- **🔗 Secure Connections** - Automatic secure API key handling between instances
### **For Users:**
- **Free Learning** - All courses are completely free (price always $0.00)
- **No Instructors** - Self-paced learning with curated resources
- **Progress Tracking** - Monitor your learning progress across courses
- **Single Sign-On** - One GitHub account for all Trackeep instances
### **For Administrators:**
- **Course Creation** - Easy-to-use interface for creating learning paths
- **Resource Management** - Support for YouTube, Zero to Mastery, GitHub, Fireship links
- **Instance Monitoring** - Track all connected Trackeep instances
- **User Analytics** - Dashboard with comprehensive statistics
## 🚀 **Quick Start**
### **1. Setup the Main Controller**
```bash
# Navigate to the main controller
cd oauth-service
# Install frontend dependencies
npm install
# Build the frontend
npm run build
# Run the service (production mode)
go run main.go
```
### **2. Development Mode**
For development with hot reload:
```bash
# Use the development script (starts both backend and frontend)
./dev.sh
# Or start manually:
# Terminal 1: Backend
go run main.go
# Terminal 2: Frontend dev server
npm run dev
```
### **3. Access the Dashboard**
Open your browser to:
- **Dashboard**: http://localhost:9090/dashboard (production) or http://localhost:5174/dashboard (development)
- **Course Management**: http://localhost:9090/dashboard/courses
- **Instance Management**: http://localhost:9090/dashboard/instances
- **API Documentation**: http://localhost:9090/api/v1
### **4. GitHub OAuth Setup (Optional)**
For full authentication, set up GitHub OAuth:
1. Go to GitHub Settings → Developer settings → OAuth Apps
2. Create a new OAuth app with:
- **Application name**: Trackeep Main Controller
- **Homepage URL**: `http://localhost:9090`
- **Authorization callback URL**: `http://localhost:9090/auth/github/callback`
3. Add credentials to `.env` file
## 📡 **API Endpoints**
### **Authentication:**
- `GET /auth/github` - Initiate GitHub OAuth flow
- `GET /auth/github/callback` - Handle GitHub callback
- `POST /api/v1/email/send` - Send verification code
- `POST /api/v1/email/verify` - Verify email code
### **Course Management:**
- `GET /api/v1/courses` - List all courses
- `POST /api/v1/courses` - Create new course
- `GET /api/v1/courses/:id` - Get course details
- `PUT /api/v1/courses/:id` - Update course
- `DELETE /api/v1/courses/:id` - Delete course
- `GET /api/v1/courses/:id/resources` - Get course resources
- `POST /api/v1/courses/:id/resources` - Add course resource
### **User Progress:**
- `GET /api/v1/progress/:user_id` - Get user's all progress
- `GET /api/v1/progress/:user_id/:course_id` - Get course progress
- `POST /api/v1/progress/:user_id/:course_id` - Update progress
### **Instance Management:**
- `GET /api/v1/instances` - List all instances
- `POST /api/v1/instances` - Register new instance
- `GET /api/v1/instances/:id` - Get instance details
- `PUT /api/v1/instances/:id` - Update instance
- `DELETE /api/v1/instances/:id` - Delete instance
### **Dashboard:**
- `GET /api/v1/dashboard/stats` - Get dashboard statistics
- `GET /api/v1/dashboard/courses` - Get courses for dashboard
- `GET /api/v1/dashboard/users` - Get users for dashboard (admin only)
## 🏗️ **Architecture**
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Trackeep App │ │ Main Controller │ │ GitHub API │
│ │ │ │ │ │
│ OAuth Login ────┼───>│ /auth/github ────>│ OAuth Flow │
│ │ │ │ │ │
│ Course API ─────┼───>│ /api/v1/courses │ │ │
│ │ │ │ │ │
│ Progress Sync ──┼───>│ /api/v1/progress │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
```
## 📚 **Course Structure**
### **Supported Resource Types:**
- **🎥 YouTube** - Video tutorials and playlists
- **🎓 Zero to Mastery** - ZTM courses and content
- **🐙 GitHub** - Repositories, projects, and code examples
- **🔥 Fireship** - Fast-paced tutorials and courses
- **🔗 Links** - Any other web resources
### **Course Example:**
```json
{
"title": "Complete Web Development Bootcamp",
"description": "Learn modern web development from scratch",
"category": "web-development",
"difficulty": "beginner",
"duration": 40,
"price": 0.0,
"tags": ["javascript", "react", "nodejs"],
"resources": [
{
"title": "Introduction to Web Development",
"type": "youtube",
"url": "https://www.youtube.com/watch?v=RW-sB6GeA_Q",
"duration": 45,
"is_required": true
}
]
}
```
## 🔒 **Security Features**
- **🔐 JWT Authentication** - Secure token-based authentication
- **🛡️ API Key Management** - Automatic secure key generation for instances
- **🔗 CORS Support** - Configurable allowed origins
- **✅ CSRF Protection** - State parameter validation
- **📊 Rate Limiting** - GitHub API rate limit awareness
## 🎨 **Dashboard Features**
### **Main Dashboard:**
- 📊 Real-time statistics
- 📚 Recent courses overview
- 🖥️ Active instances monitoring
- 📈 User progress analytics
### **Course Management:**
- Easy course creation wizard
- ✏️ Visual course editing
- 🏷️ Tag-based organization
- 📱 Responsive design
### **Instance Management:**
- 🔗 Secure instance registration
- 📊 Connection status monitoring
- 🔑 API key management
- 📈 Instance analytics
## 🔧 **Configuration**
### **Environment Variables:**
```bash
# Service Configuration
PORT=9090
JWT_SECRET=your-super-secret-jwt-key
# GitHub OAuth (Optional)
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_REDIRECT_URL=http://localhost:9090/auth/github/callback
# Email Verification (Optional)
SMTP_HOST=smtp.purelymail.com
SMTP_PORT=587
SMTP_USERNAME=your_purelymail_username
SMTP_PASSWORD=your_purelymail_password
# CORS
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:8080
```
## 🚀 **Production Deployment**
### **Docker Deployment:**
```dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download && go build -o trackeep-controller
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/trackeep-controller .
COPY .env .
COPY templates/ ./templates/
EXPOSE 9090
CMD ["./trackeep-controller"]
```
### **Docker Compose:**
```yaml
version: '3.8'
services:
trackeep-controller:
build: ./oauth-service
ports:
- "9090:9090"
environment:
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- JWT_SECRET=${JWT_SECRET}
restart: unless-stopped
```
## 📝 **Benefits**
### **For Learners:**
-**Completely Free** - All courses are $0.00
-**Self-Paced** - Learn at your own speed
-**Quality Content** - Curated YouTube, ZTM, GitHub, Fireship resources
-**Progress Tracking** - Monitor your learning journey
-**Single Sign-On** - One account for all Trackeep instances
### **For Administrators:**
-**Easy Management** - Beautiful dashboard interface
-**Secure Connections** - Automatic API key handling
-**Scalable** - Serve unlimited instances
-**Analytics** - Comprehensive usage statistics
-**Zero Setup** - Works out of the box with sample data
### **For Developers:**
-**RESTful API** - Clean, well-documented endpoints
-**Flexible Resources** - Support for multiple content types
-**Secure by Default** - Built-in authentication and authorization
-**Easy Integration** - Simple API key-based connections
## 🎯 **Use Cases**
- **🎓 Educational Platforms** - Free learning management system
- **👥 Developer Communities** - Share learning resources
- **🏢 Corporate Training** - Internal skill development
- **📚 Course Aggregators** - Curate learning content
- **🚀 Startup Education** - Onboarding and training programs
## 🔄 **Multi-Instance Support**
The Main Controller can serve multiple Trackeep instances:
```javascript
// Instance 1
fetch('http://localhost:9090/api/v1/courses', {
headers: { 'Authorization': 'Bearer instance1_api_key' }
});
// Instance 2
fetch('http://localhost:9090/api/v1/courses', {
headers: { 'Authorization': 'Bearer instance2_api_key' }
});
```
Each instance gets its own API key and can securely access the centralized course catalog and user management.
---
**Trackeep Main Controller** - Complete learning management system with beautiful dashboard and secure multi-instance support. 🚀
@@ -0,0 +1,198 @@
# Trackeep Integration Guide
## Architecture Overview
This OAuth service is designed **only for authentication**. Trackeep instances (user-hosted) handle all GitHub data tracking directly.
## How It Works
### 1. User Authentication Flow
1. User clicks "Login with GitHub" in Trackeep
2. Trackeep redirects to: `https://oauth.tdvorak.dev/auth/github?redirect_uri=https://user-trackeep-instance.com`
3. OAuth service handles GitHub authentication
4. OAuth service redirects back: `https://user-trackeep-instance.com/auth/callback?token=JWT&user=username`
### 2. What Trackeep Receives
The JWT token contains:
```json
{
"user_id": 123,
"github_id": 456789,
"username": "johndoe",
"email": "john@example.com",
"access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
"token_type": "bearer",
"expires_at": 1738123456,
"exp": 1738123456,
"iat": 1737518656
}
```
### 3. Trackeep GitHub API Access
Trackeep instances can now make GitHub API calls using the user's `access_token`:
```javascript
// Example: Get user repositories
const response = await fetch('https://api.github.com/user/repos', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
});
// Example: Get commits for a repo
const commits = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
});
```
## Trackeep Implementation Guide
### 1. OAuth Login Button
```html
<a href="https://oauth.tdvorak.dev/auth/github?redirect_uri=https://your-trackeep-instance.com">
Login with GitHub
</a>
```
### 2. Handle OAuth Callback
```javascript
// In your /auth/callback route
async function handleOAuthCallback(req, res) {
const { token, user: username } = req.query;
// Decode and verify JWT
const jwtPayload = decodeJWT(token);
// Store user session
req.session.user = {
id: jwtPayload.user_id,
username: jwtPayload.username,
email: jwtPayload.email,
githubAccessToken: jwtPayload.access_token,
tokenType: jwtPayload.token_type,
expiresAt: jwtPayload.expires_at
};
// Redirect to dashboard
res.redirect('/dashboard');
}
```
### 3. GitHub API Helper
```javascript
class GitHubAPI {
constructor(accessToken) {
this.accessToken = accessToken;
}
async makeRequest(url) {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
});
return response.json();
}
async getUserRepos() {
return this.makeRequest('https://api.github.com/user/repos');
}
async getRepoCommits(owner, repo) {
return this.makeRequest(`https://api.github.com/repos/${owner}/${repo}/commits`);
}
async getRepoPulls(owner, repo) {
return this.makeRequest(`https://api.github.com/repos/${owner}/${repo}/pulls`);
}
async getBranches(owner, repo) {
return this.makeRequest(`https://api.github.com/repos/${owner}/${repo}/branches`);
}
}
```
### 4. Track Data Collection
```javascript
// Example: Track repository activity
async function trackRepositoryActivity(user, repoFullName) {
const [owner, repo] = repoFullName.split('/');
const github = new GitHubAPI(user.githubAccessToken);
// Get commits
const commits = await github.getRepoCommits(owner, repo);
// Get pull requests
const pulls = await github.getRepoPulls(owner, repo);
// Store in your local database
await storeActivityData({
userId: user.id,
repo: repoFullName,
commits: commits.length,
pullRequests: pulls.length,
lastActivity: new Date()
});
}
```
## Security Considerations
### 1. Token Storage
- Store GitHub access tokens securely (encrypted at rest)
- Never expose tokens in client-side JavaScript
- Use secure, HTTP-only cookies for session management
### 2. Token Expiration
- Monitor `expires_at` field in JWT
- Refresh tokens before expiration if needed
- Handle token expiry gracefully
### 3. Rate Limiting
- GitHub API has rate limits (5,000 requests/hour for authenticated users)
- Implement caching to reduce API calls
- Handle rate limit responses (HTTP 429)
## Available GitHub Scopes
The OAuth service requests these scopes:
- `user:email` - Read user email addresses
- `read:user` - Read user profile data
- `repo` - Access to repositories (full control)
This allows Trackeep instances to:
- Read repository data
- Access commit history
- Monitor pull requests
- Track branch activity
## API Endpoints
### OAuth Service
- `GET /auth/github` - Initiate OAuth flow
- `GET /auth/github/callback` - Handle GitHub callback
- `GET /api/v1/user/me` - Get current user info
### GitHub API (via access token)
- `GET /user/repos` - User repositories
- `GET /repos/{owner}/{repo}/commits` - Repository commits
- `GET /repos/{owner}/{repo}/pulls` - Pull requests
- `GET /repos/{owner}/{repo}/branches` - Branches
- And all other GitHub API endpoints
## Benefits of This Architecture
1. **Separation of Concerns** - OAuth service only handles authentication
2. **User Privacy** - GitHub data stays in user's Trackeep instance
3. **Scalability** - Each user instance handles its own GitHub API calls
4. **Security** - No centralized GitHub data storage
5. **Flexibility** - Trackeep can implement custom tracking logic
## Example Implementation
See the `examples/` directory for complete implementation examples in different frameworks.
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Trackeep Main Controller Development Script
# This script starts both the backend API server and frontend dev server
echo "🚀 Starting Trackeep Main Controller Development Environment..."
# Check if we're in the right directory
if [ ! -f "main.go" ]; then
echo "❌ Error: Please run this script from the oauth-service directory"
exit 1
fi
# Start backend server in background
echo "🔧 Starting backend API server on port 9090..."
go run main.go &
BACKEND_PID=$!
# Wait a moment for backend to start
sleep 2
# Start frontend dev server
echo "🎨 Starting frontend dev server on port 5174..."
npm run dev &
FRONTEND_PID=$!
echo ""
echo "✅ Trackeep Main Controller is running!"
echo ""
echo "📊 Dashboard: http://localhost:5174/dashboard"
echo "📚 Courses: http://localhost:5174/dashboard/courses"
echo "🖥️ Instances: http://localhost:5174/dashboard/instances"
echo "🔧 API: http://localhost:9090/api/v1"
echo "💚 Health Check: http://localhost:9090/health"
echo ""
echo "Press Ctrl+C to stop both servers"
echo ""
# Function to kill both processes on exit
cleanup() {
echo ""
echo "🛑 Stopping servers..."
kill $BACKEND_PID 2>/dev/null
kill $FRONTEND_PID 2>/dev/null
echo "✅ All servers stopped"
exit 0
}
# Set up trap to kill processes on Ctrl+C
trap cleanup INT
# Wait for both processes
wait
+49
View File
@@ -0,0 +1,49 @@
version: '3.8'
services:
oauth-service:
build: ./oauth-service
container_name: github-oauth-service
ports:
- "9090:9090"
environment:
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- GITHUB_REDIRECT_URL=http://localhost:9090/auth/github/callback
- JWT_SECRET=${JWT_SECRET:-your-super-secret-jwt-key-change-in-production}
- PORT=9090
- GIN_MODE=release
- CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:8080,https://yourdomain.com
- DEFAULT_CLIENT_URL=http://localhost:5173
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
volumes:
- ./oauth-service/.env:/app/.env:ro
restart: unless-stopped
networks:
- oauth-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9090/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Optional: Redis for session storage (for production)
redis:
image: redis:7-alpine
container_name: oauth-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
restart: unless-stopped
networks:
- oauth-network
command: redis-server --appendonly yes
volumes:
redis-data:
networks:
oauth-network:
driver: bridge
+39
View File
@@ -0,0 +1,39 @@
module trackeep-main-controller
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/joho/godotenv v1.4.0
golang.org/x/oauth2 v0.8.0
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+104
View File
@@ -0,0 +1,104 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Trackeep Main Controller</title>
</head>
<body>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "trackeep-main-controller-ui",
"version": "1.0.0",
"description": "Trackeep Main Controller Frontend",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"solid-js": "^1.8.7",
"@solidjs/router": "^0.8.3",
"tailwindcss": "^3.4.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vite-plugin-solid": "^2.8.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# GitHub OAuth Service Setup Script
echo "🚀 Setting up GitHub OAuth Service..."
# Create directory if it doesn't exist
mkdir -p oauth-service
cd oauth-service
# Check if Go is installed
if ! command -v go &> /dev/null; then
echo "❌ Go is not installed. Please install Go first."
exit 1
fi
# Initialize Go module
echo "📦 Initializing Go module..."
go mod init github-oauth-service
# Install dependencies
echo "📥 Installing dependencies..."
go get github.com/gin-gonic/gin
go get github.com/golang-jwt/jwt/v5
go get github.com/joho/godotenv
go get golang.org/x/oauth2
# Create .env file if it doesn't exist
if [ ! -f .env ]; then
echo "📝 Creating .env file from template..."
cp .env.example .env
echo "⚠️ Please edit .env file with your GitHub OAuth credentials"
fi
# Make the service executable
chmod +x main.go
echo "✅ GitHub OAuth Service setup complete!"
echo ""
echo "📋 Next steps:"
echo "1. Edit oauth-service/.env with your GitHub OAuth credentials"
echo "2. Run: cd oauth-service && go run main.go"
echo "3. Service will start on port 9090"
echo ""
echo "🔗 OAuth endpoints:"
echo "- Initiate: http://localhost:9090/auth/github"
echo "- Callback: http://localhost:9090/auth/github/callback"
echo "- Health: http://localhost:9090/health"
+18
View File
@@ -0,0 +1,18 @@
import { Router, Route } from '@solidjs/router';
import { Dashboard } from './components/Dashboard';
import { CourseManagement } from './components/CourseManagement';
import { InstanceManagement } from './components/InstanceManagement';
import './styles.css';
function App() {
return (
<Router>
<Route path="/" component={Dashboard} />
<Route path="/dashboard" component={Dashboard} />
<Route path="/dashboard/courses" component={CourseManagement} />
<Route path="/dashboard/instances" component={InstanceManagement} />
</Router>
);
}
export default App;
@@ -0,0 +1,537 @@
import { createSignal, onMount, For, Show } from 'solid-js';
interface Course {
id: number;
title: string;
description: string;
category: string;
difficulty: 'beginner' | 'intermediate' | 'advanced';
duration: number;
price: number;
thumbnail: string;
tags: string[];
resources: CourseResource[];
created_at: string;
updated_at: string;
created_by: number;
is_active: boolean;
}
interface CourseResource {
id: number;
course_id: number;
title: string;
type: 'youtube' | 'ztm' | 'github' | 'fireship' | 'link';
url: string;
description: string;
duration: number;
order: number;
is_required: boolean;
}
interface Instance {
id: number;
name: string;
url: string;
api_key: string;
is_active: boolean;
version: string;
created_at: string;
last_sync: string;
admin_user_id: number;
}
export const CourseManagement = () => {
const [courses, setCourses] = createSignal<Course[]>([]);
const [instances, setInstances] = createSignal<Instance[]>([]);
const [loading, setLoading] = createSignal(true);
const [showModal, setShowModal] = createSignal(false);
const [editingCourse, setEditingCourse] = createSignal<Course | null>(null);
const [tags, setTags] = createSignal<string[]>([]);
const [resources, setResources] = createSignal<CourseResource[]>([]);
const [tagInput, setTagInput] = createSignal('');
// Form state
const [formData, setFormData] = createSignal({
title: '',
category: '',
difficulty: '' as 'beginner' | 'intermediate' | 'advanced' | '',
duration: '',
description: '',
});
const categories = [
'programming',
'design',
'business',
'marketing',
'data-science',
'web-development',
'mobile-development',
'devops',
'other'
];
const resourceTypes = [
{ value: 'youtube', label: 'YouTube', color: '#ff0000' },
{ value: 'ztm', label: 'ZTM', color: '#3b82f6' },
{ value: 'github', label: 'GitHub', color: '#333' },
{ value: 'fireship', label: 'Fireship', color: '#f59e0b' },
{ value: 'link', label: 'Link', color: '#6b7280' }
];
onMount(async () => {
await loadCourses();
await loadInstances();
});
const loadCourses = async () => {
try {
const response = await fetch('/api/v1/courses');
const data = await response.json();
setCourses(data.courses || []);
} catch (error) {
console.error('Error loading courses:', error);
} finally {
setLoading(false);
}
};
const loadInstances = async () => {
try {
const response = await fetch('/api/v1/instances');
const data = await response.json();
setInstances(data.instances || []);
} catch (error) {
console.error('Error loading instances:', error);
}
};
const openCreateModal = () => {
setEditingCourse(null);
setFormData({
title: '',
category: '',
difficulty: '',
duration: '',
description: '',
});
setTags([]);
setResources([]);
setShowModal(true);
};
const openEditModal = (course: Course) => {
setEditingCourse(course);
setFormData({
title: course.title,
category: course.category,
difficulty: course.difficulty,
duration: course.duration.toString(),
description: course.description,
});
setTags(course.tags || []);
setResources(course.resources || []);
setShowModal(true);
};
const closeModal = () => {
setShowModal(false);
setEditingCourse(null);
setTags([]);
setResources([]);
};
const addTag = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
const value = tagInput().trim();
if (value && !tags().includes(value)) {
setTags([...tags(), value]);
setTagInput('');
}
}
};
const removeTag = (tagToRemove: string) => {
setTags(tags().filter(tag => tag !== tagToRemove));
};
const addResource = () => {
setResources([...resources(), {
id: Date.now(),
course_id: editingCourse()?.id || 0,
title: '',
type: 'link',
url: '',
description: '',
duration: 0,
order: resources().length + 1,
is_required: false
}]);
};
const updateResource = (index: number, field: keyof CourseResource, value: any) => {
const updatedResources = [...resources()];
updatedResources[index] = { ...updatedResources[index], [field]: value };
setResources(updatedResources);
};
const removeResource = (index: number) => {
setResources(resources().filter((_, i) => i !== index));
};
const saveCourse = async () => {
try {
const courseData = {
...formData(),
duration: parseInt(formData().duration),
tags: tags(),
resources: resources()
};
const url = editingCourse() ? `/api/v1/courses/${editingCourse()!.id}` : '/api/v1/courses';
const method = editingCourse() ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(courseData)
});
if (response.ok) {
closeModal();
await loadCourses();
} else {
const error = await response.json();
alert('Error: ' + (error.error || 'Failed to save course'));
}
} catch (error) {
console.error('Error saving course:', error);
alert('Error: Failed to save course');
}
};
const deleteCourse = async (courseId: number) => {
if (!confirm('Are you sure you want to delete this course?')) return;
try {
const response = await fetch(`/api/v1/courses/${courseId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
await loadCourses();
} else {
const error = await response.json();
alert('Error: ' + (error.error || 'Failed to delete course'));
}
} catch (error) {
console.error('Error deleting course:', error);
alert('Error: Failed to delete course');
}
};
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'bg-green-100 text-green-800';
case 'intermediate': return 'bg-orange-100 text-orange-800';
case 'advanced': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
return (
<div class="min-h-screen bg-gradient-to-br from-indigo-500 to-purple-600 p-6">
<div class="max-w-7xl mx-auto">
{/* Header */}
<header class="bg-white/95 backdrop-blur-sm rounded-2xl p-6 mb-8 shadow-xl">
<div class="flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-white font-bold">
T
</div>
<h1 class="text-2xl font-bold text-gray-900">Trackeep Controller</h1>
</div>
<nav class="flex gap-2">
<a href="/dashboard" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Dashboard</a>
<a href="/dashboard/courses" class="px-4 py-2 rounded-lg bg-indigo-500 text-white hover:bg-indigo-600 transition-colors">Courses</a>
<a href="/dashboard/instances" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Instances</a>
<a href="/api/v1/user/me" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Profile</a>
</nav>
</div>
</header>
{/* Main Content */}
<div class="bg-white/95 backdrop-blur-sm rounded-2xl p-6 mb-8 shadow-xl">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold text-gray-900">Course Management</h2>
<button
onClick={openCreateModal}
class="px-6 py-3 bg-indigo-500 text-white rounded-lg hover:bg-indigo-600 transition-colors flex items-center gap-2"
>
<span>+</span> Create New Course
</button>
</div>
<Show when={loading()} fallback={
<Show when={courses().length > 0} fallback={
<div class="text-center py-16 text-gray-500">
<div class="text-6xl mb-4 opacity-50">📚</div>
<div class="text-xl font-semibold mb-2">No courses yet</div>
<p>Create your first learning course to get started!</p>
</div>
}>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={courses()}>
{(course) => (
<div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden group">
<div class="h-48 bg-gradient-to-r from-indigo-500 to-purple-600 relative">
<div class="absolute inset-0 flex items-center justify-center text-white text-5xl font-bold">
{course.title.charAt(0).toUpperCase()}
</div>
<div class="absolute top-4 right-4 bg-white/90 backdrop-blur-sm px-3 py-1 rounded-full text-sm font-semibold text-gray-900">
FREE
</div>
</div>
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-2">{course.title}</h3>
<p class="text-gray-600 text-sm mb-4 line-clamp-2">{course.description}</p>
<div class="flex justify-between items-center mb-4 text-sm text-gray-500">
<span>{course.category}</span>
<span class={`px-2 py-1 rounded-full text-xs font-medium ${getDifficultyColor(course.difficulty)}`}>
{course.difficulty}
</span>
<span>{course.duration}h</span>
</div>
<div class="flex gap-2">
<button
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors text-sm"
onClick={() => window.open(`/api/v1/courses/${course.id}`, '_blank')}
>
👁 View
</button>
<button
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors text-sm"
onClick={() => openEditModal(course)}
>
Edit
</button>
<button
class="flex-1 px-3 py-2 border border-red-300 text-red-600 rounded-lg hover:bg-red-50 transition-colors text-sm"
onClick={() => deleteCourse(course.id)}
>
🗑 Delete
</button>
</div>
</div>
</div>
)}
</For>
</div>
</Show>
}>
<div class="text-center py-8 text-gray-500">Loading courses...</div>
</Show>
</div>
</div>
{/* Course Modal */}
<Show when={showModal()}>
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-2xl p-8 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-semibold text-gray-900">
{editingCourse() ? 'Edit Course' : 'Create New Course'}
</h3>
<button
onClick={closeModal}
class="text-gray-500 hover:text-gray-700 text-2xl font-bold"
>
&times;
</button>
</div>
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Course Title *</label>
<input
type="text"
value={formData().title}
onInput={(e) => setFormData({ ...formData(), title: e.currentTarget.value })}
placeholder="Course Title"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Category *</label>
<select
value={formData().category}
onChange={(e) => setFormData({ ...formData(), category: e.currentTarget.value })}
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select Category</option>
<For each={categories}>
{(category) => <option value={category}>{category}</option>}
</For>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Difficulty *</label>
<select
value={formData().difficulty}
onChange={(e) => setFormData({ ...formData(), difficulty: e.currentTarget.value as any })}
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select Difficulty</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Duration (hours) *</label>
<input
type="number"
value={formData().duration}
onInput={(e) => setFormData({ ...formData(), duration: e.currentTarget.value })}
min="1"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Description *</label>
<textarea
value={formData().description}
onInput={(e) => setFormData({ ...formData(), description: e.currentTarget.value })}
placeholder="Course description"
rows={4}
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Tags (press Enter to add)</label>
<div class="flex flex-wrap gap-2 p-3 border-2 border-gray-200 rounded-lg min-h-[50px] cursor-text" onClick={(e: MouseEvent) => {
const target = e.currentTarget as HTMLElement;
const input = target.querySelector('input') as HTMLInputElement;
input?.focus();
}}>
<For each={tags()}>
{(tag) => (
<span class="bg-indigo-500 text-white px-2 py-1 rounded-md text-sm flex items-center gap-1">
{tag}
<button type="button" onClick={() => removeTag(tag)} class="font-bold">&times;</button>
</span>
)}
</For>
<input
type="text"
value={tagInput()}
onInput={(e) => setTagInput(e.currentTarget.value)}
onKeyDown={addTag}
placeholder="Add tags..."
class="border-none outline-none flex-1 min-w-[100px] p-1"
/>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-4">
<h4 class="text-lg font-medium text-gray-900">Course Resources</h4>
<button
type="button"
onClick={addResource}
class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<span>+</span> Add Resource
</button>
</div>
<div class="space-y-3">
<For each={resources()}>
{(resource, index) => (
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<div class="flex-1 space-y-2">
<input
type="text"
placeholder="Resource Title"
value={resource.title}
onInput={(e) => updateResource(index(), 'title', e.currentTarget.value)}
class="w-full p-2 border border-gray-200 rounded-md"
/>
<div class="flex gap-2">
<select
value={resource.type}
onChange={(e) => updateResource(index(), 'type', e.currentTarget.value)}
class="p-2 border border-gray-200 rounded-md"
>
<For each={resourceTypes}>
{(type) => <option value={type.value}>{type.label}</option>}
</For>
</select>
<input
type="url"
placeholder="URL"
value={resource.url}
onInput={(e) => updateResource(index(), 'url', e.currentTarget.value)}
class="flex-1 p-2 border border-gray-200 rounded-md"
/>
<input
type="number"
placeholder="Duration (min)"
value={resource.duration}
onInput={(e) => updateResource(index(), 'duration', parseInt(e.currentTarget.value) || 0)}
class="w-24 p-2 border border-gray-200 rounded-md"
/>
</div>
</div>
<button
type="button"
onClick={() => removeResource(index())}
class="px-3 py-2 border border-red-300 text-red-600 rounded-lg hover:bg-red-50"
>
&times;
</button>
</div>
)}
</For>
</div>
</div>
<div class="flex gap-3 justify-end">
<button
type="button"
onClick={closeModal}
class="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={saveCourse}
class="px-6 py-3 bg-indigo-500 text-white rounded-lg hover:bg-indigo-600 transition-colors"
>
Save Course
</button>
</div>
</div>
</div>
</div>
</Show>
</div>
);
};
@@ -0,0 +1,262 @@
import { createSignal, onMount, For, Show } from 'solid-js';
interface DashboardStats {
total_users: number;
total_courses: number;
total_instances: number;
active_courses: number;
total_progress: number;
}
interface Course {
id: number;
title: string;
category: string;
difficulty: string;
duration: number;
thumbnail: string;
created_at: string;
is_active: boolean;
}
interface Instance {
id: number;
name: string;
url: string;
version: string;
is_active: boolean;
created_at: string;
last_sync: string;
api_key: string;
}
export const Dashboard = () => {
const [stats, setStats] = createSignal<DashboardStats>({
total_users: 0,
total_courses: 0,
total_instances: 0,
active_courses: 0,
total_progress: 0
});
const [courses, setCourses] = createSignal<Course[]>([]);
const [instances, setInstances] = createSignal<Instance[]>([]);
const [loading, setLoading] = createSignal(true);
onMount(async () => {
await Promise.all([
loadStats(),
loadCourses(),
loadInstances()
]);
setLoading(false);
});
const loadStats = async () => {
try {
const response = await fetch('/api/v1/dashboard/stats');
const data = await response.json();
setStats(data);
} catch (error) {
console.error('Error loading stats:', error);
}
};
const loadCourses = async () => {
try {
const response = await fetch('/api/v1/dashboard/courses');
const data = await response.json();
setCourses(data.courses || []);
} catch (error) {
console.error('Error loading courses:', error);
}
};
const loadInstances = async () => {
try {
const response = await fetch('/api/v1/instances');
const data = await response.json();
setInstances(data.instances || []);
} catch (error) {
console.error('Error loading instances:', error);
}
};
const formatDate = (dateString: string) => {
if (!dateString) return 'Never';
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'bg-green-100 text-green-800';
case 'intermediate': return 'bg-orange-100 text-orange-800';
case 'advanced': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
return (
<div class="min-h-screen bg-gradient-to-br from-indigo-500 to-purple-600 p-6">
<div class="max-w-7xl mx-auto">
{/* Header */}
<header class="glass rounded-2xl p-6 mb-8 shadow-xl">
<div class="flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-white font-bold">
T
</div>
<h1 class="text-2xl font-bold text-gray-900">Trackeep Controller</h1>
</div>
<nav class="flex gap-2">
<a href="/dashboard" class="px-4 py-2 rounded-lg bg-indigo-500 text-white hover:bg-indigo-600 transition-colors">Dashboard</a>
<a href="/dashboard/courses" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Courses</a>
<a href="/dashboard/instances" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Instances</a>
<a href="/api/v1/user/me" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Profile</a>
</nav>
</div>
</header>
{/* Stats Grid */}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="glass rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300">
<div class="w-12 h-12 bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white text-xl mb-4">
👥
</div>
<div class="text-3xl font-bold text-gray-900 mb-2">{stats().total_users}</div>
<div class="text-gray-600 font-medium">Total Users</div>
</div>
<div class="glass rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300">
<div class="w-12 h-12 bg-gradient-to-r from-green-500 to-green-600 rounded-lg flex items-center justify-center text-white text-xl mb-4">
📚
</div>
<div class="text-3xl font-bold text-gray-900 mb-2">{stats().active_courses}</div>
<div class="text-gray-600 font-medium">Active Courses</div>
</div>
<div class="glass rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300">
<div class="w-12 h-12 bg-gradient-to-r from-purple-500 to-purple-600 rounded-lg flex items-center justify-center text-white text-xl mb-4">
🖥
</div>
<div class="text-3xl font-bold text-gray-900 mb-2">{stats().total_instances}</div>
<div class="text-gray-600 font-medium">Connected Instances</div>
</div>
<div class="glass rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300">
<div class="w-12 h-12 bg-gradient-to-r from-orange-500 to-orange-600 rounded-lg flex items-center justify-center text-white text-xl mb-4">
📈
</div>
<div class="text-3xl font-bold text-gray-900 mb-2">{stats().total_progress}</div>
<div class="text-gray-600 font-medium">Learning Progress</div>
</div>
</div>
{/* Main Content */}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Recent Courses */}
<div class="lg:col-span-2">
<div class="glass rounded-2xl p-6 shadow-xl">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-900">Recent Courses</h2>
<a href="/dashboard/courses" class="px-4 py-2 bg-indigo-500 text-white rounded-lg hover:bg-indigo-600 transition-colors">
Manage Courses
</a>
</div>
<Show when={loading()} fallback={
<Show when={courses().length > 0} fallback={
<div class="text-center py-12 text-gray-500">
<div class="text-5xl mb-4 opacity-50">📚</div>
<div class="text-lg font-semibold mb-2">No courses yet</div>
<p>Create your first course to get started!</p>
</div>
}>
<div class="space-y-4">
<For each={courses().slice(0, 5)}>
{(course) => (
<div class="flex items-center gap-4 p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="w-12 h-12 bg-gradient-to-r from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center text-white font-bold">
{course.title.charAt(0).toUpperCase()}
</div>
<div class="flex-1">
<div class="font-medium text-gray-900">{course.title}</div>
<div class="text-sm text-gray-600">{course.category} {course.difficulty} {course.duration}h</div>
</div>
<div class="flex gap-2">
<button
class="p-2 text-gray-600 hover:text-indigo-600 transition-colors"
onClick={() => window.open(`/api/v1/courses/${course.id}`, '_blank')}
title="View"
>
👁
</button>
<button
class="p-2 text-gray-600 hover:text-indigo-600 transition-colors"
onClick={() => window.location.href = `/dashboard/courses?edit=${course.id}`}
title="Edit"
>
</button>
</div>
</div>
)}
</For>
</div>
</Show>
}>
<div class="text-center py-8 text-gray-500">Loading courses...</div>
</Show>
</div>
</div>
{/* Active Instances */}
<div>
<div class="glass rounded-2xl p-6 shadow-xl">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-900">Active Instances</h2>
<a href="/dashboard/instances" class="text-indigo-600 hover:text-indigo-700 text-sm font-medium">
View All
</a>
</div>
<Show when={loading()} fallback={
<Show when={instances().length > 0} fallback={
<div class="text-center py-12 text-gray-500">
<div class="text-5xl mb-4 opacity-50">🖥</div>
<div class="text-lg font-semibold mb-2">No instances</div>
<p>Register your first instance to get started!</p>
</div>
}>
<div class="space-y-3">
<For each={instances().slice(0, 3)}>
{(instance) => (
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class={`w-2 h-2 rounded-full ${instance.is_active ? 'bg-green-500' : 'bg-red-500'}`}></div>
<div class="flex-1">
<div class="font-medium text-gray-900">{instance.name}</div>
<div class="text-sm text-gray-600">{instance.version}</div>
</div>
<button
class="p-2 text-gray-600 hover:text-indigo-600 transition-colors"
onClick={() => window.open(`/api/v1/instances/${instance.id}`, '_blank')}
title="View"
>
🔗
</button>
</div>
)}
</For>
</div>
</Show>
}>
<div class="text-center py-8 text-gray-500">Loading instances...</div>
</Show>
</div>
</div>
</div>
</div>
</div>
);
};
@@ -0,0 +1,388 @@
import { createSignal, onMount, For, Show } from 'solid-js';
interface Instance {
id: number;
name: string;
url: string;
api_key: string;
is_active: boolean;
version: string;
created_at: string;
last_sync: string;
admin_user_id: number;
}
export const InstanceManagement = () => {
const [instances, setInstances] = createSignal<Instance[]>([]);
const [loading, setLoading] = createSignal(true);
const [showModal, setShowModal] = createSignal(false);
const [editingInstance, setEditingInstance] = createSignal<Instance | null>(null);
// Form state
const [formData, setFormData] = createSignal({
name: '',
url: '',
version: ''
});
onMount(async () => {
await loadInstances();
setLoading(false);
});
const loadInstances = async () => {
try {
const response = await fetch('/api/v1/instances');
const data = await response.json();
setInstances(data.instances || []);
} catch (error) {
console.error('Error loading instances:', error);
}
};
const openCreateModal = () => {
setEditingInstance(null);
setFormData({
name: '',
url: '',
version: ''
});
setShowModal(true);
};
const openEditModal = (instance: Instance) => {
setEditingInstance(instance);
setFormData({
name: instance.name,
url: instance.url,
version: instance.version || ''
});
setShowModal(true);
};
const closeModal = () => {
setShowModal(false);
setEditingInstance(null);
};
const saveInstance = async () => {
try {
const url = editingInstance() ? `/api/v1/instances/${editingInstance()!.id}` : '/api/v1/instances';
const method = editingInstance() ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(formData())
});
if (response.ok) {
closeModal();
await loadInstances();
if (!editingInstance()) {
const result = await response.json();
if (result.api_key) {
alert(`🎉 Instance registered successfully!\n\nAPI Key: ${result.api_key}\n\nSave this key securely - it will not be shown again.`);
}
}
} else {
const error = await response.json();
alert('Error: ' + (error.error || 'Failed to save instance'));
}
} catch (error) {
console.error('Error saving instance:', error);
alert('Error: Failed to save instance');
}
};
const deleteInstance = async (instanceId: number) => {
if (!confirm('Are you sure you want to delete this instance? This action cannot be undone.')) return;
try {
const response = await fetch(`/api/v1/instances/${instanceId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
await loadInstances();
} else {
const error = await response.json();
alert('Error: ' + (error.error || 'Failed to delete instance'));
}
} catch (error) {
console.error('Error deleting instance:', error);
alert('Error: Failed to delete instance');
}
};
const testConnection = async (instance: Instance) => {
try {
const response = await fetch(`${instance.url}/health`, {
method: 'GET',
signal: AbortSignal.timeout(5000)
});
if (response.ok) {
alert('✅ Connection successful! Instance is responding.');
} else {
alert('❌ Connection failed. Instance returned an error.');
}
} catch (error) {
alert('❌ Connection failed. Unable to reach the instance.');
}
};
const copyApiKey = (apiKey: string, event: MouseEvent) => {
navigator.clipboard.writeText(apiKey).then(() => {
// Show feedback (you could implement a toast here)
const btn = event.target as HTMLButtonElement;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
(btn as HTMLButtonElement).style.background = '#10b981';
setTimeout(() => {
btn.textContent = originalText;
(btn as HTMLButtonElement).style.background = '';
}, 2000);
});
};
const formatDate = (dateString: string) => {
if (!dateString) return 'Never';
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
return (
<div class="min-h-screen bg-gradient-to-br from-indigo-500 to-purple-600 p-6">
<div class="max-w-7xl mx-auto">
{/* Header */}
<header class="glass rounded-2xl p-6 mb-8 shadow-xl">
<div class="flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-white font-bold">
T
</div>
<h1 class="text-2xl font-bold text-gray-900">Trackeep Controller</h1>
</div>
<nav class="flex gap-2">
<a href="/dashboard" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Dashboard</a>
<a href="/dashboard/courses" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Courses</a>
<a href="/dashboard/instances" class="px-4 py-2 rounded-lg bg-indigo-500 text-white hover:bg-indigo-600 transition-colors">Instances</a>
<a href="/api/v1/user/me" class="px-4 py-2 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors">Profile</a>
</nav>
</div>
</header>
{/* Main Content */}
<div class="glass rounded-2xl p-6 shadow-xl">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold text-gray-900">Instance Management</h2>
<button
onClick={openCreateModal}
class="px-6 py-3 bg-indigo-500 text-white rounded-lg hover:bg-indigo-600 transition-colors flex items-center gap-2"
>
<span>+</span> Register New Instance
</button>
</div>
<Show when={loading()} fallback={
<Show when={instances().length > 0} fallback={
<div class="text-center py-16 text-gray-500">
<div class="text-6xl mb-4 opacity-50">🖥</div>
<div class="text-xl font-semibold mb-2">No instances registered</div>
<p>Register your first Trackeep instance to get started!</p>
</div>
}>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={instances()}>
{(instance) => (
<div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden relative">
<div class={`absolute top-4 right-4 w-3 h-3 rounded-full ${instance.is_active ? 'bg-green-500' : 'bg-red-500'} ${instance.is_active ? 'animate-pulse' : ''}`}></div>
<div class="p-6">
<div class="flex justify-between items-start mb-4">
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 mb-1">{instance.name}</h3>
<a
href={instance.url}
target="_blank"
rel="noopener noreferrer"
class="text-indigo-600 hover:text-indigo-700 text-sm mb-2 block"
>
{instance.url}
</a>
<div class="flex items-center gap-2 text-sm text-gray-600">
<div class={`w-2 h-2 rounded-full ${instance.is_active ? 'bg-green-500' : 'bg-red-500'}`}></div>
<span>{instance.is_active ? 'Active' : 'Inactive'}</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-3 mb-4">
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide">Version</div>
<div class="text-sm font-medium text-gray-900">{instance.version || 'Unknown'}</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide">Created</div>
<div class="text-sm font-medium text-gray-900">{formatDate(instance.created_at)}</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide">Last Sync</div>
<div class="text-sm font-medium text-gray-900">{formatDate(instance.last_sync)}</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide">Instance ID</div>
<div class="text-sm font-medium text-gray-900">#{instance.id}</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-3 mb-4">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">API Key</div>
<div class="flex items-center gap-2">
<input
type="text"
readonly
value={instance.api_key}
class="flex-1 text-xs font-mono bg-transparent border-none outline-none text-gray-600"
/>
<button
onClick={(e: MouseEvent) => copyApiKey(instance.api_key, e)}
class="px-2 py-1 bg-indigo-500 text-white text-xs rounded hover:bg-indigo-600 transition-colors"
>
Copy
</button>
</div>
</div>
<div class="grid grid-cols-3 gap-2 pt-4 border-t border-gray-200">
<div class="text-center">
<div class="text-lg font-semibold text-indigo-600">{Math.floor(Math.random() * 100)}</div>
<div class="text-xs text-gray-500">Users</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-indigo-600">{Math.floor(Math.random() * 50)}</div>
<div class="text-xs text-gray-500">Courses</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-indigo-600">{Math.floor(Math.random() * 1000)}</div>
<div class="text-xs text-gray-500">API Calls</div>
</div>
</div>
<div class="flex gap-2 mt-4">
<button
class="flex-1 p-2 text-gray-600 hover:text-indigo-600 hover:bg-gray-50 rounded-lg transition-colors text-sm"
onClick={() => testConnection(instance)}
title="Test Connection"
>
🔗
</button>
<button
class="flex-1 p-2 text-gray-600 hover:text-indigo-600 hover:bg-gray-50 rounded-lg transition-colors text-sm"
onClick={() => openEditModal(instance)}
title="Edit"
>
</button>
<button
class="flex-1 p-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors text-sm"
onClick={() => deleteInstance(instance.id)}
title="Delete"
>
🗑
</button>
</div>
</div>
</div>
)}
</For>
</div>
</Show>
}>
<div class="text-center py-8 text-gray-500">Loading instances...</div>
</Show>
</div>
</div>
{/* Instance Modal */}
<Show when={showModal()}>
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-2xl p-8 max-w-md w-full">
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-semibold text-gray-900">
{editingInstance() ? 'Edit Instance' : 'Register New Instance'}
</h3>
<button
onClick={closeModal}
class="text-gray-500 hover:text-gray-700 text-2xl font-bold"
>
&times;
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Instance Name *</label>
<input
type="text"
value={formData().name}
onInput={(e) => setFormData({ ...formData(), name: e.currentTarget.value })}
placeholder="My Trackeep Instance"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Instance URL *</label>
<input
type="url"
value={formData().url}
onInput={(e) => setFormData({ ...formData(), url: e.currentTarget.value })}
placeholder="https://myapp.trackeep.com"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Version</label>
<input
type="text"
value={formData().version}
onInput={(e) => setFormData({ ...formData(), version: e.currentTarget.value })}
placeholder="1.0.0"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div>
<div class="flex gap-3 justify-end mt-6">
<button
type="button"
onClick={closeModal}
class="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={saveInstance}
class="px-6 py-3 bg-indigo-500 text-white rounded-lg hover:bg-indigo-600 transition-colors"
>
{editingInstance() ? 'Update Instance' : 'Register Instance'}
</button>
</div>
</div>
</div>
</Show>
</div>
);
};
+15
View File
@@ -0,0 +1,15 @@
import { render } from 'solid-js/web';
import { Router } from '@solidjs/router';
import App from './App';
const root = document.getElementById('root');
if (root) {
render(() => (
<Router>
<App />
</Router>
), root);
} else {
console.error('Root element not found');
}
+47
View File
@@ -0,0 +1,47 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles for Trackeep-inspired UI */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Glassmorphism effects */
.glass {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
/* Custom animations */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
+26
View File
@@ -0,0 +1,26 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#6366f1',
dark: '#4f46e5'
},
secondary: '#8b5cf6',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
dark: '#1f2937',
gray: '#6b7280',
light: '#f3f4f6',
white: '#ffffff'
}
},
},
plugins: [],
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES6"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js"
},
"include": ["src"],
"exclude": ["node_modules"]
}
+27
View File
@@ -0,0 +1,27 @@
import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';
export default defineConfig({
plugins: [solid()],
server: {
port: 5174,
proxy: {
'/api': {
target: 'http://localhost:9090',
changeOrigin: true,
},
'/auth': {
target: 'http://localhost:9090',
changeOrigin: true,
},
'/health': {
target: 'http://localhost:9090',
changeOrigin: true,
}
}
},
build: {
outDir: '../static',
emptyOutDir: true
}
});