Skip to content
Merged

Ci #14

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches: [main, dev]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
checks:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Format check
run: npm run format:check

- name: Type check
run: npm run typecheck

- name: Test
run: npm test
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"js/ts.tsdk.path": "node_modules/typescript/lib"
}
"js/ts.tsdk.path": "node_modules/typescript/lib"
}
45 changes: 26 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![CI](https://github.com/aga87/student-progress-api/actions/workflows/ci.yml/badge.svg)](https://github.com/aga87/student-progress-api/actions)

# Student Progress API

Backend service for managing student results, built with MySQL on Google Cloud SQL and Redis for caching. Infrastructure is provisioned using Terraform.
Expand All @@ -22,8 +24,8 @@ Implements a cache-aside strategy with explicit cache invalidation on writes and
- Express – REST API layer
- Google Cloud SQL - Database
- Google Cloud Run – serverless container platform
- Redis – caching layer
- Docker – local containerised Redis development
- Redis – caching layer
- Docker – local containerised Redis development
- Terraform – infrastructure provisioning

## Architecture
Expand All @@ -48,6 +50,7 @@ Cloud Run accesses Redis using Direct VPC egress (`private-ranges-only`).
- No database passwords are stored or used in the application

Local:

```
Client → Node.js app (Express)
Expand All @@ -63,6 +66,7 @@ Client → Node.js app (Express)
```

Production:

```
Client → Cloud Run
Expand All @@ -73,14 +77,12 @@ Client → Cloud Run

**Database access** uses IAM database authentication:


```txt
Local dev → individual IAM DB user
Cloud Run app → service account IAM DB user
Admin tasks → separate admin user / controlled IAM access
```


### API Scope

This project intentionally implements a small set of representative endpoints rather than a complete CRUD API.
Expand All @@ -92,15 +94,15 @@ Authentication is intentionally not implemented.

In a production system, authentication would be introduced at the HTTP boundary via Express middleware. Typical approaches include:

- JWT-based authentication (access + refresh tokens, stateless verification, token rotation)
- External identity providers using OAuth2 / OpenID Connect
- JWT-based authentication (access + refresh tokens, stateless verification, token rotation)
- External identity providers using OAuth2 / OpenID Connect

For GCP-based deployments, this service is designed to integrate with platform-native solutions such as:

- Cloud Run IAM authentication for service-to-service communication
- Identity-Aware Proxy (IAP) for user-level access control without embedding auth logic in the application

## Project structure
## Project structure

```
src/ → application code
Expand All @@ -109,6 +111,10 @@ sql/ → raw SQL (schema + seed)
infra/ → Terraform infrastructure configuration
```

## Prerequisites

- Node.js (see `.nvmrc` or `mise.toml` for the required version)

## Infrastructure (Terraform)

Infrastructure is provisioned using Terraform.
Expand Down Expand Up @@ -138,10 +144,10 @@ Run once per environment.
The Cloud SQL instance, database, Secret Manager containers, and IAM-based application user are provisioned via Terraform.
Credential values (such as the admin/root password) are configured separately to avoid storing secrets in Terraform state.

### 1. **Create a new database admin user**
### 1. **Create a new database admin user**

The ‘root’@’%’ user is the default and most popular super user and therefore is often targeted by hackers. Creating a new admin user is the best security practice.

1. Store password in Secret Manager first:

```bash
Expand Down Expand Up @@ -173,13 +179,15 @@ gcloud sql users delete root \
gcloud sql instances describe student-progress-mysql-staging \
--format='value(connectionName)'
```

2. Run proxy **without** IAM auth

```shell
cloud-sql-proxy student-progress-staging:europe-west3:student-progress-mysql-staging --port 3306
```

3. Connect as admin user:

```shell
mysql -h 127.0.0.1 -P 3306 -u admin-user \
-p"$(gcloud secrets versions access latest --secret=staging-db-admin-password)"
Expand All @@ -196,14 +204,16 @@ TO 'student-progress-app-sa'@'%';
5. Reconnect as IAM user to test database privileges and the production service account identity locally.

Allow a developer (or CI) to impersonate the application service account:

```bash
gcloud iam service-accounts add-iam-policy-binding \
student-progress-app-sa@student-progress-staging.iam.gserviceaccount.com \
--member="user:<YOUR_EMAIL>" \
--role="roles/iam.serviceAccountTokenCreator"
```

Run the proxy while impersonating the service account:
Run the proxy while impersonating the service account:

```bash
cloud-sql-proxy \
--auto-iam-authn \
Expand Down Expand Up @@ -264,7 +274,8 @@ gcloud run deploy student-progress-api \
--vpc-egress private-ranges-only \
--allow-unauthenticated \
--set-env-vars "NODE_ENV=production,DB_CONNECTION_TYPE=cloud-sql-iam,DB_INSTANCE_CONNECTION_NAME=student-progress-staging:europe-west3:student-progress-mysql-staging,DB_USER=student-progress-app-sa,DB_NAME=student_progress,REDIS_HOST=<REDIS_HOST>,REDIS_PORT=6379,REDIS_TTL_SECONDS=60"
```
```

Note: Replace `<REDIS_HOST>` with the Memorystore private IP.

## One-off Local Development Setup
Expand Down Expand Up @@ -312,20 +323,19 @@ Note: For Cloud SQL MySQL IAM users, the MySQL username is shortened.
Example:

```txt
IAM email: dev-user@example.com
IAM email: dev-user@example.com
MySQL user: dev-user
```

4. Update `DB_USER` env var - also shorthand.

4. Update `DB_USER` env var - also shorthand.

5. Authenticate locally

```bash
gcloud auth application-default login
```

6. Start proxy*
6. Start proxy\*

```shell
npm run dev:proxy
Expand All @@ -337,7 +347,6 @@ npm run dev:proxy
npm run db:test
```


### 3. **Init Redis container**

```shell
Expand All @@ -352,20 +361,18 @@ docker stop student-progress-redis

Future development startup will restart Redis automatically via the dev script.


## Local Development

```shell
npm run dev
```

This will:

- start the Redis Docker container
- start the Cloud SQL Auth Proxy
- start the application in watch mode



## Database Workflow

Local database setup is fully script-driven.
Expand Down
43 changes: 43 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';

export default tseslint.config(
{
ignores: ['dist/**', 'build/**', 'coverage/**'],
},

js.configs.recommended,

// JS / config files (no type-aware linting)
{
files: ['**/*.js', '**/*.mjs', '**/*.cjs'],
languageOptions: {
sourceType: 'module',
},
},

// TS files (type-aware)
{
files: ['**/*.ts', '**/*.tsx'],
extends: [...tseslint.configs.recommendedTypeChecked],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},

rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
},
},

prettier
);
2 changes: 2 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
node = "22"
Loading