Skip to content

feat(host/login): add the login page with a Sign in with Edupass button #76

Description

@YimingIsCOLD

User story

As an unauthenticated teacher, I want a login page with a "Sign in with Edupass" button, so that I can start Edupass sign-in from the workspace instead of typing a route by hand.

Background

Teacher Workspace has no sign-in surface. #75 adds the server-side Edupass routes (/auth/edupass to start sign-in, /auth/edupass/callback to complete it) and explicitly leaves the user interface out of scope: a teacher has to open /auth/edupass directly in the browser. This issue supplies that missing entry point.

The page is a static, unauthenticated surface. Its only action is a top-level navigation to /auth/edupass; it performs no OIDC work of its own and holds no client-side auth state.

Sign-in can fail on the server (mismatched correlation state, an unverifiable ID token, or an error response from the provider), and every one of those failures ends on the server side with nowhere to report it. The agreed contract is that the server redirects back to /login?error=oauth2_callback_failed, and this page turns that parameter into a visible message so a failed sign-in does not dead-end on a blank page. This issue only consumes the parameter; emitting it belongs to the server work.

The workspace shell (SidebarProvider + AppSidebar) currently wraps every route in App.tsx. An unauthenticated teacher has no apps to navigate to, so /login renders outside that shell.

Acceptance criteria

Teacher starts Edupass sign-in from the login page

  • Given an unauthenticated teacher on /login
  • When the teacher activates the "Sign in with Edupass" button
  • Then the browser performs a top-level navigation to /auth/edupass, leaving the single-page app rather than handling the click in the client router

Login page renders outside the workspace sidebar shell

  • Given an unauthenticated teacher
  • When the teacher opens /login at any viewport width
  • Then the sign-in content is displayed, and neither the app sidebar nor the mobile sidebar trigger is present on the page

Failed Edupass sign-in is reported on the login page

  • Given sign-in failed and the teacher was returned to /login?error=oauth2_callback_failed
  • When the page loads
  • Then a message stating that sign-in failed is displayed, and the "Sign in with Edupass" button remains available so the teacher can retry

Login page shows no error on a normal visit

  • Given a teacher opens /login with no error parameter, an empty error parameter, or an error value other than oauth2_callback_failed
  • When the page loads
  • Then no error message is displayed, and the page renders exactly as it does on a first visit

Workspace routes are unaffected by the login page

  • Given the login page exists
  • When a teacher opens /, /students, /posts, /groups, or an unknown path
  • Then each route renders as it did before, inside the workspace sidebar shell, with the sidebar present

Out of scope

  • The /auth/edupass route and the entire Edupass OIDC exchange (feat(server/auth): add the Edupass OIDC client and login routes #75). This page only links to the route; it is not created or changed here.
  • Emitting the /login?error=oauth2_callback_failed redirect. The server side of that contract belongs to feat(server/auth): add the Edupass OIDC client and login routes #75. This issue only reads the parameter.
  • Error codes other than oauth2_callback_failed. Any other value renders no error.
  • Gating routes behind authentication. Every existing route stays reachable while unauthenticated, and nothing redirects an unauthenticated teacher to /login.
  • Redirecting an already-authenticated teacher away from /login. There is no client-visible session state to check yet.
  • The demo account sign-in shown in the prototype. It is deliberately excluded.

Design assets

Prototype: https://teacherworkspace-prototype.string.sg/login (build the Edupass button only; omit the demo account).

Route structure after this change:

flowchart LR
    A["/login"] -->|"Sign in with Edupass"| B["GET /auth/edupass"]
    B -.->|"sign-in fails (#75)"| C["/login?error=oauth2_callback_failed"]
    C --> A
    B -.->|"sign-in succeeds (#75)"| D["/"]
Loading

Technical context

Pending grooming.

Data model

Pending grooming.

API contract

Pending grooming.

Error contract

Pending grooming.

Additional test scenarios

Pending grooming.

Hard constraints

Pending grooming.


🤖 Generated with aif-create-issue

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions