Week 4 Day 05 Childcare Domain Generated by Gemini Code Assist

Backend Infrastructure & API

Comprehensive guide to building the complete backend on Google Cloud Platform.

Architecture Overview

Prerequisites

  1. Google Cloud SDK (gcloud) installed and authenticated.
  2. Node.js installed.
  3. Firebase Project created and linked to your GCP project.
  4. Terminal open at your project root.

Step 1: Environment & Identity Setup

Create specific Service Accounts for API Gateway and Cloud Functions.

# 1. Set Project Variables
export PROJECT_ID="your-project-id"
export REGION="us-central1"
export GATEWAY_SA_NAME="api-gateway-sa"
export FUNC_SA_NAME="backend-function-sa"

# 2. Set the active project
gcloud config set project $PROJECT_ID

# 3. Enable Required Google Cloud APIs
gcloud services enable \
  cloudfunctions.googleapis.com \
  apigateway.googleapis.com \
  servicemanagement.googleapis.com \
  servicecontrol.googleapis.com \
  firestore.googleapis.com \
  storage.googleapis.com \
  iam.googleapis.com

# 4. Create Service Account for API Gateway
gcloud iam service-accounts create $GATEWAY_SA_NAME \
  --display-name="API Gateway Service Account"

# 5. Create Service Account for Cloud Functions
gcloud iam service-accounts create $FUNC_SA_NAME \
  --display-name="Backend Function Service Account"

# 6. Grant Permissions to Function Service Account
# Access to Firestore
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$FUNC_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/datastore.user"

# Access to Cloud Storage
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$FUNC_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/storage.admin"

Step 2: Create Storage Buckets

# 1. Define Unique Bucket Names
export SYLLABUS_BUCKET="daycare-management$PROJECT_ID"

# 2. Create Buckets
gcloud storage buckets create gs://$SYLLABUS_BUCKET --location=$REGION

Step 3: Develop the Backend Code

1. Initialize package.json

{
  "name": "daycare-backend",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "@google-cloud/functions-framework": "^3.0.0",
    "@google-cloud/storage": "^7.0.0",
    "firebase-admin": "^11.0.0"
  }
}

2. Create index.js (The Logic Core)

const functions = require('@google-cloud/functions-framework');
const admin = require('firebase-admin');
const { Storage } = require('@google-cloud/storage');

if (admin.apps.length === 0) {
  admin.initializeApp();
}
const db = admin.firestore();
const storage = new Storage();

// --- HELPER: Security & RBAC ---
async function getAuthenticatedUser(req) {
  const userInfoHeader = req.get('X-Apigateway-Api-User-Info');
  if (!userInfoHeader) throw new Error('Unauthenticated: Missing Identity Header');
  
  const userPayload = JSON.parse(Buffer.from(userInfoHeader, 'base64').toString());
  const uid = userPayload.user_id;
  
  const userDoc = await db.collection('users').doc(uid).get();
  if (!userDoc.exists) throw new Error('User profile not found');
  return { uid, role: userDoc.data().role }; 
}

// ==========================================
// 1. STAFF OPERATIONS
// ==========================================

exports.submitDailyLog = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'staff') return res.status(403).send('Staff only');
    const data = req.body;
    data.authorStaffId = user.uid;
    data.date = admin.firestore.FieldValue.serverTimestamp();
    await db.collection('daily_class_logs').add(data);
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.createSyllabus = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'staff') return res.status(403).send('Staff only');

    const { title, weekNumber, topic, objectives, fileName } = req.body;
    
    const planRef = await db.collection('syllabus_plans').add({
      title, weekNumber, topic, objectives,
      authorStaffId: user.uid,
      status: 'active',
      createdAt: admin.firestore.FieldValue.serverTimestamp()
    });

    const bucket = storage.bucket(process.env.SYLLABUS_BUCKET);
    const file = bucket.file(`syllabus/${planRef.id}/${fileName}`);
    const [uploadUrl] = await file.getSignedUrl({
      action: 'write', expires: Date.now() + 15 * 60 * 1000, contentType: 'application/pdf',
    });

    await planRef.update({ contentPdfUrl: file.publicUrl() });
    res.json({ success: true, planId: planRef.id, uploadUrl });
  } catch (err) { res.status(500).send(err.message); }
};

