mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
testing 127.0.0.1 issue
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -12,6 +12,9 @@
|
||||
* Derivation rules:
|
||||
* unit → completed when ALL its non-deleted lessons have a completed row for this user
|
||||
* course → completed when ALL its non-deleted units have a completed row for this user
|
||||
* AND, if the course has a course assessment, the user has passed it.
|
||||
* A course with no assessment built yet can never reach 'completed' here —
|
||||
* reading alone isn't course completion.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 21, 2026
|
||||
@@ -22,6 +25,24 @@ const sequelize = require('../config/db.config');
|
||||
const CourseReadingProgress = require('../models/courses/course_reading_progress.mdl');
|
||||
const Lesson = require('../models/courses/lessons.mdl');
|
||||
const Unit = require('../models/courses/units.mdl');
|
||||
const CourseAssessment = require('../models/courses/course_assessment.mdl');
|
||||
const QuizAttempt = require('../models/courses/quiz_attempt.mdl');
|
||||
|
||||
// A course only counts as fully complete once it has a built assessment AND the user passed it.
|
||||
async function hasPassedCourseAssessment(userId, courseId, t) {
|
||||
const assessment = await CourseAssessment.findOne({
|
||||
where: { course_id: courseId },
|
||||
attributes: ['assessment_id'],
|
||||
transaction: t,
|
||||
});
|
||||
if (!assessment) return false;
|
||||
|
||||
const passedAttempt = await QuizAttempt.findOne({
|
||||
where: { user_id: userId, assessment_id: assessment.assessment_id, passed: true },
|
||||
transaction: t,
|
||||
});
|
||||
return !!passedAttempt;
|
||||
}
|
||||
|
||||
// ─── Core UPSERT ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -74,7 +95,8 @@ async function deriveUnitStatus(userId, courseId, unitId, t) {
|
||||
return completedCount === lessons.length ? 'completed' : 'in_progress';
|
||||
}
|
||||
|
||||
// Course is completed when every non-deleted unit under it has a completed row for this user.
|
||||
// Course is completed when every non-deleted unit under it has a completed row for this user
|
||||
// AND the course's assessment (if one has been built) has been passed by this user.
|
||||
async function deriveCourseStatus(userId, courseId, t) {
|
||||
const units = await Unit.findAll({
|
||||
where: { course_id: courseId },
|
||||
@@ -95,7 +117,11 @@ async function deriveCourseStatus(userId, courseId, t) {
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
return completedCount === units.length ? 'completed' : 'in_progress';
|
||||
const allUnitsRead = completedCount === units.length;
|
||||
if (!allUnitsRead) return 'in_progress';
|
||||
|
||||
const assessmentPassed = await hasPassedCourseAssessment(userId, courseId, t);
|
||||
return assessmentPassed ? 'completed' : 'in_progress';
|
||||
}
|
||||
|
||||
// ─── Main entry point ─────────────────────────────────────────────────────────
|
||||
|
||||
+13
-56
@@ -46,64 +46,21 @@ const DEFAULT_BUCKET = process.env.S3_BUCKET;
|
||||
// from the machine running Garage itself; S3_PUBLIC_URL is the externally
|
||||
// reachable address (tunnel/CDN/domain).
|
||||
//
|
||||
// Rather than always preferring one, probe S3_ENDPOINT and use it when it's
|
||||
// actually reachable (same-machine dev setup — no extra hop through the
|
||||
// tunnel), falling back to S3_PUBLIC_URL when it isn't (any other machine).
|
||||
//
|
||||
// The probe runs once at startup and then on a background timer — never on
|
||||
// the request path itself. A machine without Garage would otherwise pay the
|
||||
// full HeadBucket timeout on whichever upload/asset request happens to land
|
||||
// right after the cache expires; polling in the background means every
|
||||
// request just reads the last known-good host instantly.
|
||||
const PROBE_TIMEOUT_MS = 1500;
|
||||
const PROBE_CACHE_MS = 15000;
|
||||
|
||||
let hostCache = { host: process.env.S3_PUBLIC_URL || process.env.S3_ENDPOINT || "" };
|
||||
|
||||
async function probeEndpoint(endpoint) {
|
||||
const probe = new S3Client({
|
||||
endpoint,
|
||||
region: process.env.S3_REGION || "garage",
|
||||
credentials,
|
||||
forcePathStyle: true,
|
||||
});
|
||||
await Promise.race([
|
||||
probe.send(new HeadBucketCommand({ Bucket: DEFAULT_BUCKET })),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), PROBE_TIMEOUT_MS)),
|
||||
]);
|
||||
// This used to probe S3_ENDPOINT from the server and prefer it when reachable,
|
||||
// but that measures the wrong machine: Garage is always co-located with this
|
||||
// backend (see docker-compose.yml), so the probe was *always* reachable from
|
||||
// here and always resolved to 127.0.0.1 — even for browsers on other machines,
|
||||
// which then failed to connect to it. There is no way for the server to
|
||||
// determine what's reachable from the client by probing itself, so just trust
|
||||
// config: prefer S3_PUBLIC_URL whenever it's set, and only fall back to
|
||||
// S3_ENDPOINT for pure single-machine dev setups with no public URL at all.
|
||||
function resolvePublicHost() {
|
||||
return process.env.S3_PUBLIC_URL || process.env.S3_ENDPOINT || "";
|
||||
}
|
||||
|
||||
async function refreshHostCache() {
|
||||
const endpoint = process.env.S3_ENDPOINT;
|
||||
const publicUrl = process.env.S3_PUBLIC_URL || "";
|
||||
|
||||
if (!endpoint) { hostCache = { host: publicUrl }; return; }
|
||||
if (!publicUrl) { hostCache = { host: endpoint }; return; }
|
||||
|
||||
try {
|
||||
await probeEndpoint(endpoint);
|
||||
hostCache = { host: endpoint };
|
||||
} catch {
|
||||
hostCache = { host: publicUrl };
|
||||
}
|
||||
}
|
||||
|
||||
// Kick off the first probe immediately so the cache is populated before any
|
||||
// request needs it, then keep it fresh in the background. unref() so this
|
||||
// timer alone doesn't keep the process (or a test run) alive.
|
||||
const initialProbe = refreshHostCache();
|
||||
const refreshTimer = setInterval(refreshHostCache, PROBE_CACHE_MS);
|
||||
refreshTimer.unref?.();
|
||||
|
||||
async function resolvePublicHost() {
|
||||
await initialProbe; // no-op after the first call — already resolved
|
||||
return hostCache.host;
|
||||
}
|
||||
|
||||
// Public client — lazily built against whichever host resolvePublicHost()
|
||||
// picks, so it follows the reachability check instead of a fixed endpoint.
|
||||
async function getPublicClient() {
|
||||
const endpoint = await resolvePublicHost();
|
||||
// Public client — built against whichever host resolvePublicHost() picks.
|
||||
function getPublicClient() {
|
||||
const endpoint = resolvePublicHost();
|
||||
return new S3Client({
|
||||
endpoint,
|
||||
region: process.env.S3_REGION || "garage",
|
||||
|
||||
@@ -6,9 +6,14 @@
|
||||
* OTP; trust rolls forward 30 days on each trusted login and is
|
||||
* tied to both an opaque cookie token (device_trust) and a
|
||||
* User-Agent fingerprint, so a stolen cookie alone isn't enough
|
||||
* once the fingerprint no longer matches. Trust is revoked on
|
||||
* logout, password change/reset, admin ban/deactivate, or a
|
||||
* single session being terminated.
|
||||
* once the fingerprint no longer matches. Ordinary logout does
|
||||
* NOT revoke trust or clear the device_trust cookie — expires_at
|
||||
* is the only thing that ends the OTP-skip window in the normal
|
||||
* case, so logging out and back in on the same device still
|
||||
* skips OTP until the 30-day window actually lapses. Trust is
|
||||
* only force-revoked by password change/reset, admin ban/
|
||||
* deactivate/force-logout, or a single session being explicitly
|
||||
* terminated.
|
||||
* Author: Kenneth Obsequio
|
||||
* Date Created: Jul. 5, 2026
|
||||
***********************************************************************************************************************************************************************
|
||||
@@ -103,6 +108,12 @@ const issueOrRefresh = async (res, userId, fingerprintHash, sessionId) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Revokes trust for one specific device by its cookie token. Not called by
|
||||
* the normal logout flow (see auth.controller.js exports.logout) — ordinary
|
||||
* logout intentionally leaves trust intact. Kept as a primitive for a
|
||||
* future explicit "forget this device" action, should one be added.
|
||||
*/
|
||||
const revokeByToken = async (userId, rawToken) => {
|
||||
if (!rawToken) return;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user