first
This commit is contained in:
commit
529a5466f1
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"template": "node"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Supabase Configuration
|
||||||
|
VITE_SUPABASE_URL=your_supabase_url_here
|
||||||
|
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key_here
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
AUTH_USERNAME=admin
|
||||||
|
AUTH_PASSWORD=password123
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=3000
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
*Zone.Identifier
|
||||||
|
**/*Zone.Identifier
|
|
@ -0,0 +1,104 @@
|
||||||
|
# Log Microservice
|
||||||
|
|
||||||
|
A Node.js microservice that receives log data via POST requests with basic authentication and stores them in a PostgreSQL database via Supabase.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **HTTP API**: RESTful endpoints for receiving log data
|
||||||
|
- **Basic Authentication**: Secure endpoints with username/password authentication
|
||||||
|
- **PostgreSQL Storage**: Stores logs in Supabase PostgreSQL database
|
||||||
|
- **Input Validation**: Validates incoming data and provides meaningful error messages
|
||||||
|
- **Default Values**: Automatically applies default values for optional fields
|
||||||
|
- **Health Check**: Built-in health check endpoint
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### POST /logs
|
||||||
|
Receives log data and stores it in the database.
|
||||||
|
|
||||||
|
**Authentication**: Basic Auth required
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "This is a log message (required)",
|
||||||
|
"project": "My Project (optional, defaults to 'Project 1')",
|
||||||
|
"type": "Error (optional, defaults to 'Info')",
|
||||||
|
"owner": "john.doe (optional, defaults to 'N/A')",
|
||||||
|
"avatar_src": "/custom-avatar.png (optional, defaults to '/rectangle-15.png')",
|
||||||
|
"status": "Active (optional, defaults to 'Pending')"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Log entry created successfully",
|
||||||
|
"id": 1703123456
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /health
|
||||||
|
Health check endpoint to verify service status.
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Set up Supabase**: Click the "Connect to Supabase" button to configure your database
|
||||||
|
2. **Configure Environment**: Copy `.env.example` to `.env` and update the values
|
||||||
|
3. **Run the Service**: Use `npm start` to start the microservice
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
The service automatically creates a `logs` table with the following structure:
|
||||||
|
|
||||||
|
- `id` (bigint) - Epoch time identifier
|
||||||
|
- `body` (text) - First 200 characters of the message
|
||||||
|
- `project` (text) - Project name
|
||||||
|
- `type` (text) - Log type (Info, Error, Warning, etc.)
|
||||||
|
- `date` (date) - Date of the log entry
|
||||||
|
- `avatar_src` (text) - Avatar image source
|
||||||
|
- `owner` (text) - Owner of the log entry
|
||||||
|
- `description` (text) - Full log message
|
||||||
|
- `created_at` (timestamptz) - Creation timestamp
|
||||||
|
- `status` (text) - Status of the log entry
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
The service uses HTTP Basic Authentication. Default credentials:
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `password123`
|
||||||
|
|
||||||
|
Update these in your `.env` file for production use.
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using curl to send a log entry
|
||||||
|
curl -X POST http://localhost:3000/logs \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Basic YWRtaW46cGFzc3dvcmQxMjM=" \
|
||||||
|
-d '{
|
||||||
|
"message": "User login successful",
|
||||||
|
"project": "Authentication Service",
|
||||||
|
"type": "Info",
|
||||||
|
"owner": "auth-service"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The service provides detailed error responses:
|
||||||
|
|
||||||
|
- `400 Bad Request`: Invalid input data
|
||||||
|
- `401 Unauthorized`: Missing or invalid authentication
|
||||||
|
- `404 Not Found`: Unknown endpoint
|
||||||
|
- `500 Internal Server Error`: Database or server errors
|
|
@ -0,0 +1,3 @@
|
||||||
|
// run `node index.js` in the terminal
|
||||||
|
|
||||||
|
console.log(`Hello Node.js v${process.versions.node}!`);
|
|
@ -0,0 +1,38 @@
|
||||||
|
export function authenticateBasic(req) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Missing or invalid Authorization header'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64Credentials = authHeader.split(' ')[1];
|
||||||
|
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
|
||||||
|
const [username, password] = credentials.split(':');
|
||||||
|
|
||||||
|
// Simple hardcoded credentials for demo
|
||||||
|
// In production, these should be stored securely and hashed
|
||||||
|
const validUsername = process.env.AUTH_USERNAME || 'admin';
|
||||||
|
const validPassword = process.env.AUTH_PASSWORD || 'password123';
|
||||||
|
|
||||||
|
if (username === validUsername && password === validPassword) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
username
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid credentials'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid Authorization header format'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
{
|
||||||
|
"name": "log-microservice",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "log-microservice",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"pg": "^8.11.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg": {
|
||||||
|
"version": "8.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz",
|
||||||
|
"integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-connection-string": "^2.9.0",
|
||||||
|
"pg-pool": "^3.10.0",
|
||||||
|
"pg-protocol": "^1.10.0",
|
||||||
|
"pg-types": "2.2.0",
|
||||||
|
"pgpass": "1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"pg-cloudflare": "^1.2.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg-native": ">=3.0.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"pg-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-cloudflare": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/pg-connection-string": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ=="
|
||||||
|
},
|
||||||
|
"node_modules/pg-int8": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-pool": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-protocol": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q=="
|
||||||
|
},
|
||||||
|
"node_modules/pg-types": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-int8": "1.0.1",
|
||||||
|
"postgres-array": "~2.0.0",
|
||||||
|
"postgres-bytea": "~1.0.0",
|
||||||
|
"postgres-date": "~1.0.4",
|
||||||
|
"postgres-interval": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pgpass": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||||
|
"dependencies": {
|
||||||
|
"split2": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-bytea": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-date": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-interval": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "log-microservice",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Microservice for receiving and storing log data",
|
||||||
|
"main": "server.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node server.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pg": "^8.11.3"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import pkg from 'pg';
|
||||||
|
import { authenticateBasic } from './middleware/auth.js';
|
||||||
|
import { validateLogData } from './utils/validation.js';
|
||||||
|
import { createLogEntry } from './services/logService.js';
|
||||||
|
|
||||||
|
const { Client } = pkg;
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// PostgreSQL connection configuration from environment variables
|
||||||
|
const dbConfig = {
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: process.env.DB_PORT || 5432,
|
||||||
|
database: process.env.DB_NAME || 'postgres',
|
||||||
|
user: process.env.DB_USER || 'postgres',
|
||||||
|
password: process.env.DB_PASSWORD || 'password',
|
||||||
|
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize PostgreSQL client
|
||||||
|
const client = new Client(dbConfig);
|
||||||
|
|
||||||
|
// Connect to PostgreSQL
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log('Connected to PostgreSQL database');
|
||||||
|
|
||||||
|
// Create logs table if it doesn't exist
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
body text NOT NULL,
|
||||||
|
project text NOT NULL DEFAULT 'Project 1',
|
||||||
|
type text NOT NULL DEFAULT 'Info',
|
||||||
|
date date NOT NULL,
|
||||||
|
avatar_src text NOT NULL DEFAULT '/rectangle-15.png',
|
||||||
|
owner text NOT NULL DEFAULT 'N/A',
|
||||||
|
description text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
status text NOT NULL DEFAULT 'Pending'
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
console.log('Logs table ready');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to PostgreSQL:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
// Set CORS headers
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Health check endpoint
|
||||||
|
if (req.method === 'GET' && url.pathname === '/health') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ status: 'healthy', timestamp: new Date().toISOString() }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log entry endpoint
|
||||||
|
if (req.method === 'POST' && url.pathname === '/logs') {
|
||||||
|
// Authenticate request
|
||||||
|
const authResult = authenticateBasic(req);
|
||||||
|
if (!authResult.success) {
|
||||||
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Unauthorized', message: authResult.message }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
let body = '';
|
||||||
|
req.on('data', chunk => {
|
||||||
|
body += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(body);
|
||||||
|
|
||||||
|
// Validate input data
|
||||||
|
const validation = validateLogData(data);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Invalid input', errors: validation.errors }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create log entry
|
||||||
|
const logEntry = await createLogEntry(client, data);
|
||||||
|
|
||||||
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Log entry created successfully',
|
||||||
|
id: logEntry.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing request:', error);
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 404 for unknown routes
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Not found' }));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Server error:', error);
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Log microservice running on port ${PORT}`);
|
||||||
|
console.log(`Health check available at: http://localhost:${PORT}/health`);
|
||||||
|
console.log(`Log endpoint available at: http://localhost:${PORT}/logs`);
|
||||||
|
});
|
|
@ -0,0 +1,55 @@
|
||||||
|
export async function createLogEntry(client, data) {
|
||||||
|
const now = new Date();
|
||||||
|
const epochTime = Math.floor(now.getTime() / 1000);
|
||||||
|
|
||||||
|
// Create log entry with defaults
|
||||||
|
const logEntry = {
|
||||||
|
id: epochTime,
|
||||||
|
body: data.message.substring(0, 200), // First 200 characters
|
||||||
|
project: data.project || 'Project 1',
|
||||||
|
type: data.type || 'Info',
|
||||||
|
date: now.toISOString().split('T')[0], // YYYY-MM-DD format
|
||||||
|
avatar_src: data.avatar_src || '/rectangle-15.png',
|
||||||
|
owner: data.owner || 'N/A',
|
||||||
|
description: data.message, // Full message
|
||||||
|
created_at: now.toISOString(),
|
||||||
|
status: data.status || 'Pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert into database
|
||||||
|
const query = `
|
||||||
|
INSERT INTO logs (id, body, project, type, date, avatar_src, owner, description, created_at, status)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
logEntry.id,
|
||||||
|
logEntry.body,
|
||||||
|
logEntry.project,
|
||||||
|
logEntry.type,
|
||||||
|
logEntry.date,
|
||||||
|
logEntry.avatar_src,
|
||||||
|
logEntry.owner,
|
||||||
|
logEntry.description,
|
||||||
|
logEntry.created_at,
|
||||||
|
logEntry.status
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client.query(query, values);
|
||||||
|
const insertedData = result.rows[0];
|
||||||
|
|
||||||
|
console.log('Log entry created:', {
|
||||||
|
id: insertedData.id,
|
||||||
|
project: insertedData.project,
|
||||||
|
type: insertedData.type,
|
||||||
|
timestamp: insertedData.created_at
|
||||||
|
});
|
||||||
|
|
||||||
|
return insertedData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database error:', error);
|
||||||
|
throw new Error(`Failed to insert log entry: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
# Create logs table for microservice
|
||||||
|
|
||||||
|
1. New Tables
|
||||||
|
- `logs`
|
||||||
|
- `id` (bigint, primary key) - Epoch time identifier
|
||||||
|
- `body` (text) - First 200 characters of log message
|
||||||
|
- `project` (text) - Project name, defaults to "Project 1"
|
||||||
|
- `type` (text) - Log type, defaults to "Info"
|
||||||
|
- `date` (date) - Date of the log entry
|
||||||
|
- `avatar_src` (text) - Avatar source URL, defaults to "/rectangle-15.png"
|
||||||
|
- `owner` (text) - Owner of the log entry, defaults to "N/A"
|
||||||
|
- `description` (text) - Full log message
|
||||||
|
- `created_at` (timestamptz) - Timestamp when record was created
|
||||||
|
- `status` (text) - Status of the log entry, defaults to "Pending"
|
||||||
|
|
||||||
|
2. Security
|
||||||
|
- Enable RLS on `logs` table
|
||||||
|
- Add policy for authenticated users to insert and read logs
|
||||||
|
*/
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
body text NOT NULL,
|
||||||
|
project text NOT NULL DEFAULT 'Project 1',
|
||||||
|
type text NOT NULL DEFAULT 'Info',
|
||||||
|
date date NOT NULL,
|
||||||
|
avatar_src text NOT NULL DEFAULT '/rectangle-15.png',
|
||||||
|
owner text NOT NULL DEFAULT 'N/A',
|
||||||
|
description text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
status text NOT NULL DEFAULT 'Pending'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE logs ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policy to allow authenticated users to insert logs
|
||||||
|
CREATE POLICY "Allow authenticated users to insert logs"
|
||||||
|
ON logs
|
||||||
|
FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (true);
|
||||||
|
|
||||||
|
-- Policy to allow authenticated users to read logs
|
||||||
|
CREATE POLICY "Allow authenticated users to read logs"
|
||||||
|
ON logs
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
-- Policy to allow service role to perform all operations
|
||||||
|
CREATE POLICY "Allow service role full access"
|
||||||
|
ON logs
|
||||||
|
FOR ALL
|
||||||
|
TO service_role
|
||||||
|
USING (true);
|
|
@ -0,0 +1,38 @@
|
||||||
|
export function validateLogData(data) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
// Check if data exists
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
errors.push('Request body must be a valid JSON object');
|
||||||
|
return { isValid: false, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required message field
|
||||||
|
if (!data.message || typeof data.message !== 'string') {
|
||||||
|
errors.push('message field is required and must be a string');
|
||||||
|
} else if (data.message.trim().length === 0) {
|
||||||
|
errors.push('message field cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate optional fields if provided
|
||||||
|
if (data.project !== undefined && typeof data.project !== 'string') {
|
||||||
|
errors.push('project field must be a string if provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type !== undefined && typeof data.type !== 'string') {
|
||||||
|
errors.push('type field must be a string if provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.owner !== undefined && typeof data.owner !== 'string') {
|
||||||
|
errors.push('owner field must be a string if provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.avatar_src !== undefined && typeof data.avatar_src !== 'string') {
|
||||||
|
errors.push('avatar_src field must be a string if provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue