chore: relocate backend into apps/api ahead of monorepo merge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:20:40 +08:00
co-authored by Claude Sonnet 5
parent af44598df6
commit eaa6d2276a
388 changed files with 0 additions and 13998 deletions
+42
View File
@@ -0,0 +1,42 @@
/**
* One-time backfill: recompute duration_seconds for every lesson
* that has at least one saved page block.
*
* Run from the project root:
* node scripts/backfill-durations.js
*/
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '../.env') });
const sequelize = require('../config/db.config');
const LessonPage = require('../models/courses/lesson_page.mdl');
const { recomputeDurations } = require('../utils/duration.util');
(async () => {
await sequelize.authenticate();
console.log('DB connected.\n');
const pages = await LessonPage.findAll({
attributes: ['lesson_id', 'blocks'],
where: sequelize.literal(`jsonb_array_length(blocks::jsonb) > 0`),
});
console.log(`Found ${pages.length} lessons with blocks. Recomputing...`);
let ok = 0, fail = 0;
for (const page of pages) {
try {
await recomputeDurations(page.lesson_id);
process.stdout.write('.');
ok++;
} catch (err) {
process.stdout.write('✗');
console.error(`\n lesson ${page.lesson_id}: ${err.message}`);
fail++;
}
}
console.log(`\n\nDone — ${ok} recomputed, ${fail} failed.`);
await sequelize.close();
})();
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh
# Wraps the Garage daemon: starts it in the background, runs one-time cluster
# initialization using the local RPC connection, then hands control back to
# the daemon process. Safe to re-run — all operations are idempotent.
set -e
garage server &
DAEMON_PID=$!
echo "Waiting for Garage to be ready..."
until garage status > /dev/null 2>&1; do
sleep 2
done
echo "Garage is up. Running cluster initialization..."
NODE_ID=$(garage node id 2>/dev/null | head -1 | awk '{print $1}')
garage layout assign "$NODE_ID" -z dc1 -c 100G 2>/dev/null || true
garage layout apply --version 1 2>/dev/null || true
garage bucket create "$S3_BUCKET" 2>/dev/null || true
# --yes skips the interactive confirmation prompt.
garage key import "$S3_ACCESS_KEY" "$S3_SECRET_KEY" -n starr-app --yes 2>/dev/null || true
garage bucket allow "$S3_BUCKET" --read --write --owner --key "$S3_ACCESS_KEY" 2>/dev/null || true
# Flag file read by the healthcheck — ensures the backend waits for full init.
touch /var/lib/garage/meta/.ready
echo "Garage init complete."
wait $DAEMON_PID
@@ -0,0 +1,79 @@
/***********************************************************************************************************************************************************************
* File Name: get_gmail_refresh_token.js
* Type of Program: One-time setup script (run locally, not deployed)
* Description: Mints a long-lived Gmail API refresh token for the sending mailbox
* (EMAIL_FROM, e.g. services.philpro@gmail.com), so services/email.service.js
* can send mail over HTTPS via the Gmail API instead of SMTP.
*
* Prerequisites (one-time, in Google Cloud Console — same project as GOOGLE_CLIENT_ID):
* 1. APIs & Services → Library → enable "Gmail API".
* 2. APIs & Services → Credentials → open the OAuth client used for GOOGLE_CLIENT_ID
* → Authorized redirect URIs → add: http://localhost:5555/oauth2callback
* (You can remove this URI again after this script succeeds.)
*
* Usage:
* node scripts/get_gmail_refresh_token.js
* → prints an auth URL. Open it in a browser, sign in AS the EMAIL_FROM mailbox,
* approve the "Send email on your behalf" consent screen.
* → the script prints GMAIL_REFRESH_TOKEN — copy it into Render's env vars.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jul. 14, 2026
***********************************************************************************************************************************************************************/
'use strict';
require('dotenv').config();
const http = require('http');
const { OAuth2Client } = require('google-auth-library');
const REDIRECT_URI = 'http://localhost:5555/oauth2callback';
const SCOPE = 'https://www.googleapis.com/auth/gmail.send';
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
if (!clientId || !clientSecret) {
console.error('Missing GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET in .env');
process.exit(1);
}
const client = new OAuth2Client(clientId, clientSecret, REDIRECT_URI);
const authUrl = client.generateAuthUrl({
access_type: 'offline',
prompt: 'consent', // forces a refresh_token even if this account consented before
scope: [SCOPE],
});
console.log('\nOpen this URL, sign in AS the EMAIL_FROM mailbox, and approve access:\n');
console.log(authUrl, '\n');
console.log(`Waiting for the redirect on ${REDIRECT_URI} ...\n`);
const server = http.createServer(async (req, res) => {
if (!req.url.startsWith('/oauth2callback')) {
res.writeHead(404).end();
return;
}
const code = new URL(req.url, REDIRECT_URI).searchParams.get('code');
if (!code) {
res.writeHead(400).end('Missing ?code — check the URL Google redirected you to.');
return;
}
try {
const { tokens } = await client.getToken(code);
res.writeHead(200, { 'Content-Type': 'text/plain' }).end('Done — check your terminal.');
console.log('GMAIL_REFRESH_TOKEN=' + tokens.refresh_token, '\n');
console.log('Copy the line above into Render\'s environment variables, then redeploy.');
} catch (err) {
res.writeHead(500).end('Token exchange failed — see terminal.');
console.error('Token exchange failed:', err.response?.data || err.message);
} finally {
server.close();
}
});
server.listen(5555);