Skip to content

Commit 71f2b18

Browse files
committed
Seed Visionaries development tenant and document onboarding
1 parent 06e80fe commit 71f2b18

4 files changed

Lines changed: 190 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ Use Node.js compatible with the installed Next.js version and pnpm 10.17.1 (`npm
105105

106106
Open http://localhost:3000 and sign in. Invite a test address or send a member email to exercise the local flow. Default Juno uses `NODE_ENV=test`: it simulates domain registration and accepts emails without delivering them. Its DNS records are fake and must not be published. A successful request here does **not** prove inbox delivery.
107107

108+
For the Visionaries tenant, see [Visionaries development setup](docs/VISIONARIES_DEVELOPMENT.md)
109+
for local URLs, branding defaults, and student/admin test accounts.
110+
108111
### Real email (team-assisted)
109112

110113
Ask an infrastructure maintainer for an approved SendGrid key and sender domain. Set `SENDGRID_KEY` and `EMAIL_SENDER_DOMAIN` in your local `.env`; the domain is the base domain, such as `example.org`, without a URL scheme or the `mail.` prefix. The app sends as `<org-slug>@mail.<domain>`.

‎docs/VISIONARIES_DEVELOPMENT.md‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Visionaries to the Throne development
2+
3+
This development tenant supports the signup (#274) and login (#275) work in the
4+
shared ServiceStart codebase. It does not provision a production deployment or
5+
implement the camper application workflow.
6+
7+
## First-time setup
8+
9+
Complete the [README local setup](../README.md#getting-started), including Docker,
10+
Juno, your local `.env`, and database migrations. Keep the template's simulated
11+
email configuration for ordinary development.
12+
13+
Add `visionariesforthethrone.lvh.me` to the comma-separated `ALLOWED_DEV_ORIGINS`
14+
value in your local `.env` (preserve any existing entries). Restart Next.js after
15+
changing it. For this tenant's auth work, set:
16+
17+
```dotenv
18+
BETTER_AUTH_URL=http://visionariesforthethrone.lvh.me:3000
19+
```
20+
21+
With your **local development database** configured, run:
22+
23+
```bash
24+
pnpm run db:migrate
25+
pnpm run db:seed
26+
pnpm dev
27+
```
28+
29+
Do not run the development seed against production: it creates accounts with
30+
known test passwords. Existing contributors can pull this change and rerun
31+
`pnpm run db:seed`; no new schema migration is required.
32+
33+
## URLs and accounts
34+
35+
- Signup: <http://visionariesforthethrone.lvh.me:3000/signup>
36+
- Login: <http://visionariesforthethrone.lvh.me:3000/login>
37+
- Existing application home: <http://visionariesforthethrone.lvh.me:3000/>
38+
39+
`lvh.me` resolves to loopback; no hosts-file entry is normally needed. Use the
40+
tenant hostname, not `localhost`, which selects the default ServiceStart tenant.
41+
42+
All these development accounts use `password123`:
43+
44+
| Email | Existing ServiceStart role | Use |
45+
| ----------------------- | -------------------------- | --------------------------------- |
46+
| `owner@example.com` | owner | Organization owner controls |
47+
| `admin@example.com` | admin | Staff access |
48+
| `member1@example.com` | member | Student/member access |
49+
| `member2@example.com` | member | Second student/member |
50+
| `nonmember@example.com` | none | Signed-in user without membership |
51+
52+
The same email addresses exist in other seeded organizations, with distinct user
53+
IDs and memberships. Accounts are selected by the tenant hostname. Public signup
54+
continues to use existing ServiceStart behavior; a member fixture does not imply
55+
camp acceptance or automatic membership for new signups.
56+
57+
## Configuration and scope
58+
59+
- Organization ID: `org_visionariesforthethrone`
60+
- Slug: `visionariesforthethrone`
61+
- Primary color: `#5C218C`; secondary color: `#C29BDC` (Figma palette).
62+
- Development tagline: `Visionaries to the Throne`.
63+
- Initial navbar: horizontal-center, white.
64+
65+
These are organization configuration rows, not organization-name checks in app
66+
code. Rerunning the seed reapplies these configuration defaults while retaining
67+
existing account IDs and credentials. The seed does not set `LogoUrl`: until an
68+
approved logo is configured, the existing ServiceStart/Sunset fallback appears.
69+
Configure the approved asset through the existing `LogoUrl` setting when available.
70+
71+
The current shared login/signup UI is intentional. The final layout, logo, copy,
72+
student/admin landing destinations, and account-versus-membership approval flow
73+
remain part of product/design coordination for #274 and #275. Use the existing
74+
home page to exercise development accounts until those destinations are built.
75+
No user schema, camper schema, or new role is introduced here.
76+
77+
## Verify your setup
78+
79+
1. Open signup while signed out; verify the Visionaries name and purple background.
80+
2. Sign in as `member1@example.com`, then sign out and sign in as `admin@example.com`.
81+
3. Check the existing member and admin experiences using their respective accounts.
82+
4. Rerun the seed and confirm the same accounts still work.
83+
84+
The seed integration suite checks repeatability, branding, roles, and login across
85+
all seeded tenants. With the local test database running on port 5433:
86+
87+
```bash
88+
pnpm exec vitest run tests/unit/scripts/seed.test.ts
89+
```
90+
91+
New PR preview databases also run this seed through the existing workflow. An
92+
existing preview database needs reseeding to receive the tenant. Preview hostname
93+
and authentication-origin routing still need a valid tenant-aware URL; a normal
94+
Netlify preview URL alone does not establish the Visionaries hostname. Local
95+
development above is the supported starting point for these tickets.
96+
97+
`pnpm run setupOrg` sets up email and optional file storage; it does not create
98+
the organization. Production organization provisioning, staff invitations, DNS,
99+
and real email setup are separate maintainer tasks.
100+
101+
## Design reference
102+
103+
[Visionaries Figma section](https://www.figma.com/design/PtssEcXD74l902nKJ2uIAh/Service-Start-Fall-2026?node-id=20936-3387)

‎scripts/seed.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,26 @@ const ORGS: Array<{
2626
name: string;
2727
slug: string;
2828
navbar: NavbarConfig;
29+
config?: Partial<Record<OrganizationConfigKey, string>>;
2930
}> = [
3031
{
3132
id: "org_servicestart",
3233
name: "ServiceStart",
3334
slug: "servicestart",
3435
navbar: { variant: "horizontal-center", color: "red" },
3536
},
37+
{
38+
id: "org_visionariesforthethrone",
39+
name: "Visionaries to the Throne",
40+
slug: "visionariesforthethrone",
41+
navbar: { variant: "horizontal-center", color: "white" },
42+
// Development defaults from the Figma style guide, not final page designs.
43+
config: {
44+
[OrganizationConfigKey.PrimaryColor]: "#5C218C",
45+
[OrganizationConfigKey.SecondaryColor]: "#C29BDC",
46+
[OrganizationConfigKey.Tagline]: "Visionaries to the Throne",
47+
},
48+
},
3649
{
3750
id: "org_vertical_icon",
3851
name: "Vertical Icon Org",
@@ -76,6 +89,14 @@ export async function main() {
7689
OrganizationConfigKey.NavbarColor,
7790
org.navbar.color,
7891
);
92+
93+
for (const [key, value] of Object.entries(org.config ?? {})) {
94+
await OrganizationConfigService.setConfig(
95+
org.id,
96+
key as OrganizationConfigKey,
97+
value,
98+
);
99+
}
79100
}
80101

81102
log("Organizations created with navbar configs.");

‎tests/unit/scripts/seed.test.ts‎

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
// @vitest-environment node
2+
13
import { auth } from "@/lib/auth";
24
import db from "@/lib/db";
3-
import { organizations, users, accounts } from "@/lib/schema";
5+
import {
6+
organizations,
7+
users,
8+
accounts,
9+
members,
10+
organizationConfig,
11+
OrganizationConfigKey,
12+
} from "@/lib/schema";
413
import { hashPassword } from "better-auth/crypto";
514
import { main } from "@/scripts/seed";
615
import { eq, isNull } from "drizzle-orm";
@@ -57,8 +66,8 @@ it("creates distinct admin accounts that can sign in to each seeded tenant", asy
5766
.select()
5867
.from(users)
5968
.where(eq(users.email, "admin@example.com"));
60-
expect(admins).toHaveLength(4);
61-
expect(new Set(admins.map((user) => user.organizationId)).size).toBe(4);
69+
expect(admins).toHaveLength(5);
70+
expect(new Set(admins.map((user) => user.organizationId)).size).toBe(5);
6271
expect(
6372
await db.select().from(users).where(isNull(users.organizationId)),
6473
).toHaveLength(0);
@@ -74,6 +83,57 @@ it("creates distinct admin accounts that can sign in to each seeded tenant", asy
7483
}
7584
});
7685

86+
it("seeds repeatable Visionaries branding and tenant-scoped development roles", async () => {
87+
await main();
88+
await main();
89+
const orgs = await db
90+
.select()
91+
.from(organizations)
92+
.where(eq(organizations.slug, "visionariesforthethrone"));
93+
expect(orgs).toHaveLength(1);
94+
const org = orgs[0];
95+
expect(org.name).toBe("Visionaries to the Throne");
96+
const configs = await db
97+
.select()
98+
.from(organizationConfig)
99+
.where(eq(organizationConfig.organizationId, org.id));
100+
const config = Object.fromEntries(configs.map((row) => [row.key, row.value]));
101+
expect(config[OrganizationConfigKey.PrimaryColor]).toBe("#5C218C");
102+
expect(config[OrganizationConfigKey.SecondaryColor]).toBe("#C29BDC");
103+
expect(config[OrganizationConfigKey.Tagline]).toBe(
104+
"Visionaries to the Throne",
105+
);
106+
expect(new Set(configs.map((row) => row.key)).size).toBe(configs.length);
107+
const memberships = await db
108+
.select()
109+
.from(members)
110+
.where(eq(members.organizationId, org.id));
111+
for (const [email, role] of [
112+
["owner@example.com", "owner"],
113+
["admin@example.com", "admin"],
114+
["member1@example.com", "member"],
115+
["nonmember@example.com", null],
116+
]) {
117+
const result = await auth.api.signInEmail({
118+
headers: new Headers({ host: "visionariesforthethrone.lvh.me:3000" }),
119+
body: { email: email!, password: DEFAULT_TEST_PASSWORD },
120+
});
121+
const [user] = await db
122+
.select()
123+
.from(users)
124+
.where(eq(users.id, result.user.id));
125+
expect(user.organizationId).toBe(org.id);
126+
expect(
127+
memberships.find((member) => member.userId === user.id)?.role ?? null,
128+
).toBe(role);
129+
}
130+
const defaults = await db
131+
.select()
132+
.from(organizationConfig)
133+
.where(eq(organizationConfig.organizationId, "org_servicestart"));
134+
expect(defaults.some((row) => row.value === "#5C218C")).toBe(false);
135+
});
136+
77137
it("repairs an old unscoped seed account without replacing its ID or credential", async () => {
78138
const password = await hashPassword(DEFAULT_TEST_PASSWORD);
79139
await db.insert(users).values({

0 commit comments

Comments
 (0)