exports.staffLeaveRequest = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'staff') return res.status(403).send('Staff only');
    await db.collection('staff_profiles').doc(user.uid).collection('leave_requests').add({
      ...req.body, status: 'pending', requestedAt: admin.firestore.FieldValue.serverTimestamp()
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.staffExitRequest = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'staff') return res.status(403).send('Staff only');
    await db.collection('staff_profiles').doc(user.uid).update({
      currentStatus: 'exit_requested',
      exitRequestDetails: { reason: req.body.reason, requestedAt: admin.firestore.FieldValue.serverTimestamp()
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.createIncidentReport = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'staff') return res.status(403).send('Staff only');
    const { targetType, targetIds, incidentData } = req.body; 

    let childrenToUpdate = [];
    if (targetType === 'single' || targetType === 'list') childrenToUpdate = targetIds;
    else if (targetType === 'class') {
      const snapshot = await db.collection('children').where('assignedClassroomId', '==', targetIds[0]).get();
      childrenToUpdate = snapshot.docs.map(doc => doc.id);
    }

    const batch = db.batch();
    childrenToUpdate.forEach(childId => {
      const ref = db.collection('children').doc(childId).collection('incidents').doc();
      batch.set(ref, { ...incidentData, authorStaffId: user.uid, timestamp: admin.firestore.FieldValue.serverTimestamp() });
    });
    await batch.commit();
    res.json({ success: true, count: childrenToUpdate.length });
  } catch (err) { res.status(500).send(err.message); }
};

exports.updateStudentDevelopment = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'staff') return res.status(403).send('Staff only');
    const { childId, developmentData } = req.body;
    await db.collection('children').doc(childId).collection('development_logs').add({
      ...developmentData, authorStaffId: user.uid, date: admin.firestore.FieldValue.serverTimestamp()
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.approveChildLeave = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'staff') return res.status(403).send('Staff only');
    const { childId, requestId, decision } = req.body;
    await db.collection('children').doc(childId).collection('leave_requests').doc(requestId).update({
      status: decision, staffActionBy: user.uid, actionDate: admin.firestore.FieldValue.serverTimestamp()
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

// ==========================================
// 2. PARENT OPERATIONS
// ==========================================

exports.submitEnrollment = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'parent') return res.status(403).send('Parents only');
    const data = req.body;
    data.submittedByParentUid = user.uid; 
    data.status = 'pending';
    data.submissionDate = admin.firestore.FieldValue.serverTimestamp();
    const ref = await db.collection('enrollment_forms').add(data);
    res.json({ success: true, formId: ref.id });
  } catch (err) { res.status(500).send(err.message); }
};

