Merge branch 'dev' into feature/session-persistence-new

This commit is contained in:
2026-03-17 18:40:22 +05:30
257 changed files with 32250 additions and 1272 deletions

View File

@@ -0,0 +1,343 @@
---
name: api-authentication
description: Implement secure API authentication with JWT, OAuth 2.0, API keys, and session management. Use when securing APIs, managing tokens, or implementing user authentication flows.
---
# API Authentication
## Overview
Implement comprehensive authentication strategies for APIs including JWT tokens, OAuth 2.0, API keys, and session management with proper security practices.
## When to Use
- Securing API endpoints
- Implementing user login/logout flows
- Managing access tokens and refresh tokens
- Integrating OAuth 2.0 providers
- Protecting sensitive data
- Implementing API key authentication
## Instructions
### 1. **JWT Authentication**
```javascript
// Node.js JWT Implementation
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key';
const REFRESH_SECRET = process.env.REFRESH_SECRET || 'your-refresh-secret';
// User login endpoint
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user in database
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate tokens
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
SECRET_KEY,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token in database
await RefreshToken.create({ token: refreshToken, userId: user.id });
res.json({
accessToken,
refreshToken,
expiresIn: 900,
user: { id: user.id, email: user.email, role: user.role }
});
} catch (error) {
res.status(500).json({ error: 'Authentication failed' });
}
});
// Refresh token endpoint
app.post('/api/auth/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// Verify token exists in database
const storedToken = await RefreshToken.findOne({
token: refreshToken,
userId: decoded.userId
});
if (!storedToken) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Generate new access token
const newAccessToken = jwt.sign(
{ userId: decoded.userId },
SECRET_KEY,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken, expiresIn: 900 });
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// Middleware to verify JWT
const verifyToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer token
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
res.status(403).json({ error: 'Invalid token' });
}
};
// Protected endpoint
app.get('/api/profile', verifyToken, (req, res) => {
res.json({ user: req.user });
});
// Logout endpoint
app.post('/api/auth/logout', verifyToken, async (req, res) => {
try {
await RefreshToken.deleteOne({ userId: req.user.userId });
res.json({ message: 'Logged out successfully' });
} catch (error) {
res.status(500).json({ error: 'Logout failed' });
}
});
```
### 2. **OAuth 2.0 Implementation**
```javascript
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/api/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
firstName: profile.name.givenName,
lastName: profile.name.familyName
});
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// OAuth routes
app.get('/api/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/api/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
const token = jwt.sign(
{ userId: req.user.id, email: req.user.email },
SECRET_KEY,
{ expiresIn: '7d' }
);
res.redirect(`/dashboard?token=${token}`);
}
);
```
### 3. **API Key Authentication**
```javascript
// API Key middleware
const verifyApiKey = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
try {
// Verify API key format and existence
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
const apiKeyRecord = await ApiKey.findOne({ key_hash: keyHash, active: true });
if (!apiKeyRecord) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.apiKey = apiKeyRecord;
next();
} catch (error) {
res.status(500).json({ error: 'Authentication failed' });
}
};
// Generate API key endpoint
app.post('/api/apikeys/generate', verifyToken, async (req, res) => {
try {
const apiKey = crypto.randomBytes(32).toString('hex');
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
const record = await ApiKey.create({
userId: req.user.userId,
key_hash: keyHash,
name: req.body.name,
active: true
});
res.json({ apiKey, message: 'Save this key securely' });
} catch (error) {
res.status(500).json({ error: 'Failed to generate API key' });
}
});
// Protected endpoint with API key
app.get('/api/data', verifyApiKey, (req, res) => {
res.json({ data: 'sensitive data for API key holder' });
});
```
### 4. **Python Authentication Implementation**
```python
from flask import Flask, request, jsonify
from flask_jwt_extended import JWTManager, create_access_token, jwt_required
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'secret-key'
jwt = JWTManager(app)
@app.route('/api/auth/login', methods=['POST'])
def login():
data = request.get_json()
user = User.query.filter_by(email=data['email']).first()
if not user or not check_password_hash(user.password, data['password']):
return jsonify({'error': 'Invalid credentials'}), 401
access_token = create_access_token(
identity=user.id,
additional_claims={'email': user.email, 'role': user.role}
)
return jsonify({
'accessToken': access_token,
'user': {'id': user.id, 'email': user.email}
}), 200
@app.route('/api/protected', methods=['GET'])
@jwt_required()
def protected():
from flask_jwt_extended import get_jwt_identity
user_id = get_jwt_identity()
return jsonify({'userId': user_id}), 200
def require_role(role):
def decorator(fn):
@wraps(fn)
@jwt_required()
def wrapper(*args, **kwargs):
from flask_jwt_extended import get_jwt
claims = get_jwt()
if claims.get('role') != role:
return jsonify({'error': 'Forbidden'}), 403
return fn(*args, **kwargs)
return wrapper
return decorator
@app.route('/api/admin', methods=['GET'])
@require_role('admin')
def admin_endpoint():
return jsonify({'message': 'Admin data'}), 200
```
## Best Practices
### ✅ DO
- Use HTTPS for all authentication
- Store tokens securely (HttpOnly cookies)
- Implement token refresh mechanism
- Set appropriate token expiration times
- Hash and salt passwords
- Use strong secret keys
- Validate tokens on every request
- Implement rate limiting on auth endpoints
- Log authentication attempts
- Rotate secrets regularly
### ❌ DON'T
- Store passwords in plain text
- Send tokens in URL parameters
- Use weak secret keys
- Store sensitive data in JWT payload
- Ignore token expiration
- Disable HTTPS in production
- Log sensitive tokens
- Reuse API keys across services
- Store credentials in code
## Security Headers
```javascript
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
```

View File

