Skip to content
Merged
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
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A student learning workspace with original practice questions, explanations, and
</div>

> [!NOTE]
> This is an early student-product foundation. Four practice topics contain 24 original multiple-choice questions. Practice and browser-saved progress work without login. An optional ASP.NET Core backend provides Google sign-in and account progress when configured. Billing and checkout are not enabled. Course notes, guides, and job listings are still awaiting published content.
> This is an early student-product foundation. Four practice topics contain 24 original multiple-choice questions. Practice and browser-saved progress work without login. An optional ASP.NET Core backend provides Google sign-in, account progress, and private portfolio drafts. The portfolio editor, two themes, Markdown import, and public-page support are implemented; public publishing is off by default pending deployment configuration. Billing and checkout are not enabled. Course notes, guides, and job listings are awaiting published content.

![Guest learning workspace before the first session](docs/images/learning-workspace.png)

Expand Down Expand Up @@ -49,16 +49,25 @@ their answers. Run the app locally to explore the implemented routes:
| `/practice/[topic]` | Multiple-choice practice sessions with scores, correct answers, and explanations. |
| `/games` | Pattern Sprint and Digit Recall: short reasoning and memory exercises. |
| `/account` | Optional Google sign-in and cloud progress controls when the backend is configured. |
| `/learn/portfolio` | In-memory guest preview; signed-in private drafts, Markdown import, theme choice, and explicit publication when enabled. |
| `/u/[username]` | A student's published snapshot; drafts, suspended or withdrawn pages return 404. |
| `/dashboard` | Engineering course, branch, and semester navigation for future reviewed notes. |

Questions are original learning exercises. They are independent of employer assessments and do
not predict or guarantee hiring outcomes. The first question bank is intentionally small so it
can be reviewed and improved before expanding.