exports.updateChildDetails = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'parent') return res.status(403).send('Parents only');
    const { childId, extendedDetails } = req.body;
    const childRef = db.collection('children').doc(childId);
    if (!(await childRef.get()).data().parentUids.includes(user.uid)) return res.status(403).send('Unauthorized');
    await childRef.update({ extendedDetails });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.childLeaveRequest = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'parent') return res.status(403).send('Parents only');
    const { childId, note, date } = req.body;
    const childRef = db.collection('children').doc(childId);
    if (!(await childRef.get()).data().parentUids.includes(user.uid)) return res.status(403).send('Unauthorized');
    await childRef.collection('leave_requests').add({
      note, date, status: 'pending', requestedBy: user.uid, createdAt: admin.firestore.FieldValue.serverTimestamp()
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.childExitRequest = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'parent') return res.status(403).send('Parents only');
    const { childId, reason } = req.body;
    const childRef = db.collection('children').doc(childId);
    if (!(await childRef.get()).data().parentUids.includes(user.uid)) return res.status(403).send('Unauthorized');
    await childRef.update({
      status: 'exit_requested', exitDetails: { reason, requestedBy: user.uid, requestedAt: admin.firestore.FieldValue.serverTimestamp() }
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

// ==========================================
// 3. ADMIN OPERATIONS
// ==========================================

exports.approveEnrollment = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'admin') return res.status(403).send('Admins only');
    const { formId } = req.body;
    await db.collection('enrollment_forms').doc(formId).update({ 
      status: 'approved', adminActionDate: admin.firestore.FieldValue.serverTimestamp()
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.approveStaffLeave = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'admin') return res.status(403).send('Admins only');
    const { staffUid, requestId, decision } = req.body;
    await db.collection('staff_profiles').doc(staffUid).collection('leave_requests').doc(requestId).update({
      status: decision, adminDecidedBy: user.uid
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.approveStaffExit = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'admin') return res.status(403).send('Admins only');
    const { staffUid } = req.body;
    await db.collection('staff_profiles').doc(staffUid).update({
      currentStatus: 'exited', 'exitRequestDetails.approvedBy': user.uid
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

exports.approveChildExit = async (req, res) => {
  try {
    const user = await getAuthenticatedUser(req);
    if (user.role !== 'admin') return res.status(403).send('Admins only');
    const { childId } = req.body;
    await db.collection('children').doc(childId).update({
      status: 'graduated', 'exitDetails.adminFinalizedDate': admin.firestore.FieldValue.serverTimestamp()
    });
    res.json({ success: true });
  } catch (err) { res.status(500).send(err.message); }
};

Step 4: Deploy Cloud Functions

Deploy functions privately using the Function Service Account.

# Common flags for all deployments
FLAGS="--runtime nodejs18 --trigger-http --no-allow-unauthenticated --service-account=$FUNC_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com --region=$REGION"

# 1. Staff Functions
gcloud functions deploy submitDailyLog $FLAGS
gcloud functions deploy createSyllabus $FLAGS --set-env-vars SYLLABUS_BUCKET=$SYLLABUS_BUCKET
gcloud functions deploy staffLeaveRequest $FLAGS
gcloud functions deploy staffExitRequest $FLAGS
gcloud functions deploy createIncidentReport $FLAGS
gcloud functions deploy updateStudentDevelopment $FLAGS
gcloud functions deploy approveChildLeave $FLAGS

# 2. Parent Functions
gcloud functions deploy submitEnrollment $FLAGS
gcloud functions deploy updateChildDetails $FLAGS
gcloud functions deploy childLeaveRequest $FLAGS
gcloud functions deploy childExitRequest $FLAGS

# 3. Admin Functions
gcloud functions deploy approveEnrollment $FLAGS
gcloud functions deploy approveStaffLeave $FLAGS
gcloud functions deploy approveStaffExit $FLAGS
gcloud functions deploy approveChildExit $FLAGS

Step 5: Grant Gateway Permissions

# List of all function names
FUNCTIONS=("submitDailyLog" "createSyllabus" "staffLeaveRequest" "staffExitRequest" "createIncidentReport" "updateStudentDevelopment" "approveChildLeave" "submitEnrollment" "updateChildDetails" "childLeaveRequest" "childExitRequest" "approveEnrollment" "approveStaffLeave" "approveStaffExit" "approveChildExit")

# Loop to grant permissions
for FUNC in "${FUNCTIONS[@]}"
do
  gcloud functions add-iam-policy-binding $FUNC \
    --region=$REGION \
    --member="serviceAccount:$GATEWAY_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/cloudfunctions.invoker"
done

Step 6: Configure & Deploy API Gateway

1. Create api-spec.yaml

swagger: '2.0'
info:
  title: DayCare API
  description: Secure API for DayCare Mobile App
  version: 1.0.0
schemes:
  - https
produces:
  - application/json
security:
  - firebase: []

# Firebase Auth Configuration
securityDefinitions:
  firebase:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    x-google-issuer: "https://securetoken.google.com/[PROJECT_ID]"
    x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com"
    x-google-audiences: "[PROJECT_ID]"

paths:
  # --- PARENT ENDPOINTS ---
  /enrollment/submit:
    post:
      summary: Parent submits enrollment
      operationId: submitEnrollment
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/submitEnrollment
      responses: { '200': { description: Success } }

  /parent/child-details:
    put:
      summary: Update child extended details
      operationId: updateChildDetails
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/updateChildDetails
      responses: { '200': { description: Success } }

  /parent/child-leave:
    post:
      summary: Submit absence/leave note
      operationId: childLeaveRequest
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/childLeaveRequest
      responses: { '200': { description: Success } }

  /parent/child-exit:
    post:
      summary: Request child exit
      operationId: childExitRequest
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/childExitRequest
      responses: { '200': { description: Success } }

  # --- STAFF ENDPOINTS ---
  /staff/daily-log:
    post:
      summary: Staff submits daily log
      operationId: submitDailyLog
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/submitDailyLog
      responses: { '200': { description: Success } }

  /staff/syllabus:
    post:
      summary: Create syllabus & get upload URL
      operationId: createSyllabus
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/createSyllabus
      responses: { '200': { description: Success } }

  /staff/leave:
    post:
      summary: Raise staff leave
      operationId: staffLeaveRequest
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/staffLeaveRequest
      responses: { '200': { description: Success } }

  /staff/exit:
    post:
      summary: Raise staff exit
      operationId: staffExitRequest
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/staffExitRequest
      responses: { '200': { description: Success } }

  /staff/incident:
    post:
      summary: Report incident
      operationId: createIncidentReport
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/createIncidentReport
      responses: { '200': { description: Success } }

  /staff/student-dev:
    post:
      summary: Update student development
      operationId: updateStudentDevelopment
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/updateStudentDevelopment
      responses: { '200': { description: Success } }

  /staff/approve-child-leave:
    post:
      summary: Approve child leave request
      operationId: approveChildLeave
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveChildLeave
      responses: { '200': { description: Success } }

  # --- ADMIN ENDPOINTS ---
  /admin/enrollment/approve:
    post:
      summary: Admin approves enrollment
      operationId: approveEnrollment
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveEnrollment
      responses: { '200': { description: Success } }

  /admin/approve-staff-leave:
    post:
      summary: Approve staff leave
      operationId: approveStaffLeave
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveStaffLeave
      responses: { '200': { description: Success } }

  /admin/approve-staff-exit:
    post:
      summary: Approve staff exit
      operationId: approveStaffExit
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveStaffExit
      responses: { '200': { description: Success } }

  /admin/approve-child-exit:
    post:
      summary: Approve child exit
      operationId: approveChildExit
      x-google-backend:
        address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveChildExit
      responses: { '200': { description: Success } }

2. Deploy the Gateway

# 1. Create API Config
gcloud api-gateway api-configs create daycare-config-final \
  --api=daycare-api \
  --openapi-spec=api-spec.yaml \
  --project=$PROJECT_ID \
  --backend-auth-service-account="$GATEWAY_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com"

# 2. Create/Update Gateway
gcloud api-gateway gateways create daycare-gateway \
  --api=daycare-api \
  --api-config=daycare-config-final \
  --location=$REGION \
  --project=$PROJECT_ID

# 3. Get Your API URL
gcloud api-gateway gateways describe daycare-gateway \
  --location=$REGION \
  --format="get(defaultHostname)"

Final Usage

  1. Mobile App: Signs user in via Firebase Auth -> Gets ID Token.
  2. Request: App sends request to https://[GATEWAY_URL]/staff/syllabus with header Authorization: Bearer [FIREBASE_ID_TOKEN].
  3. Result: Secure, role-validated execution of your backend logic.