@@ -0,0 +1,624 @@
---
name: api-contract-testing
description: Verify API contracts between services to ensure compatibility and prevent breaking changes. Use for contract testing, Pact, API contract validation, schema validation, and consumer-driven contracts.
---
# API Contract Testing
## Overview
Contract testing verifies that APIs honor their contracts between consumers and providers. It ensures that service changes don't break dependent consumers without requiring full integration tests. Contract tests validate request/response formats, data types, and API behavior independently.
## When to Use
- Testing microservices communication
- Preventing breaking API changes
- Validating API versioning
- Testing consumer-provider contracts
- Ensuring backward compatibility
- Validating OpenAPI/Swagger specifications
- Testing third-party API integrations
- Catching contract violations in CI
## Key Concepts
- **Consumer**: Service that calls an API
- **Provider**: Service that exposes the API
- **Contract**: Agreement on API request/response format
- **Pact**: Consumer-defined expectations
- **Schema**: Structure definition (OpenAPI, JSON Schema)
- **Stub**: Generated mock from contract
- **Broker**: Central repository for contracts
## Instructions
### 1. **Pact for Consumer-Driven Contracts**
#### Consumer Test (Jest/Pact)
```typescript
// tests/pact/user-service.pact.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { UserService } from '../../src/services/UserService';
const { like, eachLike, iso8601DateTimeWithMillis } = MatchersV3;
const provider = new PactV3({
consumer: 'OrderService',
provider: 'UserService',
port: 1234,
dir: './pacts',
});
describe('User Service Contract', () => {
const userService = new UserService('http://localhost:1234');
describe('GET /users/:id', () => {
test('returns user when found', async () => {
await provider
.given('user with ID 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123',
headers: {
Authorization: like('Bearer token'),
},
})
.willRespondWith({
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: {
id: like('123'),
email: like('user@example.com'),
name: like('John Doe'),
age: like(30),
createdAt: iso8601DateTimeWithMillis('2024-01-01T00:00:00.000Z'),
role: like('user'),
},
})
.executeTest(async (mockServer) => {
const user = await userService.getUser('123');
expect(user.id).toBe('123');
expect(user.email).toBeDefined();
expect(user.name).toBeDefined();
});
});
test('returns 404 when user not found', async () => {
await provider
.given('user with ID 999 does not exist')
.uponReceiving('a request for non-existent user')
.withRequest({
method: 'GET',
path: '/users/999',
})
.willRespondWith({
status: 404,
headers: {
'Content-Type': 'application/json',
},
body: {
error: like('User not found'),
code: like('USER_NOT_FOUND'),
},
})
.executeTest(async (mockServer) => {
await expect(userService.getUser('999')).rejects.toThrow(
'User not found'
);
});
});
});
describe('POST /users', () => {
test('creates new user', async () => {
await provider
.given('user does not exist')
.uponReceiving('a request to create user')
.withRequest({
method: 'POST',
path: '/users',
headers: {
'Content-Type': 'application/json',
},
body: {
email: like('newuser@example.com'),
name: like('New User'),
age: like(25),
},
})
.willRespondWith({
status: 201,
headers: {
'Content-Type': 'application/json',
},
body: {
id: like('new-123'),
email: like('newuser@example.com'),
name: like('New User'),
age: like(25),
createdAt: iso8601DateTimeWithMillis(),
role: 'user',
},
})
.executeTest(async (mockServer) => {
const user = await userService.createUser({
email: 'newuser@example.com',
name: 'New User',
age: 25,
});
expect(user.id).toBeDefined();
expect(user.email).toBe('newuser@example.com');
});
});
});
describe('GET /users/:id/orders', () => {
test('returns user orders', async () => {
await provider
.given('user 123 has orders')
.uponReceiving('a request for user orders')
.withRequest({
method: 'GET',
path: '/users/123/orders',
query: {
limit: '10',
offset: '0',
},
})
.willRespondWith({
status: 200,
body: {
orders: eachLike({
id: like('order-1'),
total: like(99.99),
status: like('completed'),
createdAt: iso8601DateTimeWithMillis(),
}),
total: like(5),
hasMore: like(false),
},
})
.executeTest(async (mockServer) => {
const response = await userService.getUserOrders('123', {
limit: 10,
offset: 0,
});
expect(response.orders).toBeDefined();
expect(Array.isArray(response.orders)).toBe(true);
expect(response.total).toBeDefined();
});
});
});
});
```
#### Provider Test (Verify Contract)
```typescript
// tests/pact/user-service.provider.test.ts
import { Verifier } from '@pact-foundation/pact';
import path from 'path';
import { app } from '../../src/app';
import { setupTestDB, teardownTestDB } from '../helpers/db';
describe('Pact Provider Verification', () => {
let server;
beforeAll(async () => {
await setupTestDB();
server = app.listen(3001);
});
afterAll(async () => {
await teardownTestDB();
server.close();
});
test('validates the expectations of OrderService', () => {
return new Verifier({
provider: 'UserService',
providerBaseUrl: 'http://localhost:3001',
pactUrls: [
path.resolve(__dirname, '../../pacts/orderservice-userservice.json'),
],
// Provider state setup
stateHandlers: {
'user with ID 123 exists': async () => {
await createTestUser({ id: '123', name: 'John Doe' });
},
'user with ID 999 does not exist': async () => {
await deleteUser('999');
},
'user 123 has orders': async () => {
await createTestUser({ id: '123' });
await createTestOrder({ userId: '123' });
},
},
})
.verifyProvider()
.then((output) => {
console.log('Pact Verification Complete!');
});
});
});
```
### 2. **OpenAPI Schema Validation**
```typescript
// tests/contract/openapi.test.ts
import request from 'supertest';
import { app } from '../../src/app';
import OpenAPIValidator from 'express-openapi-validator';
import fs from 'fs';
import yaml from 'js-yaml';
describe('OpenAPI Contract Validation', () => {
let validator;
beforeAll(() => {
const spec = yaml.load(
fs.readFileSync('./openapi.yaml', 'utf8')
);
validator = OpenAPIValidator.middleware({
apiSpec: spec,
validateRequests: true,
validateResponses: true,
});
});
test('GET /users/:id matches schema', async () => {
const response = await request(app)
.get('/users/123')
.expect(200);
// Validate against OpenAPI schema
expect(response.body).toMatchObject({
id: expect.any(String),
email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/),
name: expect.any(String),
age: expect.any(Number),
createdAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
});
});
test('POST /users validates request body', async () => {
const invalidUser = {
email: 'invalid-email', // Should fail validation
name: 'Test',
};
await request(app)
.post('/users')
.send(invalidUser)
.expect(400);
});
});
```
### 3. **JSON Schema Validation**
```python
# tests/contract/test_schema_validation.py
import pytest
import jsonschema
from jsonschema import validate
import json
# Define schemas
USER_SCHEMA = {
"type": "object",
"required": ["id", "email", "name"],
"properties": {
"id": {"type": "string"},
"email": {"type": "string", "format": "email"},
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"role": {"type": "string", "enum": ["user", "admin"]},
"createdAt": {"type": "string", "format": "date-time"},
},
"additionalProperties": False
}
ORDER_SCHEMA = {
"type": "object",
"required": ["id", "userId", "total", "status"],
"properties": {
"id": {"type": "string"},
"userId": {"type": "string"},
"total": {"type": "number", "minimum": 0},
"status": {
"type": "string",
"enum": ["pending", "paid", "shipped", "delivered", "cancelled"]
},
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["productId", "quantity", "price"],
"properties": {
"productId": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"price": {"type": "number", "minimum": 0},
}
}
}
}
}
class TestAPIContracts:
def test_get_user_response_schema(self, api_client):
"""Validate user endpoint response against schema."""
response = api_client.get('/api/users/123')
assert response.status_code == 200
data = response.json()
# Validate against schema
validate(instance=data, schema=USER_SCHEMA)
def test_create_user_request_schema(self, api_client):
"""Validate create user request body."""
valid_user = {
"email": "test@example.com",
"name": "Test User",
"age": 30,
}
response = api_client.post('/api/users', json=valid_user)
assert response.status_code == 201
# Response should also match schema
validate(instance=response.json(), schema=USER_SCHEMA)
def test_invalid_request_rejected(self, api_client):
"""Invalid requests should be rejected."""
invalid_user = {
"email": "not-an-email",
"age": -5, # Invalid age
}
response = api_client.post('/api/users', json=invalid_user)
assert response.status_code == 400
def test_order_response_schema(self, api_client):
"""Validate order endpoint response."""
response = api_client.get('/api/orders/order-123')
assert response.status_code == 200
validate(instance=response.json(), schema=ORDER_SCHEMA)
def test_order_items_array_validation(self, api_client):
"""Validate nested array schema."""
order_data = {
"userId": "user-123",
"items": [
{"productId": "prod-1", "quantity": 2, "price": 29.99},
{"productId": "prod-2", "quantity": 1, "price": 49.99},
]
}
response = api_client.post('/api/orders', json=order_data)
assert response.status_code == 201
result = response.json()
validate(instance=result, schema=ORDER_SCHEMA)
```
### 4. **REST Assured for Java**
```java
// ContractTest.java
import io.restassured.RestAssured;
import io.restassured.module.jsv.JsonSchemaValidator;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class UserAPIContractTest {
@Test
public void getUserShouldMatchSchema() {
given()
.pathParam("id", "123")
.when()
.get("/api/users/{id}")
.then()
.statusCode(200)
.body(JsonSchemaValidator.matchesJsonSchemaInClasspath("schemas/user-schema.json"))
.body("id", notNullValue())
.body("email", matchesPattern("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"))
.body("age", greaterThanOrEqualTo(0));
}
@Test
public void createUserShouldValidateRequest() {
String userJson = """
{
"email": "test@example.com",
"name": "Test User",
"age": 30
}
""";
given()
.contentType("application/json")
.body(userJson)
.when()
.post("/api/users")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("email", equalTo("test@example.com"))
.body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T.*"));
}
@Test
public void getUserOrdersShouldReturnArray() {
given()
.pathParam("id", "123")
.queryParam("limit", 10)
.when()
.get("/api/users/{id}/orders")
.then()
.statusCode(200)
.body("orders", isA(java.util.List.class))
.body("orders[0].id", notNullValue())
.body("orders[0].status", isIn(Arrays.asList(
"pending", "paid", "shipped", "delivered", "cancelled"
)))
.body("total", greaterThanOrEqualTo(0));
}
@Test
public void invalidRequestShouldReturn400() {
String invalidUser = """
{
"email": "not-an-email",
"age": -5
}
""";
given()
.contentType("application/json")
.body(invalidUser)
.when()
.post("/api/users")
.then()
.statusCode(400)
.body("error", notNullValue());
}
}
```
### 5. **Contract Testing with Postman**
```json
// postman-collection.json
{
"info": {
"name": "User API Contract Tests"
},
"item": [
{
"name": "Get User",
"request": {
"method": "GET",
"url": "{{baseUrl}}/users/{{userId}}"
},
"test": "
pm.test('Response status is 200', () => {
pm.response.to.have.status(200);
});
pm.test('Response matches schema', () => {
const schema = {
type: 'object',
required: ['id', 'email', 'name'],
properties: {
id: { type: 'string' },
email: { type: 'string', format: 'email' },
name: { type: 'string' },
age: { type: 'integer' }
}
};
pm.response.to.have.jsonSchema(schema);
});
pm.test('Email format is valid', () => {
const data = pm.response.json();
pm.expect(data.email).to.match(/^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$/);
});
"
}
]
}
```
### 6. **Pact Broker Integration**
```yaml
# .github/workflows/contract-tests.yml
name: Contract Tests
on: [push, pull_request]
jobs:
consumer-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run test:pact
- name: Publish Pacts
run: |
npx pact-broker publish ./pacts \
--consumer-app-version=${{ github.sha }} \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
provider-tests:
runs-on: ubuntu-latest
needs: consumer-tests
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run test:pact:provider
- name: Can I Deploy?
run: |
npx pact-broker can-i-deploy \
--pacticipant=UserService \
--version=${{ github.sha }} \
--to-environment=production \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
```
## Best Practices
### ✅ DO
- Test contracts from consumer perspective
- Use matchers for flexible matching
- Validate schema structure, not specific values
- Version your contracts
- Test error responses
- Use Pact broker for contract sharing
- Run contract tests in CI
- Test backward compatibility
### ❌ DON'T
- Test business logic in contract tests
- Hard-code specific values in contracts
- Skip error scenarios
- Test UI in contract tests
- Ignore contract versioning
- Deploy without contract verification
- Test implementation details
- Mock contract tests
## Tools
- **Pact**: Consumer-driven contracts (multiple languages)
- **Spring Cloud Contract**: JVM contract testing
- **OpenAPI/Swagger**: API specification and validation
- **Postman**: API contract testing
- **REST Assured**: Java API testing
- **Dredd**: OpenAPI/API Blueprint testing
- **Spectral**: OpenAPI linting
## Examples
See also: integration-testing, api-versioning-strategy, continuous-testing for comprehensive API testing strategies.

View File

@@ -0,0 +1,659 @@
---
name: api-security-hardening
description: Secure REST APIs with authentication, rate limiting, CORS, input validation, and security middleware. Use when building or hardening API endpoints against common attacks.
---
# API Security Hardening
## Overview
Implement comprehensive API security measures including authentication, authorization, rate limiting, input validation, and attack prevention to protect against common vulnerabilities.
## When to Use
- New API development
- Security audit remediation
- Production API hardening
- Compliance requirements
- High-traffic API protection
- Public API exposure
## Implementation Examples
### 1. **Node.js/Express API Security**
```javascript
// secure-api.js - Comprehensive API security
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const hpp = require('hpp');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const validator = require('validator');
class SecureAPIServer {
constructor() {
this.app = express();
this.setupSecurityMiddleware();
this.setupRoutes();
}
setupSecurityMiddleware() {
// 1. Helmet - Set security headers
this.app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// 2. CORS configuration
const corsOptions = {
origin: (origin, callback) => {
const whitelist = [
'https://example.com',
'https://app.example.com'
];
if (!origin || whitelist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
};
this.app.use(cors(corsOptions));
// 3. Rate limiting
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'rate_limit_exceeded',
message: 'Too many requests, please try again later',
retryAfter: req.rateLimit.resetTime
});
}
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Stricter limit for auth endpoints
skipSuccessfulRequests: true
});
this.app.use('/api/', generalLimiter);
this.app.use('/api/auth/', authLimiter);
// 4. Body parsing with size limits
this.app.use(express.json({ limit: '10kb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// 5. NoSQL injection prevention
this.app.use(mongoSanitize());
// 6. XSS protection
this.app.use(xss());
// 7. HTTP Parameter Pollution prevention
this.app.use(hpp());
// 8. Request ID for tracking
this.app.use((req, res, next) => {
req.id = require('crypto').randomUUID();
res.setHeader('X-Request-ID', req.id);
next();
});
// 9. Security logging
this.app.use(this.securityLogger());
}
securityLogger() {
return (req, res, next) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
const logEntry = {
timestamp: new Date().toISOString(),
requestId: req.id,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration,
ip: req.ip,
userAgent: req.get('user-agent')
};
// Log suspicious activity
if (res.statusCode === 401 || res.statusCode === 403) {
console.warn('Security event:', logEntry);
}
if (res.statusCode >= 500) {
console.error('Server error:', logEntry);
}
});
next();
};
}
// JWT authentication middleware
authenticateJWT() {
return (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'unauthorized',
message: 'Missing or invalid authorization header'
});
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'api.example.com',
audience: 'api.example.com'
});
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'token_expired',
message: 'Token has expired'
});
}
return res.status(401).json({
error: 'invalid_token',
message: 'Invalid token'
});
}
};
}
// Input validation middleware
validateInput(schema) {
return (req, res, next) => {
const errors = [];
// Validate request body
if (schema.body) {
for (const [field, rules] of Object.entries(schema.body)) {
const value = req.body[field];
if (rules.required && !value) {
errors.push(`${field} is required`);
continue;
}
if (value) {
// Type validation
if (rules.type === 'email' && !validator.isEmail(value)) {
errors.push(`${field} must be a valid email`);
}
if (rules.type === 'uuid' && !validator.isUUID(value)) {
errors.push(`${field} must be a valid UUID`);
}
if (rules.type === 'url' && !validator.isURL(value)) {
errors.push(`${field} must be a valid URL`);
}
// Length validation
if (rules.minLength && value.length < rules.minLength) {
errors.push(`${field} must be at least ${rules.minLength} characters`);
}
if (rules.maxLength && value.length > rules.maxLength) {
errors.push(`${field} must be at most ${rules.maxLength} characters`);
}
// Pattern validation
if (rules.pattern && !rules.pattern.test(value)) {
errors.push(`${field} format is invalid`);
}
}
}
}
if (errors.length > 0) {
return res.status(400).json({
error: 'validation_error',
message: 'Input validation failed',
details: errors
});
}
next();
};
}
// Authorization middleware
authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'unauthorized',
message: 'Authentication required'
});
}
if (roles.length > 0 && !roles.includes(req.user.role)) {
return res.status(403).json({
error: 'forbidden',
message: 'Insufficient permissions'
});
}
next();
};
}
setupRoutes() {
// Public endpoint
this.app.get('/api/health', (req, res) => {
res.json({ status: 'healthy' });
});
// Protected endpoint with validation
this.app.post('/api/users',
this.authenticateJWT(),
this.authorize('admin'),
this.validateInput({
body: {
email: { required: true, type: 'email' },
name: { required: true, minLength: 2, maxLength: 100 },
password: { required: true, minLength: 8 }
}
}),
async (req, res) => {
try {
// Sanitized and validated input
const { email, name, password } = req.body;
// Process request
res.status(201).json({
message: 'User created successfully',
userId: '123'
});
} catch (error) {
res.status(500).json({
error: 'internal_error',
message: 'An error occurred'
});
}
}
);
// Error handling middleware
this.app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
error: 'internal_error',
message: 'An unexpected error occurred',
requestId: req.id
});
});
}
start(port = 3000) {
this.app.listen(port, () => {
console.log(`Secure API server running on port ${port}`);
});
}
}
// Usage
const server = new SecureAPIServer();
server.start(3000);
```
### 2. **Python FastAPI Security**
```python
# secure_api.py
from fastapi import FastAPI, HTTPException, Depends, Security, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from pydantic import BaseModel, EmailStr, validator, Field
import jwt
from datetime import datetime, timedelta
import re
from typing import Optional, List
import secrets
app = FastAPI()
security = HTTPBearer()
limiter = Limiter(key_func=get_remote_address)
# Rate limiting
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://example.com",
"https://app.example.com"
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
max_age=3600
)
# Trusted hosts
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["example.com", "*.example.com"]
)
# Security headers middleware
@app.middleware("http")
async def add_security_headers(request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Content-Security-Policy"] = "default-src 'self'"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
return response
# Input validation models
class CreateUserRequest(BaseModel):
email: EmailStr
name: str = Field(..., min_length=2, max_length=100)
password: str = Field(..., min_length=8)
@validator('password')
def validate_password(cls, v):
if not re.search(r'[A-Z]', v):
raise ValueError('Password must contain uppercase letter')
if not re.search(r'[a-z]', v):
raise ValueError('Password must contain lowercase letter')
if not re.search(r'\d', v):
raise ValueError('Password must contain digit')
if not re.search(r'[!@#$%^&*]', v):
raise ValueError('Password must contain special character')
return v
@validator('name')
def validate_name(cls, v):
# Prevent XSS in name field
if re.search(r'[<>]', v):
raise ValueError('Name contains invalid characters')
return v
class APIKeyRequest(BaseModel):
name: str = Field(..., max_length=100)
expires_in_days: int = Field(30, ge=1, le=365)
# JWT token verification
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
try:
token = credentials.credentials
payload = jwt.decode(
token,
"your-secret-key",
algorithms=["HS256"],
audience="api.example.com",
issuer="api.example.com"
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Role-based authorization
def require_role(required_roles: List[str]):
def role_checker(token_payload: dict = Depends(verify_token)):
user_role = token_payload.get('role')
if user_role not in required_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
return token_payload
return role_checker
# API key authentication
def verify_api_key(api_key: str):
# Constant-time comparison to prevent timing attacks
if not secrets.compare_digest(api_key, "expected-api-key"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API key"
)
return True
# Endpoints
@app.get("/api/health")
@limiter.limit("100/minute")
async def health_check():
return {"status": "healthy"}
@app.post("/api/users")
@limiter.limit("10/minute")
async def create_user(
user: CreateUserRequest,
token_payload: dict = Depends(require_role(["admin"]))
):
"""Create new user (admin only)"""
# Hash password before storing
# hashed_password = bcrypt.hashpw(user.password.encode(), bcrypt.gensalt())
return {
"message": "User created successfully",
"user_id": "123"
}
@app.post("/api/keys")
@limiter.limit("5/hour")
async def create_api_key(
request: APIKeyRequest,
token_payload: dict = Depends(verify_token)
):
"""Generate API key"""
# Generate secure random API key
api_key = secrets.token_urlsafe(32)
expires_at = datetime.now() + timedelta(days=request.expires_in_days)
return {
"api_key": api_key,
"expires_at": expires_at.isoformat(),
"name": request.name
}
@app.get("/api/protected")
async def protected_endpoint(token_payload: dict = Depends(verify_token)):
return {
"message": "Access granted",
"user_id": token_payload.get("sub")
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000, ssl_certfile="cert.pem", ssl_keyfile="key.pem")
```
### 3. **API Gateway Security Configuration**
```yaml
# nginx-api-gateway.conf
# Nginx API Gateway with security hardening
http {
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'" always;
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# Request body size limit
client_max_body_size 10M;
client_body_buffer_size 128k;
# Timeout settings
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL configuration
ssl_certificate /etc/ssl/certs/api.example.com.crt;
ssl_certificate_key /etc/ssl/private/api.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# API endpoints
location /api/ {
# Rate limiting
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 10;
# CORS headers
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
# Block common exploits
if ($request_method !~ ^(GET|POST|PUT|DELETE|HEAD)$ ) {
return 444;
}
# Proxy to backend
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Auth endpoints with stricter limits
location /api/auth/ {
limit_req zone=auth_limit burst=5 nodelay;
proxy_pass http://backend:3000;
}
# Block access to sensitive files
location ~ /\. {
deny all;
return 404;
}
}
}
```
## Best Practices
### ✅ DO
- Use HTTPS everywhere
- Implement rate limiting
- Validate all inputs
- Use security headers
- Log security events
- Implement CORS properly
- Use strong authentication
- Version your APIs
### ❌ DON'T
- Expose stack traces
- Return detailed errors
- Trust user input
- Use HTTP for APIs
- Skip input validation
- Ignore rate limiting
## Security Checklist
- [ ] HTTPS enforced
- [ ] Authentication required
- [ ] Authorization implemented
- [ ] Rate limiting active
- [ ] Input validation
- [ ] CORS configured
- [ ] Security headers set
- [ ] Error handling secure
- [ ] Logging enabled
- [ ] API versioning
## Resources
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
- [API Security Best Practices](https://github.com/shieldfy/API-Security-Checklist)
- [JWT Best Practices](https://tools.ietf.org/html/rfc8725)

View File

@@ -0,0 +1,384 @@
---
name: database-migration-management
description: Manage database migrations and schema versioning. Use when planning migrations, version control, rollback strategies, or data transformations in PostgreSQL and MySQL.
---
# Database Migration Management
## Overview
Implement robust database migration systems with version control, rollback capabilities, and data transformation strategies. Includes migration frameworks and production deployment patterns.
## When to Use
- Schema versioning and evolution
- Data transformations and cleanup
- Adding/removing tables and columns
- Index creation and optimization
- Migration testing and validation
- Rollback planning and execution
- Multi-environment deployments
## Migration Framework Setup
### PostgreSQL - Schema Versioning
```sql
-- Create migrations tracking table
CREATE TABLE schema_migrations (
version BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
duration_ms INTEGER,
checksum VARCHAR(64)
);
-- Create migration log table
CREATE TABLE migration_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
version BIGINT NOT NULL,
status VARCHAR(20) NOT NULL,
error_message TEXT,
rolled_back_at TIMESTAMP,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Function to record migration
CREATE OR REPLACE FUNCTION record_migration(
p_version BIGINT,
p_name VARCHAR,
p_duration_ms INTEGER
) RETURNS void AS $$
BEGIN
INSERT INTO schema_migrations (version, name, duration_ms)
VALUES (p_version, p_name, p_duration_ms)
ON CONFLICT (version) DO UPDATE
SET executed_at = CURRENT_TIMESTAMP;
END;
$$ LANGUAGE plpgsql;
```
### MySQL - Migration Tracking
```sql
-- Create migrations table for MySQL
CREATE TABLE schema_migrations (
version BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
duration_ms INT,
checksum VARCHAR(64)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Migration status table
CREATE TABLE migration_status (
id INT AUTO_INCREMENT PRIMARY KEY,
version BIGINT NOT NULL,
status ENUM('pending', 'completed', 'failed', 'rolled_back'),
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
## Common Migration Patterns
### Adding Columns
**PostgreSQL - Safe Column Addition:**
```sql
-- Migration: 20240115_001_add_phone_to_users.sql
-- Add column with default (non-blocking)
ALTER TABLE users
ADD COLUMN phone VARCHAR(20) DEFAULT '';
-- Add constraint after population
ALTER TABLE users
ADD CONSTRAINT phone_format
CHECK (phone = '' OR phone ~ '^\+?[0-9\-\(\)]{10,}$');
-- Create index
CREATE INDEX CONCURRENTLY idx_users_phone ON users(phone);
-- Rollback:
-- DROP INDEX CONCURRENTLY idx_users_phone;
-- ALTER TABLE users DROP COLUMN phone;
```
**MySQL - Column Addition:**
```sql
-- Migration: 20240115_001_add_phone_to_users.sql
-- Add column with ALTER
ALTER TABLE users
ADD COLUMN phone VARCHAR(20) DEFAULT '',
ADD INDEX idx_phone (phone);
-- Rollback:
-- ALTER TABLE users DROP COLUMN phone;
```
### Renaming Columns
**PostgreSQL - Column Rename:**
```sql
-- Migration: 20240115_002_rename_user_name_columns.sql
-- Rename columns
ALTER TABLE users RENAME COLUMN user_name TO full_name;
ALTER TABLE users RENAME COLUMN user_email TO email_address;
-- Update indexes
REINDEX TABLE users;
-- Rollback:
-- ALTER TABLE users RENAME COLUMN email_address TO user_email;
-- ALTER TABLE users RENAME COLUMN full_name TO user_name;
```
### Creating Indexes Non-blocking
**PostgreSQL - Concurrent Index Creation:**
```sql
-- Migration: 20240115_003_add_performance_indexes.sql
-- Create indexes without blocking writes
CREATE INDEX CONCURRENTLY idx_orders_user_created
ON orders(user_id, created_at DESC);
CREATE INDEX CONCURRENTLY idx_products_category_active
ON products(category_id)
WHERE active = true;
-- Verify index creation
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE indexname LIKE 'idx_%';
-- Rollback:
-- DROP INDEX CONCURRENTLY idx_orders_user_created;
-- DROP INDEX CONCURRENTLY idx_products_category_active;
```
**MySQL - Online Index Creation:**
```sql
-- Migration: 20240115_003_add_performance_indexes.sql
-- Create indexes with ALGORITHM=INPLACE and LOCK=NONE
ALTER TABLE orders
ADD INDEX idx_user_created (user_id, created_at),
ALGORITHM=INPLACE, LOCK=NONE;
-- Monitor progress
SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST
WHERE INFO LIKE 'ALTER TABLE%';
```
### Data Transformations
**PostgreSQL - Data Cleanup Migration:**
```sql
-- Migration: 20240115_004_normalize_email_addresses.sql
-- Normalize existing email addresses
UPDATE users
SET email = LOWER(TRIM(email))
WHERE email != LOWER(TRIM(email));
-- Remove duplicates by keeping latest
DELETE FROM users
WHERE id NOT IN (
SELECT DISTINCT ON (LOWER(email)) id
FROM users
ORDER BY LOWER(email), created_at DESC
);
-- Rollback: Restore from backup (no safe rollback for data changes)
```
**MySQL - Bulk Data Update:**
```sql
-- Migration: 20240115_004_update_product_categories.sql
-- Update multiple rows with JOIN
UPDATE products p
JOIN category_mapping cm ON p.old_category = cm.old_name
SET p.category_id = cm.new_category_id
WHERE p.old_category IS NOT NULL;
-- Verify update
SELECT COUNT(*) as updated_count
FROM products
WHERE category_id IS NOT NULL;
```
### Table Structure Changes
**PostgreSQL - Alter Table Migration:**
```sql
-- Migration: 20240115_005_modify_order_columns.sql
-- Add new column
ALTER TABLE orders
ADD COLUMN status_updated_at TIMESTAMP;
-- Add constraint
ALTER TABLE orders
ADD CONSTRAINT valid_status
CHECK (status IN ('pending', 'processing', 'completed', 'cancelled'));
-- Set default for existing records
UPDATE orders
SET status_updated_at = updated_at
WHERE status_updated_at IS NULL;
-- Make column NOT NULL
ALTER TABLE orders
ALTER COLUMN status_updated_at SET NOT NULL;
-- Rollback:
-- ALTER TABLE orders DROP COLUMN status_updated_at;
-- ALTER TABLE orders DROP CONSTRAINT valid_status;
```
## Testing Migrations
**PostgreSQL - Test in Transaction:**
```sql
-- Test migration in transaction (will be rolled back)
BEGIN;
-- Run migration statements
ALTER TABLE users ADD COLUMN test_column VARCHAR(255);
-- Validate data
SELECT COUNT(*) FROM users;
SELECT COUNT(DISTINCT email) FROM users;
-- Rollback if issues found
ROLLBACK;
-- Or commit if all good
COMMIT;
```
**Validate Migration:**
```sql
-- Check migration was applied
SELECT version, name, executed_at FROM schema_migrations
WHERE version = 20240115005;
-- Verify table structure
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'users'
ORDER BY ordinal_position;
```
## Rollback Strategies
**PostgreSQL - Bidirectional Migrations:**
```sql
-- Migration file: 20240115_006_add_user_status.sql
-- ===== UP =====
CREATE TYPE user_status AS ENUM ('active', 'suspended', 'deleted');
ALTER TABLE users ADD COLUMN status user_status DEFAULT 'active';
-- ===== DOWN =====
-- ALTER TABLE users DROP COLUMN status;
-- DROP TYPE user_status;
```
**Rollback Execution:**
```sql
-- Function to rollback to specific version
CREATE OR REPLACE FUNCTION rollback_to_version(p_target_version BIGINT)
RETURNS TABLE (version BIGINT, name VARCHAR, status VARCHAR) AS $$
BEGIN
-- Execute down migrations in reverse order
RETURN QUERY
SELECT m.version, m.name, 'rolled_back'::VARCHAR
FROM schema_migrations m
WHERE m.version > p_target_version
ORDER BY m.version DESC;
END;
$$ LANGUAGE plpgsql;
```
## Production Deployment
**Safe Migration Checklist:**
- Test migration on production-like database
- Verify backup exists before migration
- Schedule during low-traffic window
- Monitor table locks and long-running queries
- Have rollback plan ready
- Test rollback procedure
- Document all changes
- Run in transaction when possible
- Verify data integrity after migration
- Update application code coordinated with migration
**PostgreSQL - Long Transaction Safety:**
```sql
-- Use statement timeout to prevent hanging migrations
SET statement_timeout = '30min';
-- Use lock timeout to prevent deadlocks
SET lock_timeout = '5min';
-- Run migration with timeouts
ALTER TABLE large_table
ADD COLUMN new_column VARCHAR(255),
ALGORITHM='INPLACE';
```
## Migration Examples
**Combined Migration - Multiple Changes:**
```sql
-- Migration: 20240115_007_refactor_user_tables.sql
BEGIN;
-- 1. Create new column with data from old column
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
UPDATE users SET full_name = first_name || ' ' || last_name;
-- 2. Add indexes
CREATE INDEX idx_users_full_name ON users(full_name);
-- 3. Add new constraint
ALTER TABLE users
ADD CONSTRAINT email_unique UNIQUE(email);
-- 4. Drop old columns (after verification)
-- ALTER TABLE users DROP COLUMN first_name;
-- ALTER TABLE users DROP COLUMN last_name;
COMMIT;
```
## Resources
- [Flyway - Java Migration Tool](https://flywaydb.org/)
- [Liquibase - Database Changelog](https://www.liquibase.org/)
- [Alembic - Python Migration](https://alembic.sqlalchemy.org/)
- [PostgreSQL ALTER TABLE](https://www.postgresql.org/docs/current/sql-altertable.html)
- [MySQL ALTER TABLE](https://dev.mysql.com/doc/refman/8.0/en/alter-table.html)

View File

@@ -0,0 +1,133 @@
---
name: find-skills
description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
---
# Find Skills
This skill helps you discover and install skills from the open agent skills ecosystem.
## When to Use This Skill
Use this skill when the user:
- Asks "how do I do X" where X might be a common task with an existing skill
- Says "find a skill for X" or "is there a skill for X"
- Asks "can you do X" where X is a specialized capability
- Expresses interest in extending agent capabilities
- Wants to search for tools, templates, or workflows
- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.)
## What is the Skills CLI?
The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools.
**Key commands:**
- `npx skills find [query]` - Search for skills interactively or by keyword
- `npx skills add <package>` - Install a skill from GitHub or other sources
- `npx skills check` - Check for skill updates
- `npx skills update` - Update all installed skills
**Browse skills at:** https://skills.sh/
## How to Help Users Find Skills
### Step 1: Understand What They Need
When a user asks for help with something, identify:
1. The domain (e.g., React, testing, design, deployment)
2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
3. Whether this is a common enough task that a skill likely exists
### Step 2: Search for Skills
Run the find command with a relevant query:
```bash
npx skills find [query]
```
For example:
- User asks "how do I make my React app faster?" → `npx skills find react performance`
- User asks "can you help me with PR reviews?" → `npx skills find pr review`
- User asks "I need to create a changelog" → `npx skills find changelog`
The command will return results like:
```
Install with npx skills add <owner/repo@skill>
vercel-labs/agent-skills@vercel-react-best-practices
└ https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
```
### Step 3: Present Options to the User
When you find relevant skills, present them to the user with:
1. The skill name and what it does
2. The install command they can run
3. A link to learn more at skills.sh
Example response:
```
I found a skill that might help! The "vercel-react-best-practices" skill provides
React and Next.js performance optimization guidelines from Vercel Engineering.
To install it:
npx skills add vercel-labs/agent-skills@vercel-react-best-practices
Learn more: https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
```
### Step 4: Offer to Install
If the user wants to proceed, you can install the skill for them:
```bash
npx skills add <owner/repo@skill> -g -y
```
The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts.
## Common Skill Categories
When searching, consider these common categories:
| Category | Example Queries |
| --------------- | ---------------------------------------- |
| Web Development | react, nextjs, typescript, css, tailwind |
| Testing | testing, jest, playwright, e2e |
| DevOps | deploy, docker, kubernetes, ci-cd |
| Documentation | docs, readme, changelog, api-docs |
| Code Quality | review, lint, refactor, best-practices |
| Design | ui, ux, design-system, accessibility |
| Productivity | workflow, automation, git |
## Tips for Effective Searches
1. **Use specific keywords**: "react testing" is better than just "testing"
2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd"
3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills`
## When No Skills Are Found
If no relevant skills exist:
1. Acknowledge that no existing skill was found
2. Offer to help with the task directly using your general capabilities
3. Suggest the user could create their own skill with `npx skills init`
Example:
```
I searched for skills related to "xyz" but didn't find any matches.
I can still help you with this task directly! Would you like me to proceed?
If this is something you do often, you could create your own skill:
npx skills init my-xyz-skill
```

View File

@@ -0,0 +1,292 @@
---
name: gcp-cloud-run
description: "Specialized skill for building production-ready serverless applications on GCP. Covers Cloud Run services (containerized), Cloud Run Functions (event-driven), cold start optimization, and event-dri..."
source: vibeship-spawner-skills (Apache 2.0)
risk: unknown
---
# GCP Cloud Run
## Patterns
### Cloud Run Service Pattern
Containerized web service on Cloud Run
**When to use**: ['Web applications and APIs', 'Need any runtime or library', 'Complex services with multiple endpoints', 'Stateless containerized workloads']
```javascript
```dockerfile
# Dockerfile - Multi-stage build for smaller image
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-slim
WORKDIR /app
# Copy only production dependencies
COPY --from=builder /app/node_modules ./node_modules
COPY src ./src
COPY package.json ./
# Cloud Run uses PORT env variable
ENV PORT=8080
EXPOSE 8080
# Run as non-root user
USER node
CMD ["node", "src/index.js"]
```
```javascript
// src/index.js
const express = require('express');
const app = express();
app.use(express.json());
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// API routes
app.get('/api/items/:id', async (req, res) => {
try {
const item = await getItem(req.params.id);
res.json(item);
} catch (error) {
console.error('Error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
const PORT = process.env.PORT || 8080;
const server = app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
```
```yaml
# cloudbuild.yaml
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/my-service:$COMMIT_SHA', '.']
# Push the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/my-service:$COMMIT_SHA']
# Deploy to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'my-service'
- '--image=gcr.io/$PROJECT_ID/my-service:$COMMIT_SHA'
- '--region=us-central1'
- '--platform=managed'
- '--allow-unauthenticated'
- '--memory=512Mi'
- '--cpu=1'
- '--min-instances=1'
- '--max-instances=100'
```
### Cloud Run Functions Pattern
Event-driven functions (formerly Cloud Functions)
**When to use**: ['Simple event handlers', 'Pub/Sub message processing', 'Cloud Storage triggers', 'HTTP webhooks']
```javascript
```javascript
// HTTP Function
// index.js
const functions = require('@google-cloud/functions-framework');
functions.http('helloHttp', (req, res) => {
const name = req.query.name || req.body.name || 'World';
res.send(`Hello, ${name}!`);
});
```
```javascript
// Pub/Sub Function
const functions = require('@google-cloud/functions-framework');
functions.cloudEvent('processPubSub', (cloudEvent) => {
// Decode Pub/Sub message
const message = cloudEvent.data.message;
const data = message.data
? JSON.parse(Buffer.from(message.data, 'base64').toString())
: {};
console.log('Received message:', data);
// Process message
processMessage(data);
});
```
```javascript
// Cloud Storage Function
const functions = require('@google-cloud/functions-framework');
functions.cloudEvent('processStorageEvent', async (cloudEvent) => {
const file = cloudEvent.data;
console.log(`Event: ${cloudEvent.type}`);
console.log(`Bucket: ${file.bucket}`);
console.log(`File: ${file.name}`);
if (cloudEvent.type === 'google.cloud.storage.object.v1.finalized') {
await processUploadedFile(file.bucket, file.name);
}
});
```
```bash
# Deploy HTTP function
gcloud functions deploy hello-http \
--gen2 \
--runtime nodejs20 \
--trigger-http \
--allow-unauthenticated \
--region us-central1
# Deploy Pub/Sub function
gcloud functions deploy process-messages \
--gen2 \
--runtime nodejs20 \
--trigger-topic my-topic \
--region us-central1
# Deploy Cloud Storage function
gcloud functions deploy process-uploads \
--gen2 \
--runtime nodejs20 \
--trigger-event-filters="type=google.cloud.storage.object.v1.finalized" \
--trigger-event-filters="bucket=my-bucket" \
--region us-central1
```
```
### Cold Start Optimization Pattern
Minimize cold start latency for Cloud Run
**When to use**: ['Latency-sensitive applications', 'User-facing APIs', 'High-traffic services']
```javascript
## 1. Enable Startup CPU Boost
```bash
gcloud run deploy my-service \
--cpu-boost \
--region us-central1
```
## 2. Set Minimum Instances
```bash
gcloud run deploy my-service \
--min-instances 1 \
--region us-central1
```
## 3. Optimize Container Image
```dockerfile
# Use distroless for minimal image
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY src ./src
CMD ["src/index.js"]
```
## 4. Lazy Initialize Heavy Dependencies
```javascript
// Lazy load heavy libraries
let bigQueryClient = null;
function getBigQueryClient() {
if (!bigQueryClient) {
const { BigQuery } = require('@google-cloud/bigquery');
bigQueryClient = new BigQuery();
}
return bigQueryClient;
}
// Only initialize when needed
app.get('/api/analytics', async (req, res) => {
const client = getBigQueryClient();
const results = await client.query({...});
res.json(results);
});
```
## 5. Increase Memory (More CPU)
```bash
# Higher memory = more CPU during startup
gcloud run deploy my-service \
--memory 1Gi \
--cpu 2 \
--region us-central1
```
```
## Anti-Patterns
### ❌ CPU-Intensive Work Without Concurrency=1
**Why bad**: CPU is shared across concurrent requests. CPU-bound work
will starve other requests, causing timeouts.
### ❌ Writing Large Files to /tmp
**Why bad**: /tmp is an in-memory filesystem. Large files consume
your memory allocation and can cause OOM errors.
### ❌ Long-Running Background Tasks
**Why bad**: Cloud Run throttles CPU to near-zero when not handling
requests. Background tasks will be extremely slow or stall.
## ⚠️ Sharp Edges
| Issue | Severity | Solution |
|-------|----------|----------|
| Issue | high | ## Calculate memory including /tmp usage |
| Issue | high | ## Set appropriate concurrency |
| Issue | high | ## Enable CPU always allocated |
| Issue | medium | ## Configure connection pool with keep-alive |
| Issue | high | ## Enable startup CPU boost |
| Issue | medium | ## Explicitly set execution environment |
| Issue | medium | ## Set consistent timeouts |
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

View File

@@ -37,6 +37,7 @@ Before any design work, ensure you have loaded:
- `krow-mobile-design-system` — Colors, typography, icons, spacing, component patterns
- `frontend-design`
- `ui-ux-pro-max`
- `mobile-design`
Load additional skills as needed for specific design challenges.

View File

@@ -21,19 +21,42 @@ This skill defines the design token system, component patterns, screen structure
### Color Palette
**Primary:**
| Token | Hex | Usage |
|-------|-----|-------|
| Primary | `#0A39DF` | CTAs, active states, links, selected chips, nav active icons, pay rates |
| Foreground | `#121826` | Headings, primary text, dark UI elements |
| Text Secondary | `#6A7382` | Labels, captions, inactive nav, section headers, placeholder text, back chevrons |
| Secondary BG | `#F1F3F5` | Subtle backgrounds, dividers, map placeholders |
| Border | `#D1D5DB` | Card borders, unselected chip borders, outline button borders |
| Input Border | `#E2E8F0` | Text input borders (lighter than general border) |
| Destructive | `#F04444` | Error states, destructive actions (e.g., Request Swap) |
| Background | `#FAFBFC` | Page/artboard background |
| Card BG | `#FFFFFF` | Card surfaces, input backgrounds |
| Success | `#059669` | Active status dot, checkmark icons, requirement met |
| Warning Amber | `#D97706` | Urgent/Pending badge text |
| Foreground | `#121826` | Headings, primary text, dark UI elements |
| Primary | `#0A39DF` | CTAs, active states, links, selected chips, nav active icons, pay rates |
| Primary Fg | `#F7FAFC` | Light foreground on primary surfaces |
**Semantic:**
| Token | Hex | Usage |
|-------|-----|-------|
| Secondary | `#F1F3F5` | Subtle backgrounds, dividers, secondary button bg |
| Accent | `#F9E547` | Highlight, warning chip accents |
| Text Secondary | `#6A7382` | Labels, captions, inactive nav, section headers, back chevrons |
| Destructive | `#F04444` | Error states, destructive actions |
**Border & Input:**
| Token | Hex | Usage |
|-------|-----|-------|
| Border | `#D1D5DB` | Card borders, unselected chip borders, outline button borders |
| Input | `#F5F6F8` | Text input background (read-only/disabled states) |
**Status:**
| Token | Hex | Usage |
|-------|-----|-------|
| Success | `#10B981` | Accept buttons, active status, checkmarks |
| Info | `#0A39DF` | Informational badges (same as Primary) |
| Warning | `#D97706` | Urgent/Pending badge text |
| Neutral | `#94A3B8` | Disabled text, placeholder text |
| Danger | `#F04444` | Error badges, destructive (same as Destructive) |
**Gradients:**
| Token | Definition | Usage |
|-------|-----------|-------|
| mobileHero | Foreground → Primary → Primary Fg | Hero sections, splash screens |
| adminHero | Primary → Success | Admin/dashboard hero cards |
### Semantic Badge Colors
@@ -44,74 +67,121 @@ This skill defines the design token system, component patterns, screen structure
| Pending | `#FEF9EE` | `#D97706` |
| Urgent | `#FEF9EE` | `#D97706` |
| One-Time | `#ECFDF5` | `#059669` |
| Recurring | `#EBF0FF` | `#0A39DF` (use `#EFF6FF` bg on detail pages) |
| Recurring | `#EFF6FF` | `#0A39DF` |
### Typography
| Style | Font | Size | Weight | Line Height | Usage |
|-------|------|------|--------|-------------|-------|
| Display | Inter Tight | 28px | 700 | 34px | Page titles (Find Shifts, My Shifts) |
| H1 | Inter Tight | 24px | 700 | 30px | Detail page titles (venue names) |
| H2 | Inter Tight | 20px | 700 | 26px | Section headings |
| H3 | Inter Tight | 18px | 700 | 22px | Card titles, schedule values |
| Body Large | Manrope | 16px | 600 | 20px | Button text, CTA labels |
| Body Default | Manrope | 14px | 400-500 | 18px | Body text, descriptions |
| Body Small | Manrope | 13px | 400-500 | 16px | Card metadata, time/pay info |
| Caption | Manrope | 12px | 500-600 | 16px | Small chip text, tab labels |
| Section Label | Manrope | 11px | 700 | 14px | Uppercase section headers (letter-spacing: 0.06em) |
| Badge Text | Manrope | 11px | 600-700 | 14px | Status badge labels (letter-spacing: 0.04em) |
| Nav Label | Manrope | 10px | 600 | 12px | Bottom nav labels |
**Inter Tight — Headings:**
| Style | Size | Weight | Letter Spacing | Line Height | Usage |
|-------|------|--------|---------------|-------------|-------|
| Display | 28px | 700 | -0.02em | 34px | Page titles (Find Shifts, My Shifts) |
| Heading 1 | 24px | 700 | -0.02em | 30px | Detail page titles (venue names) |
| Heading 2 | 20px | 700 | -0.01em | 26px | Section headings |
| Heading 3 | 18px | 700 | -0.01em | 22px | Card titles, schedule values |
| Heading 4 | 16px | 700 | — | 20px | Card titles (standard cards), sub-headings |
### Spacing
**Manrope — Body:**
| Style | Size | Weight | Line Height | Usage |
|-------|------|--------|-------------|-------|
| Body Large Regular | 16px | 400 | 20px | Long body text |
| Body Large Medium | 16px | 500 | 20px | Emphasized body text |
| Body Large Semibold | 16px | 600 | 20px | Strong labels, Full Width CTA text (15px) |
| Body Default | 14px | 400 | 18px | Body text, descriptions |
| Body Default Semibold | 14px | 600 | 18px | Button text, chip text, bold body |
| Caption | 12px | 400 | 16px | Small text, helper text, input labels |
| Overline Label | 11px | 600 | 14px | Uppercase section headers (letter-spacing: 0.06em) |
| Badge Text | 11px | 600-700 | 14px | Status badge labels (letter-spacing: 0.04em) |
| Nav Label | 10px | 600 | 12px | Bottom nav labels |
### Spacing Scale
| Token | Value | Usage |
|-------|-------|-------|
| Page padding | 24px | Horizontal padding from screen edge |
| Section gap | 16-24px | Between major content sections |
| Group gap | 8-12px | Within a section (e.g., label to input) |
| Element gap | 4px | Tight spacing (e.g., subtitle under title) |
| Bottom safe area | 40px | Padding below last element / CTA |
| xs | 4px | Tight spacing (subtitle under title) |
| sm | 8px | Element gap within groups |
| md | 12px | Group gap (label to input) |
| lg | 16px | Card padding, medium section gap |
| xl | 24px | Page margins, section gap |
| 2xl | 32px | Large section separation |
**Page Layout:**
| Token | Value |
|-------|-------|
| Page margins | 24px |
| Section gap | 24px |
| Card padding | 16px |
| Element gap | 8-12px |
| Background | `#FAFBFC` |
| Bottom safe area | 40px |
### Border Radii
| Token | Value | Usage |
|-------|-------|-------|
| sm | 8px | Small chips, badges, status pills, map placeholder |
| md | 12px | Cards, inputs, location cards, contact cards, search fields |
| lg | 14px | Buttons, CTA containers, shift cards (Find Shifts) |
| xl | 24px | Not commonly used |
| sm | 8px | Small chips, badges, status pills |
| md | 12px | Cards, inputs, list row containers, data rows |
| lg | 18px | Hero cards, gradient cards |
| xl | 24px | Large containers |
| pill | 999px | Progress bar segments only |
### Icon Sizes
Standard sizes: 16, 20, 24, 32dp
## 2. Component Patterns
### Buttons
**Primary CTA:**
- Background: `#0A39DF`, radius: 14px, height: 52px
- Text: Manrope 16px/600, color: `#FFFFFF`
- Padding: 16px vertical, 16px horizontal
All buttons: radius 14px, padding 12px/24px, text Manrope 14px/600
**Secondary/Outline Button:**
- Background: `#FFFFFF`, border: 1.5px `#D1D5DB`, radius: 14px, height: 52px
- Text: Manrope 16px/600, color: `#121826`
**Primary:**
- Background: `#0A39DF`, text: `#FFFFFF`
**Destructive Outline Button:**
- Background: `#FFFFFF`, border: 1.5px `#F04444`, radius: 14px
- Text: Manrope 14px/600, color: `#F04444`
**Secondary:**
- Background: `#F1F3F5`, border: 1.5px `#D1D5DB`, text: `#121826`
**Destructive:**
- Background: `#F04444`, text: `#FFFFFF`
**Disabled:**
- Background: `#F1F3F5`, no border, text: `#94A3B8`
**Accept:**
- Background: `#10B981`, text: `#FFFFFF`
**Dark:**
- Background: `#121826`, text: `#FFFFFF`
**Full Width CTA:**
- Same as Primary but `width: 100%`, padding 14px/24px, text Manrope 15px/600
**Back Icon Button (Bottom CTA):**
- 52x52px square, border: 1.5px `#D1D5DB`, radius: 14px, background: `#FFFFFF`
- 52x52px square, border: 0.5px `#D1D5DB`, radius: 14px, background: `#FFFFFF`
- Contains chevron-left SVG (20x20, viewBox 0 0 24 24, stroke `#121826`, strokeWidth 2)
- Path: `M15 18L9 12L15 6`
### Chips
All chips: border 1.5px, text Manrope 14px/600, gap 8px for icon+text
**Default (Large) - for role/skill selection:**
- Selected: bg `#EFF6FF`, border 1.5px `#0A39DF`, radius 10px, padding 12px/16px
- Checkmark icon (14x14, stroke `#0A39DF`), text Manrope 14px/600 `#0A39DF`
- Unselected: bg `#FFFFFF`, border 1.5px `#6A7382`, radius 10px, padding 12px/16px
- Selected: bg `#EFF6FF`, border `#0A39DF`, radius 10px, padding 12px/16px
- Checkmark icon (14x14, stroke `#0A39DF`), text `#0A39DF`
- Unselected: bg `#FFFFFF`, border `#6A7382`, radius 10px, padding 12px/16px
- Text Manrope 14px/500 `#6A7382`
**Warning Chips:**
- Selected: bg `#F9E5471A`, border `#E6A817`, radius 10px, padding 12px/16px
- Checkmark icon (stroke `#E6A817`), text `#E6A817`
- Unselected: bg `#FFFFFF`, border `#F0D78C`, radius 10px, padding 12px/16px
- Text `#E6A817`
**Error Chips:**
- Selected: bg `#FEF2F2`, border `#F04444`, radius 10px, padding 12px/16px
- Checkmark icon (stroke `#F04444`), text `#F04444`
- Unselected: bg `#FFFFFF`, border `#FECACA`, radius 10px, padding 12px/16px
- Text `#F04444`
**Small - for tabs, filters:**
- Selected: bg `#EFF6FF`, border 1.5px `#0A39DF`, radius 8px, padding 6px/12px
- Checkmark icon (12x12), text Manrope 12px/600 `#0A39DF`
@@ -119,8 +189,6 @@ This skill defines the design token system, component patterns, screen structure
- Text Manrope 12px/500 `#6A7382`
- Active (filled): bg `#0A39DF`, radius 8px, padding 6px/12px
- Text Manrope 12px/600 `#FFFFFF`
- Dark (filters button): bg `#121826`, radius 8px, padding 6px/12px
- Text Manrope 12px/600 `#FFFFFF`, with leading icon
**Status Badges:**
- Radius: 8px, padding: 4px/8px
@@ -131,39 +199,65 @@ This skill defines the design token system, component patterns, screen structure
- Border: 1.5px `#E2E8F0`, radius: 12px, padding: 12px/14px
- Background: `#FFFFFF`
- Placeholder: Manrope 14px/400, color `#6A7382`
- Filled: Manrope 14px/500, color `#121826`
- Label above: Manrope 14px/500, color `#121826`
- Placeholder: Manrope 14px/400, color `#94A3B8`
- Filled: Manrope 14px/400, color `#111827`
- Label above: Manrope 12px/400, spacing 0%:
- Default/filled: color `#94A3B8`
- Filled with value: color `#6A7382`
- Focused: color `#0A39DF`
- Error: color `#F04444`
- Disabled: color `#94A3B8`
- Focused: border color `#0A39DF`, border-width 2px
- Error: border color `#F04444`, helper text `#F04444`
- Error: border color `#F04444`, border-width 2px, background `#FEF2F2`
- Error helper text: Manrope 12px/400, color `#F04444`
### Cards (Shift List Items)
### Border Width
- Background: `#FFFFFF`, border: 1px `#D1D5DB`, radius: 12-14px
- Padding: 16px
- Content: venue name (Manrope 15px/600 `#121826`), subtitle (Manrope 13px/400 `#6A7382`)
- Metadata row: icon (14px, `#6A7382`) + text (Manrope 13px/500 `#6A7382`)
- Pay rate: Inter Tight 18px/700 `#0A39DF`
- **Standard border width: `0.5px`** — All card borders, dividers, and outline buttons use `0.5px` unless explicitly stated otherwise.
- **Text inputs: `1.5px`** — To ensure visibility and distinction from card borders.
- **Chips: `1.5px`** — All chip variants (default, warning, error, small).
- **Secondary buttons: `1.5px`** — Outline/secondary button borders.
### Schedule/Pay Info Cards
### Cards
- Two-column layout with 12px gap
- Background: `#FFFFFF`, border: 1px `#D1D5DB`, radius: 12px, padding: 16px
- Label: Manrope 11px/500-700 uppercase `#6A7382` (letter-spacing 0.05em)
- Value: Inter Tight 18px/700 `#121826` (schedule) or `#121826` (pay)
- Sub-text: Manrope 13px/400 `#6A7382`
**Standard Card:**
- Background: `#FFFFFF`, border: 0.5px `#D1D5DB`, radius: 12px, padding: 16px
- Title: Inter Tight 16px/700 `#121826`
- Body: Manrope 14px/400 `#6A7382`
- Gap: 8px between title and body
**Hero / Gradient Card:**
- Radius: 18px, padding: 20px, gap: 6px
- Background: gradient (mobileHero or adminHero)
- Label: Manrope 12px/400 `#FFFFFFB3` (white 70%)
- Value: Inter Tight 28px/700 `#FFFFFF`
- Sub-text: Manrope 12px/400 `#FFFFFF99` (white 60%)
**List Rows (grouped):**
- Container: radius 12px, border 0.5px `#D1D5DB`, background `#FFFFFF`, overflow clip
- Row: padding ~16px, gap between text elements 2px
- Row title: Manrope 14px/600 `#121826`
- Row subtitle: Manrope 13px/400 `#6A7382`
- Row divider: 1px `#D1D5DB` (between rows, not on last)
- Chevron: `` or SVG, `#6A7382`
**Data Row:**
- Background: `#F1F3F5`, radius: 12px, padding: 12px
- Label: Manrope 11px/400 `#6A7382`
- Value: Inter Tight 20px/700 `#121826`
- Layout: flex row, equal width columns, gap 8px
### Contact/Info Rows
- Container: radius 12px, border 1px `#D1D5DB`, background `#FFFFFF`, overflow clip
- Row: padding 13px/16px, gap 10px, border-bottom 1px `#F1F3F5` (except last)
- Container: radius 12px, border 0.5px `#D1D5DB`, background `#FFFFFF`, overflow clip
- Row: padding 13px/16px, gap 10px, border-bottom 0.5px `#F1F3F5` (except last)
- Icon: 16px, stroke `#6A7382`
- Label: Manrope 13px/500 `#6A7382`, width 72px fixed
- Value: Manrope 13px/500 `#121826` (or `#0A39DF` for phone/links)
### Section Headers
- Text: Manrope 11px/700, uppercase, letter-spacing 0.06em, color `#6A7382`
- Text: Manrope 11px/600, uppercase, letter-spacing 0.06em, color `#6A7382`
- Gap to content below: 10px
## 3. Screen Structure

View File

@@ -2,16 +2,10 @@
<!-- Provide a clear and concise description of your changes -->
---
## 🔗 Related Issues
<!-- Link any related issues using #issue_number -->
Closes #
Related to #
---
## 🎯 Type of Change
@@ -26,8 +20,6 @@ Related to #
- [ ] 🎨 **Style** (formatting, linting, or minor code style changes)
- [ ] 🏗️ **Architecture** (significant structural changes)
---
## 📦 Affected Areas
<!-- Mark the relevant areas that were modified -->
@@ -39,8 +31,6 @@ Related to #
- [ ] 🚀 **CI/CD** (GitHub Actions, deployment configs)
- [ ] 📚 **Documentation** (Docs, onboarding guides)
---
## ✅ Testing
<!-- Describe how you tested these changes -->
@@ -48,9 +38,6 @@ Related to #
**Test Details:**
<!-- Provide specific test cases or scenarios -->
---
## 🔄 Breaking Changes
<!-- Are there any breaking changes? If yes, describe them -->
@@ -61,9 +48,6 @@ Related to #
**Details:**
<!-- Describe migration path or deprecation period if applicable -->
---
## 🎯 Checklist
<!-- Complete this before requesting review -->
@@ -79,15 +63,10 @@ Related to #
- [ ] Sensitive data is not committed
- [ ] Environment variables documented (if added)
---
## 📝 Additional Notes
<!-- Any additional context, decisions, or considerations -->
---
## 🔍 Review Checklist for Maintainers
- [ ] Code quality and readability

View File

@@ -26,6 +26,14 @@ jobs:
make -n backend-smoke-core ENV=dev
make -n backend-smoke-commands ENV=dev
make -n backend-logs-core ENV=dev
make -n backend-bootstrap-v2-dev ENV=dev
make -n backend-deploy-core-v2 ENV=dev
make -n backend-deploy-commands-v2 ENV=dev
make -n backend-deploy-query-v2 ENV=dev
make -n backend-smoke-core-v2 ENV=dev
make -n backend-smoke-commands-v2 ENV=dev
make -n backend-smoke-query-v2 ENV=dev
make -n backend-logs-core-v2 ENV=dev
backend-services-tests:
runs-on: ubuntu-latest
@@ -34,6 +42,7 @@ jobs:
service:
- backend/core-api
- backend/command-api
- backend/query-api
defaults:
run:
working-directory: ${{ matrix.service }}

4
.gitignore vendored
View File

@@ -182,6 +182,8 @@ internal/launchpad/prototypes-src/
# Temporary migration artifacts
_legacy/
krow-workforce-export-latest/
skills/
skills-lock.json
# Data Connect Generated SDKs (Explicit)
apps/mobile/packages/data_connect/lib/src/dataconnect_generated/
@@ -194,4 +196,6 @@ apps/mobile/legacy/*
AGENTS.md
TASKS.md
CLAUDE.md
.claude/agents/paper-designer.md
.claude/agent-memory/paper-designer
\n# Android Signing (Secure)\n**.jks\n**key.properties

View File

@@ -28,3 +28,4 @@
| 2026-02-25 | 0.1.23 | Updated schema blueprint and reconciliation docs to add `business_memberships` and `vendor_memberships` as first-class data actors. |
| 2026-02-25 | 0.1.24 | Removed stale `m4-discrepencies.md` document from M4 planning docs cleanup. |
| 2026-02-25 | 0.1.25 | Added target schema model catalog with keys and domain relationship diagrams for slide/workshop use. |
| 2026-02-26 | 0.1.26 | Added isolated v2 backend foundation targets, scaffolded `backend/query-api`, and expanded backend CI dry-runs/tests for v2/query. |

149
CLAUDE.md
View File

@@ -1,149 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
KROW Workforce is a workforce management platform monorepo containing Flutter mobile apps, a React web dashboard, and Firebase backend services.
## Repository Structure
```
apps/mobile/ # Flutter monorepo (Melos workspace)
apps/staff/ # Staff mobile app
apps/client/ # Client (business) mobile app
packages/
design_system/ # Shared UI tokens & components
core/ # Cross-cutting concerns (mixins, extensions)
core_localization/# i18n via Slang
domain/ # Pure Dart entities & failures
data_connect/ # Firebase Data Connect adapter (connectors)
features/staff/ # Staff feature packages
features/client/ # Client feature packages
apps/web/ # React/Vite web dashboard (TypeScript, Tailwind, Redux Toolkit)
backend/
dataconnect/ # Firebase Data Connect GraphQL schemas
core-api/ # Core business logic service
cloud-functions/ # Serverless functions
```
## Common Commands
All commands use the root `Makefile` (composed from `makefiles/*.mk`). Run `make help` for the full list.
### Mobile (Flutter)
```bash
make mobile-install # Bootstrap Melos workspace + generate SDK
make mobile-staff-dev-android # Run staff app (add DEVICE=android)
make mobile-client-dev-android # Run client app
make mobile-analyze # Lint (flutter analyze)
make mobile-test # Run tests
make test-e2e # Maestro E2E tests (both apps)
```
Single-package operations via Melos:
```bash
cd apps/mobile
melos run gen:l10n # Generate localization (Slang)
melos run gen:build # Run build_runner
melos run analyze:all # Analyze all packages
melos run test:all # Test all packages
```
### Web (React/Vite)
```bash
make web-install # npm install
make web-dev # Start dev server
make web-build # Production build
make web-lint # ESLint
make web-test # Vitest
```
### Backend (Data Connect)
```bash
make dataconnect-generate-sdk [ENV=dev] # Generate SDK
make dataconnect-deploy [ENV=dev] # Deploy schemas
make dataconnect-sync-full [ENV=dev] # Deploy + migrate + generate
```
## Mobile Architecture
**Clean Architecture** with strict inward dependency flow:
```
Presentation (Pages, BLoCs, Widgets)
→ Application (Use Cases)
→ Domain (Entities, Repository Interfaces, Failures)
← Data (Repository Implementations, Connectors)
```
### Key Patterns
- **State management:** Flutter BLoC/Cubit. Register BLoCs with `i.add()` (transient), never `i.addSingleton()`. Use `BlocProvider.value()` for shared BLoCs.
- **DI & Routing:** Flutter Modular. Safe navigation via `safeNavigate()`, `safePush()`, `popSafe()`. Never use `Navigator.push()` directly.
- **Error handling in BLoCs:** Use `BlocErrorHandler` mixin with `_safeEmit()` to prevent StateError on disposed BLoCs.
- **Backend access:** All Data Connect calls go through the `data_connect` package's Connectors. Use `_service.run(() => connector.<query>().execute())` for automatic auth/token management.
- **Session management:** `SessionHandlerMixin` + `SessionListener` widget. Initialized in `main.dart` with role-based config.
- **Localization:** All user-facing strings via `context.strings.<key>` from `core_localization`. Error messages via `ErrorTranslator`.
- **Design system:** Use tokens from `UiColors`, `UiTypography`, `UiConstants`. Never hardcode colors, fonts, or spacing.
### Feature Package Structure
New features go in `apps/mobile/packages/features/<app>/<feature>/`:
```
lib/src/
domain/repositories/ # Abstract interface classes
data/repositories_impl/ # Implementations using data_connect
application/ # Use cases (business logic)
presentation/
blocs/ # BLoCs/Cubits
pages/ # Pages (prefer StatelessWidget)
widgets/ # Reusable widgets
```
### Critical Rules
- Features must not import other features directly
- Business logic belongs in Use Cases, never in BLoCs or widgets
- Firebase packages (`firebase_auth`, `firebase_data_connect`) belong only in `data_connect`
- Don't add 3rd-party packages without checking `packages/core` first
- Generated code directories are excluded from analysis: `**/dataconnect_generated/**`, `**/*.g.dart`, `**/*.freezed.dart`
## Code Generation
- **Slang** (i18n): Input `lib/src/l10n/*.i18n.json` → Output `strings.g.dart`
- **build_runner**: Various generated files (`.g.dart`, `.freezed.dart`)
- **Firebase Data Connect**: Auto-generated SDK in `packages/data_connect/lib/src/dataconnect_generated/`
## Naming Conventions (Dart)
| Type | Convention | Example |
|------|-----------|---------|
| Files | `snake_case` | `user_profile_page.dart` |
| Classes | `PascalCase` | `UserProfilePage` |
| Interfaces | suffix `Interface` | `AuthRepositoryInterface` |
| Implementations | suffix `Impl` | `AuthRepositoryImpl` |
## Key Documentation
- `docs/MOBILE/00-agent-development-rules.md` — Non-negotiable architecture rules
- `docs/MOBILE/01-architecture-principles.md` — Clean architecture details
- `docs/MOBILE/02-design-system-usage.md` — Design system token usage
- `docs/MOBILE/03-data-connect-connectors-pattern.md` — Backend integration pattern
- `docs/MOBILE/05-release-process.md` — Release quick reference
- `docs/RELEASE/mobile-releases.md` — Complete release guide
## Skills & Sub-Agents
#### Skills
- The project has 4 specialized skills in `.claude/skills/` that provide deep domain knowledge. Invoke them and other global skills that you have when working in their domains.
#### Sub-Agents
- The project has 4 sub-agents in `.claude/sub-agents/` that can be invoked for specific tasks. Invoke them and other global sub-agents that you have when working in their domains.
## CI/CD
- `.github/workflows/mobile-ci.yml` — Mobile build & test on PR
- `.github/workflows/product-release.yml` — Automated versioning, tags, APK builds
- `.github/workflows/web-quality.yml` — Web linting & tests

View File

@@ -89,6 +89,18 @@ help:
@echo " make backend-smoke-commands [ENV=dev] Run health smoke test for command service (/health)"
@echo " make backend-logs-core [ENV=dev] Tail/read logs for core service"
@echo ""
@echo " ☁️ BACKEND FOUNDATION V2 (Isolated Parallel Stack)"
@echo " ────────────────────────────────────────────────────────────────────"
@echo " make backend-bootstrap-v2-dev [ENV=dev] Bootstrap isolated v2 resources + SQL instance"
@echo " make backend-deploy-core-v2 [ENV=dev] Build and deploy core API v2 service"
@echo " make backend-deploy-commands-v2 [ENV=dev] Build and deploy command API v2 service"
@echo " make backend-deploy-query-v2 [ENV=dev] Build and deploy query API v2 scaffold"
@echo " make backend-v2-migrate-idempotency Create/upgrade command idempotency table for v2 DB"
@echo " make backend-smoke-core-v2 [ENV=dev] Run health smoke test for core API v2 (/health)"
@echo " make backend-smoke-commands-v2 [ENV=dev] Run health smoke test for command API v2 (/health)"
@echo " make backend-smoke-query-v2 [ENV=dev] Run health smoke test for query API v2 (/health)"
@echo " make backend-logs-core-v2 [ENV=dev] Tail/read logs for core API v2"
@echo ""
@echo " 🛠️ DEVELOPMENT TOOLS"
@echo " ────────────────────────────────────────────────────────────────────"
@echo " make install-melos Install Melos globally (for mobile dev)"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/launcher_icon"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/launcher_icon"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -0,0 +1,2 @@
// Build configuration for dev flavor - use AppIcon-dev
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-dev

View File

@@ -0,0 +1,2 @@
// Build configuration for stage flavor - use AppIcon-stage
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-stage

View File

@@ -256,7 +256,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
BC26E38F2F5F614000517BDF /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
@@ -383,7 +383,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us Business [STAGE]";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-stage";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -563,7 +563,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us Business [DEV] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -665,7 +665,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us Business [DEV] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -762,7 +762,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us Business [STAGE] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-stage";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -951,7 +951,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us Business [DEV] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -1040,7 +1040,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us Business [STAGE]";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-stage";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -1220,7 +1220,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us Business [DEV] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -1311,7 +1311,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us Business [STAGE] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-stage";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;

View File

@@ -0,0 +1 @@
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -0,0 +1 @@
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/launcher_icon"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/launcher_icon"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,2 @@
// Build configuration for dev flavor - use AppIcon-dev
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-dev

View File

@@ -0,0 +1,2 @@
// Build configuration for stage flavor - use AppIcon-stage
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-stage

View File

@@ -384,7 +384,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us [STAGE] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-stage";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -564,7 +564,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us [DEV] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -666,7 +666,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us [DEV] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -763,7 +763,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us [STAGE] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-stage";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -954,7 +954,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us [DEV] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -1045,7 +1045,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us [STAGE] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-stage";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -1225,7 +1225,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us [DEV] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
@@ -1314,7 +1314,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
APP_NAME = "KROW With Us [STAGE] ";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-stage";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;

View File

@@ -0,0 +1 @@
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Some files were not shown because too many files have changed in this diff Show More