The expanded [placement-game catalogue](https://github.com/procoderji-com/procoderji/issues/24)
and [student portfolios](https://github.com/procoderji-com/procoderji/issues/40) are tracked as
separate work. Check those issues for delivery status; the table above describes the current
student experience.
remains separate work. For portfolios, start with the [student journey and deployment
runbook](docs/portfolio-workspace.md): guests can try the form, Google accounts save drafts,
and students aged 18 or older can explicitly accept the publishing policy. Usernames stay
fixed while the portfolio exists; draft edits remain private until published again. Search
indexing is opt-in. Import the [Markdown template](public/templates/portfolio.md) or fill the
same fields directly; both Simple and Developer themes use that model.

Repository features do not establish that the live site has been updated.
[Issue #74](https://github.com/procoderji-com/procoderji/issues/74) tracks the outstanding
production hosting work; see the [deployment guide](docs/deployment.md).

## Progress and optional accounts

Expand Down Expand Up @@ -86,6 +95,7 @@ does not currently enable them.
| --- | --- |
| Placement practice | 24 questions across four topics, explanations, and saved session history. |
| Short games | Pattern Sprint and Digit Recall are playable without login; their scores are not saved. |
| Student portfolios | Form/live preview, Simple and Developer themes, Markdown import, authenticated drafts, and gated public pages with moderation. |
| Course catalogue | Course, branch, and semester navigation exists; no subjects or notes are published yet. |
| Guides | No reviewed guides published yet. |
| Jobs and internships | No reviewed listings published yet. |
Expand Down Expand Up @@ -131,7 +141,7 @@ troubleshooting, [backend guide](backend/README.md) for accounts, and
| `npm run dev` | Start the local development server. |
| `npm run lint` | Run ESLint. |
| `npm run typecheck` | Check TypeScript types. |
| `npm test` | Run unit tests, including practice-domain, progress-model/store, and content-validation tests. |
| `npm test` | Run practice/progress, portfolio rules/import/client, content-validation and helper tests. |
| `npm run build` | Create the production build. |
| `npm run e2e` | Build with synthetic content and check frontend browser behavior; opt into the backend with `E2E_DOTNET=1`. |
| `npm run launch-check` | Build the real site and crawl links, sitemap, metadata, and SEO rules. |
Expand All @@ -140,7 +150,7 @@ troubleshooting, [backend guide](backend/README.md) for accounts, and
| CI job | Coverage |
| --- | --- |
| Lint, typecheck, tests and build | TypeScript, code quality, domain/store tests, and the production build. |
| Browser and SEO checks | Desktop/mobile flows, a real .NET process and frontend proxy, the Google sign-in challenge, and a crawl of links/metadata. |
| Browser and SEO checks | Desktop/mobile flows, test-only .NET host using the real API/BFF, portfolio publication/moderation, Google challenge, and links/metadata. |
| Backend build and tests | The .NET solution build and tests for backend rules and access behavior. |
| Database contracts and container smoke | PostgreSQL, SQL Server and MongoDB contracts, Compose configuration, and the packaged API image. |
| Dependency review | Pull-request dependency changes checked for newly introduced high or critical vulnerabilities. |
Expand All @@ -149,7 +159,9 @@ These jobs need no Google credentials or other external secrets. Browser screens
retained for seven days when the site-quality job fails. A deployed Google sign-in flow still
needs the configured-provider checks in [Backend setup](backend/README.md). Browser CI verifies
the challenge and public callback URL with test client configuration; it does not complete real
Google consent or token exchange.
Google consent or token exchange. Full-stack portfolio checks use disposable accounts and
loopback-only fixtures in `backend/tests/ProCoderJi.BrowserHost`, which is excluded from the
production API image. The production API has no fixture login or moderation HTTP endpoint.

The E2E runner refuses to run when `src/data` or `src/content` has local changes because it
temporarily installs synthetic fixtures. See [Development](docs/development.md#checks).
Expand All @@ -161,7 +173,7 @@ src/app/ Next.js routes, metadata, and backend-for-frontend API
src/features/practice/ Pure practice rules, question-bank adapter, and practice UI
src/features/progress/ Validated attempt records and browser progress storage
src/features/account/ Optional account-provider clients and controls
src/features/portfolio/ Shared-rule validation for portfolio fields and usernames
src/features/portfolio/ Portfolio rules, editor, Markdown import, themes, and public delivery
src/components/ Shared UI and landing-page sections
src/content/ Markdown and MDX learning content
src/data/ Course, guide, and job catalogues
Expand Down Expand Up @@ -193,7 +205,7 @@ The [maintainer guide](docs/maintainer-guide.md) explains triage, review and dep
| Saved progress behavior | [Progress feature](src/features/progress/) and its model/store tests. |
| Student interface and accessibility | [Practice views](src/features/practice/client/) and shared components. |
| Optional accounts, synchronization, or persistence | [Backend setup](backend/README.md), [account feature](src/features/account/), and the Next.js `/api/platform/*` proxy. |
| Portfolio field and username validation | [Shared rules](shared/portfolio/README.md) and the [portfolio roadmap](https://github.com/procoderji-com/procoderji/issues/40). |
| Portfolio editor, themes, import, or publication | [Workspace guide](docs/portfolio-workspace.md), [shared rules](shared/portfolio/README.md), and [API contract](docs/portfolio-api.md). |
| Original notes, guides, and listings | [Content guide](docs/content-guide.md). |
| Setup and contributor guidance | `docs/` and the [contribution guide](CONTRIBUTING.md). |

Expand Down
1 change: 1 addition & 0 deletions backend/ProCoderJi.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ProCoderJi.Api.Tests/ProCoderJi.Api.Tests.csproj" />
<Project Path="tests/ProCoderJi.BrowserHost/ProCoderJi.BrowserHost.csproj" />
</Folder>
</Solution>
74 changes: 68 additions & 6 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# ProCoderJii account and progress API
# ProCoderJii account, progress and portfolio API

ASP.NET Core 10 provides Google sign-in, server-managed cookie sessions, and durable learner progress. The Next.js app forwards `/api/platform/*` to this service through its server-only BFF. Anonymous browser practice stays available when the backend or Google credentials are not configured.
ASP.NET Core 10 provides Google sign-in, server-managed cookie sessions, durable learner progress,
and portfolio drafts/publication. Next.js forwards `/api/platform/*` through its server-only
BFF. Guest practice and the in-memory portfolio preview work without backend credentials.

## Run locally

Expand Down Expand Up @@ -39,8 +41,12 @@ Users are linked by Google's stable provider subject, never by matching email ad
## API contract

Portfolio storage and authenticated/public endpoints are documented in the
[portfolio API guide](../docs/portfolio-api.md). Public publishing is disabled by default until
the owner decisions and launch work in #47/#48 are complete.
[portfolio API guide](../docs/portfolio-api.md). The editor, two themes, Markdown import,
moderation and 18+ publishing policy are implemented. `Portfolio__PublishingEnabled` remains
false by default; use the [workspace launch runbook](../docs/portfolio-workspace.md) before
enabling it. Frontend production hosting remains separate work under
[#74](https://github.com/procoderji-com/procoderji/issues/74); this guide does not establish a
deployed production service.

| Endpoint | Behavior |
| --- | --- |
Expand Down Expand Up @@ -101,8 +107,9 @@ dotnet run --project backend/src/ProCoderJi.Api --no-launch-profile -- --migrate
```

This command uses the current environment's database/provider settings and exits after
provisioning. It applies EF migrations for relational databases and creates MongoDB's unique
Google-subject index for MongoDB. Development and Testing provision automatically; production
provisioning. It applies EF migrations, including portfolio publishing-policy fields, for
relational databases and creates MongoDB's Google-subject and portfolio-username indexes.
Development and Testing provision automatically; production
does so only for `--migrate` or explicit `Database__MigrateOnStartup=true`. Back up the database
before schema changes. Shared relational migrations contain provider-specific SQL where needed;
keep both the default and SQL Server model snapshots current and run the provider checks.
Expand All @@ -120,6 +127,53 @@ replica set or multi-document transactions. See [ADR 0003](../docs/decisions/000

Razorpay or Paytm integration is planned for a later billing slice. No payment, subscription activation, or live checkout is implemented in this API yet.

## Portfolio moderation

Each public portfolio's **Report this page** link opens an email to `procoderji@gmail.com` with
its canonical address. Operators review reports and can run these commands using the deployed
API's database environment and authorised database credentials:

```bash
dotnet ProCoderJi.Api.dll --portfolio suspend --username riya-student
dotnet ProCoderJi.Api.dll --portfolio restore --username riya-student
```

For a local development database, use
`dotnet run --project backend/src/ProCoderJi.Api --no-launch-profile -- --portfolio suspend --username riya-student`.
Run `--migrate` separately before these commands when the schema needs updating. Moderation
does not start an HTTP server or migrate a database. It prints only username, status and new
revision, excluding student content and provider connection details. Exit codes are 0 success
(including an already-applied state), 2 invalid arguments, 3 missing portfolio, 1 operation
failure, and 130 cancellation. `--portfolio help` prints syntax.

Suspend removes the public snapshot, search visibility and publishing acceptance while
retaining the owner's private draft. Owners may edit/unpublish, but cannot publish or delete
to bypass suspension. Restore returns a private **draft**, not a live page: the student must
review and explicitly accept the current policy and publish again. The CLI uses revision/status
checks and supports relational providers and MongoDB. There is no production moderation HTTP
endpoint or student-supplied admin flag.

Next.js checks fresh publication state before serving cached snapshots, so suspension becomes
404 on the next request without a CLI cache-invalidation callback. Verify the public URL and
sitemap after changes. Keep report decisions and operator access in the deployment's operating
process. Requests to remove a suspended portfolio or an account go to the operator contact;
practice-history deletion is separate.

## Portfolio publishing configuration

Phase 1 fixes usernames while the portfolio exists and requires 18+ self-declaration plus
current Terms/Privacy acceptance for every publication. The server privately stores policy
version `2026-11-11` and confirmation time; it collects no birth date. This product policy does
not establish independent age verification or qualified legal review. Existing snapshots
without current acceptance stay unavailable until the student republishes.

After configuring production Google OAuth, HTTPS `PublicOrigin`, BFF connectivity, durable
storage, migrations, backup/restore, reporting and operator access, set
`Portfolio__PublishingEnabled=true` on the backend to enable publication. Keep it false while
preparing drafts or investigating a public-content incident; disabling it makes public
snapshot/state reads return 404 and empties the public index without deleting drafts. Consult
the [runbook](../docs/portfolio-workspace.md#deployment-runbook) for release checks.

## Tests

```bash
Expand All @@ -128,6 +182,14 @@ dotnet test backend/ProCoderJi.slnx --no-restore

The default suite creates isolated SQLite databases and uses test-only authentication registered by `WebApplicationFactory` in the test assembly. The API has no test-header login path. Tests cover anonymous behavior, Google challenge URL construction, CSRF/origin checks, account isolation, immutable sync, generation resets, stale requests, question validation, and retention.

Full-stack frontend checks (`E2E_DOTNET=1 npm run e2e`) build the separate
`backend/tests/ProCoderJi.BrowserHost` executable. It hosts the real API on loopback with an
owned temporary SQLite database, seeded disposable-account cookies and fixture-only publishing
controls. Origin, CSRF, ownership and persistence still use the real application pipeline.
Its `/__e2e__` endpoints and authentication scheme exist only in the test executable; the
production API Dockerfile publishes `backend/src/ProCoderJi.Api` only. Never deploy BrowserHost.
See [browser checks](../docs/development.md#browser-checks) for coverage and configuration.

To run the same API contract tests against a disposable server, set exactly one of
`PROCODERJI_TEST_POSTGRES`, `PROCODERJI_TEST_SQLSERVER`, or `PROCODERJI_TEST_MONGODB` to its
administrative test connection string. Each factory creates and drops only its randomly named
Expand Down
18 changes: 16 additions & 2 deletions backend/src/ProCoderJi.Api/Data/Mongo/MongoPlatformDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ await Users.Indexes.CreateManyAsync([
Unique = true,
Name = "IX_users_Portfolio_Username",
PartialFilterExpression = Builders<MongoUserDocument>.Filter.Type("Portfolio.Username", BsonType.String)
}),
new CreateIndexModel<MongoUserDocument>(Builders<MongoUserDocument>.IndexKeys
.Ascending("Portfolio.Status").Ascending("Portfolio.PublishedSearchVisible")
.Ascending("Portfolio.PublishingPolicyVersion").Ascending("Portfolio.Username"),
new CreateIndexOptions<MongoUserDocument>
{
Name = "IX_users_Portfolio_PublicIndex",
PartialFilterExpression = Builders<MongoUserDocument>.Filter.And(
Builders<MongoUserDocument>.Filter.Eq(user => user.Portfolio!.Status, PortfolioStatus.Published),
Builders<MongoUserDocument>.Filter.Eq(user => user.Portfolio!.PublishedSearchVisible, true))
})
], cancellationToken: cancellationToken);
}
Expand Down Expand Up @@ -93,16 +103,20 @@ public sealed class MongoPortfolioDocument
public long CreatedAtUtc { get; set; }
public long UpdatedAtUtc { get; set; }
public long? PublishedAtUtc { get; set; }
[BsonIgnoreIfNull] public string? PublishingPolicyVersion { get; set; }
[BsonIgnoreIfNull] public long? EligibilityConfirmedAtUtc { get; set; }

public PortfolioRecord ToRecord(Guid userId) => new(userId, Username, DraftJson, PublishedJson,
SearchVisible, PublishedSearchVisible, Status, Guid.Parse(Revision), CreatedAtUtc, UpdatedAtUtc, PublishedAtUtc);
SearchVisible, PublishedSearchVisible, Status, Guid.Parse(Revision), CreatedAtUtc, UpdatedAtUtc, PublishedAtUtc,
PublishingPolicyVersion, EligibilityConfirmedAtUtc);

public static MongoPortfolioDocument From(PortfolioRecord record) => new()
{
Username = record.Username, DraftJson = record.DraftJson, PublishedJson = record.PublishedJson,
SearchVisible = record.SearchVisible, PublishedSearchVisible = record.PublishedSearchVisible,
Status = record.Status, Revision = record.Revision.ToString("D"), CreatedAtUtc = record.CreatedAtUtc,
UpdatedAtUtc = record.UpdatedAtUtc, PublishedAtUtc = record.PublishedAtUtc
UpdatedAtUtc = record.UpdatedAtUtc, PublishedAtUtc = record.PublishedAtUtc,
PublishingPolicyVersion = record.PublishingPolicyVersion, EligibilityConfirmedAtUtc = record.EligibilityConfirmedAtUtc
};
}

Expand Down
Loading