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
9 changes: 6 additions & 3 deletions .beads/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ daemon.log
daemon.pid
bd.sock
sync-state.json
last-touched

# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
Expand All @@ -31,11 +32,13 @@ beads.left.meta.json
beads.right.jsonl
beads.right.meta.json

# Sync state (local-only, per-machine)
# These files are machine-specific and should not be shared across clones
.sync.lock
sync_base.jsonl

# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
# They would override fork protection in .git/info/exclude, allowing
# contributors to accidentally commit upstream issue databases.
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
# are tracked by git by default since no pattern above ignores them.

# Recently accessed files tracking
last-touched
Empty file added .beads/.sync.lock
Empty file.
123 changes: 123 additions & 0 deletions .beads/sync_base.jsonl

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Conventional Commits: `<type>[scope]: <description>`

Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`

<!-- bv-agent-instructions-v1 -->

## Agent Instructions

Uses **bd** (beads) for issue tracking:
Expand Down Expand Up @@ -94,3 +96,5 @@ Work is NOT complete until `git push` succeeds:
- `--id` and `--parent` cannot be used together
- Always add detailed descriptions using `--description` flag
- Always `bd sync` before ending session

<!-- end-bv-agent-instructions -->
17 changes: 16 additions & 1 deletion app/components/alerts.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CheckCircleIcon } from '@heroicons/react/solid'
import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/solid'
import { ReactNode } from 'react'

export function Alert({ children }: { children: ReactNode }) {
Expand All @@ -18,3 +18,18 @@ export function Alert({ children }: { children: ReactNode }) {
</div>
)
}

export function ErrorAlert({ children }: { children: ReactNode }) {
return (
<div className="bg-red-50 border-l-4 border-red-400 py-3 px-4 mt-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm font-medium text-red-900">{children}</p>
</div>
</div>
</div>
)
}
29 changes: 22 additions & 7 deletions app/routes/login.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import type { ActionFunction, LoaderFunction } from '@remix-run/node'
import { json } from '@remix-run/node'
import { Form, useLoaderData, useNavigation } from '@remix-run/react'
import { Alert } from '~/components/alerts'
import { Alert, ErrorAlert } from '~/components/alerts'
import { Button } from '~/components/form-elements'
import { auth } from '~/services/auth.server'
import { getUserSession } from '~/services/session.server'
import { commitSession, getUserSession } from '~/services/session.server'

export const loader: LoaderFunction = async ({ request }) => {
await auth.isAuthenticated(request, { successRedirect: '/dashboard' })
const session = await getUserSession(request)
// This session key `auth:magiclink` is the default one used by the EmailLinkStrategy
// you can customize it passing a `sessionMagicLinkKey` when creating an
// instance.
return json({
user: session.get('user'),
magicLinkSent: session.has('zain:magiclink'),
})
const error = session.get(auth.sessionErrorKey) as
| { message: string }
| undefined
return json(
{
user: session.get('user'),
magicLinkSent: session.has('zain:magiclink'),
error: error?.message,
},
{
headers: {
'Set-Cookie': await commitSession(session),
},
}
)
}

export const action: ActionFunction = async ({ request }) => {
Expand All @@ -31,7 +42,10 @@ export const action: ActionFunction = async ({ request }) => {
}

export default function Login() {
const { magicLinkSent } = useLoaderData<{ magicLinkSent: boolean }>()
const { magicLinkSent, error } = useLoaderData<{
magicLinkSent: boolean
error?: string
}>()
const { state } = useNavigation()

return (
Expand Down Expand Up @@ -79,6 +93,7 @@ export default function Login() {
</Button>
</div>
</Form>
{error ? <ErrorAlert>{error}</ErrorAlert> : null}
{magicLinkSent ? (
<Form action="/logout" method="post">
<input type="hidden" name="redirectTo" value="/login" />
Expand Down
2 changes: 1 addition & 1 deletion app/services/session.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const sessionStorage = createCookieSessionStorage({
sameSite: 'lax',
path: '/',
httpOnly: true,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
maxAge: 7 * 24 * 60 * 60, // 7 days in seconds
secrets: [getRequiredServerEnvVar('SESSION_SECRET')],
// normally you want this to be `secure: true`
// but that doesn't work on localhost for Safari
Expand Down
42 changes: 16 additions & 26 deletions docs/testing-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,36 +227,26 @@ npm run test:e2e:staging

#### Setting Up Staging Authentication

Since staging uses real email delivery, auth fixtures must be created manually:

1. Login to https://staging.kelas.rumahberbagi.com in your browser
2. Open DevTools > Application > Storage > Cookies
3. Export cookies using a browser extension (e.g., "EditThisCookie")
4. Save to `e2e/fixtures/auth/staging/<role>.staging.json` in this format:

```json
{
"cookies": [
{
"name": "__session",
"value": "...",
"domain": "staging.kelas.rumahberbagi.com",
"path": "/",
"expires": -1,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
],
"origins": []
}
Since staging uses real email delivery, auth fixtures are created using npm
scripts that run Playwright codegen:

```bash
npm run test:e2e:staging:auth:member # Login with: member@rumahberbagi.com
npm run test:e2e:staging:auth:author # Login with: vika@rumahberbagi.com
npm run test:e2e:staging:auth:admin # Login with: admin@rumahberbagi.com
```

1. Run the npm script for the role you need
2. Login with the email shown in the terminal
3. Close the browser - storage state is saved automatically

Required auth fixtures:

- `member.staging.json` - Regular member
- `author.staging.json` - Course author
- `admin.staging.json` - Administrator
| Fixture | Email | Role |
| --------------------- | ----------------------- | -------------- |
| `member.staging.json` | member@rumahberbagi.com | Regular member |
| `author.staging.json` | vika@rumahberbagi.com | Course author |
| `admin.staging.json` | admin@rumahberbagi.com | Administrator |

**Note:** Auth fixtures are gitignored and must be recreated when sessions
expire.
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"test:e2e:run": "cross-env RUNNING_E2E=true DATABASE_URL=file:./test.db start-server-and-test start:e2e http-get://localhost:3000/ test:e2e",
"test:e2e:docker": "playwright test --config=playwright.docker.config.ts",
"test:e2e:staging": "playwright test --config=playwright.staging.config.ts",
"test:e2e:staging:auth:member": "echo 'Login with: member@rumahberbagi.com' && playwright codegen https://staging.kelas.rumahberbagi.com --save-storage=e2e/fixtures/auth/staging/member.staging.json",
"test:e2e:staging:auth:author": "echo 'Login with: vika@rumahberbagi.com' && playwright codegen https://staging.kelas.rumahberbagi.com --save-storage=e2e/fixtures/auth/staging/author.staging.json",
"test:e2e:staging:auth:admin": "echo 'Login with: admin@rumahberbagi.com' && playwright codegen https://staging.kelas.rumahberbagi.com --save-storage=e2e/fixtures/auth/staging/admin.staging.json",
"test:e2e:production": "cross-env BASE_URL=https://kelas.rumahberbagi.com playwright test --config=playwright.docker.config.ts",
"type-check": "tsc --noEmit",
"setup": "npm install && prisma migrate reset --force && npm run test:e2e:run",
Expand Down
29 changes: 15 additions & 14 deletions playwright-staging-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@ const REQUIRED_AUTH_FILES = [
*
* Unlike local tests, staging authentication cannot be automated via magic link
* because emails are sent to real addresses. Instead, this setup validates that
* manually-exported auth fixtures exist.
* auth fixtures exist, which are generated using Playwright codegen.
*
* To create auth fixtures for staging:
* 1. Login to https://staging.kelas.rumahberbagi.com in your browser
* 2. Open DevTools > Application > Storage > Cookies
* 3. Export the cookies using browser extension or manually copy them
* 4. Save to e2e/fixtures/auth/staging/<role>.staging.json in Playwright's
* storageState format:
* {
* "cookies": [{ "name": "...", "value": "...", "domain": "...", ... }],
* "origins": []
* }
* To create auth fixtures for staging, use the npm scripts:
*
* npm run test:e2e:staging:auth:member # Login with: member@rumahberbagi.com
* npm run test:e2e:staging:auth:author # Login with: vika@rumahberbagi.com
* npm run test:e2e:staging:auth:admin # Login with: admin@rumahberbagi.com
*
* 1. Run the npm script for the role you need
* 2. Login with the email shown above in the browser that opens
* 3. Close the browser - storage state is saved automatically
*/
async function globalSetup() {
if (!fs.existsSync(STAGING_AUTH_DIR)) {
Expand All @@ -46,9 +45,11 @@ async function globalSetup() {
for (const file of missingFiles) {
console.warn(` - ${STAGING_AUTH_DIR}/${file}`)
}
console.warn(
'\nSee playwright-staging-setup.ts for instructions on creating these files.\n'
)
console.warn('\nGenerate with: npm run test:e2e:staging:auth:<role>\n')
console.warn('Staging emails:')
console.warn(' member: member@rumahberbagi.com')
console.warn(' author: vika@rumahberbagi.com')
console.warn(' admin: admin@rumahberbagi.com\n')
}

console.log('✓ Staging global setup complete')
Expand Down
Loading