Skip to content

Latest commit

 

History

History
391 lines (311 loc) · 8.78 KB

File metadata and controls

391 lines (311 loc) · 8.78 KB

Getting Started with the Sponsor Model

Quick Start Guide

Step 1: Create Your First Global Sponsor

Run the interactive script:

pnpm tsx scripts/create-global-sponsor.ts

Follow the prompts to create your account with Global Sponsor privileges.

Step 2: Start the Development Server

pnpm dev

Step 3: Log In

Navigate to http://localhost:5173/login and log in with your Global Sponsor credentials.

Step 4: Invite a School Sponsor

Create a simple API route or use the browser console on an authenticated page:

// In browser console on authenticated page
await fetch("/api/invite-sponsor", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "teacher@school.edu",
    roleType: "SCHOOL_SPONSOR",
    schoolName: "Point Loma High School",
    districtName: "Point Loma Unified School District",
    domains: ["psdr3.org", "student.psdr3.org"],
  }),
});

Or create this temporary API route at src/routes/api/invite-sponsor/+server.ts:

import { json } from "@sveltejs/kit";
import { createSponsorInvitation } from "$lib/sponsors";
import { RoleType } from "@prisma/client";
import type { RequestHandler } from "./$types";
import { canInviteSponsors } from "$lib/permissions";

export const POST: RequestHandler = async ({ request, locals }) => {
  if (!locals.user) {
    return json({ error: "Unauthorized" }, { status: 401 });
  }

  const { email, roleType, schoolName, districtName, domains } =
    await request.json();

  // Check permissions
  const level =
    roleType === "GLOBAL_SPONSOR"
      ? "global"
      : roleType === "DISTRICT_SPONSOR"
        ? "district"
        : "school";

  if (!canInviteSponsors(locals.user, level)) {
    return json({ error: "Insufficient permissions" }, { status: 403 });
  }

  try {
    const invitationId = await createSponsorInvitation({
      email,
      roleType: roleType as RoleType,
      schoolName,
      districtName,
      domains: domains
        ? domains.split(",").map((d: string) => d.trim())
        : undefined,
      inviterId: locals.user.id,
    });

    return json({ success: true, invitationId });
  } catch (error) {
    console.error("Invitation error:", error);
    return json({ error: "Failed to send invitation" }, { status: 500 });
  }
};

Step 5: Accept Invitation

The sponsor receives an email with a link like:

http://localhost:5173/setup?token=abc123...

They click it, create an account (or log in), and complete the setup.

Common Tasks

Check User Roles

// In any server-side code
import { loadUserWithRoles } from "$lib/permissions";

const user = await loadUserWithRoles(userId);
console.log(user.roles);
// [
//   { type: 'GLOBAL_SPONSOR', ... },
//   { type: 'SCHOOL_SPONSOR', schoolId: '...' }
// ]

Grant a Role Manually

import { grantRole } from "$lib/permissions";
import { RoleType } from "@prisma/client";

// Make someone a district sponsor
await grantRole(userId, RoleType.DISTRICT_SPONSOR, {
  districtId: "district-uuid",
  grantedBy: currentUser.id,
});

// Make someone a student in a cohort
await grantRole(userId, RoleType.STUDENT, {
  schoolId: "school-uuid",
  cohortId: "cohort-uuid",
  grantedBy: currentUser.id,
});

Create a District and School

import { prisma } from "$lib/db";

// Create district
const district = await prisma.district.create({
  data: {
    name: "Point Loma Unified School District",
    slug: "psdr3",
    description: "San Diego area school district",
  },
});

// Create school
const school = await prisma.school.create({
  data: {
    name: "Point Loma High School",
    slug: "plhs",
    districtId: district.id,
    autoApproveStudents: false,
  },
});

// Register domains
await prisma.domain.createMany({
  data: [
    {
      domain: "psdr3.org",
      districtId: district.id,
      verified: true,
    },
    {
      domain: "student.psdr3.org",
      districtId: district.id,
      schoolId: school.id,
      verified: true,
    },
  ],
});

// Create a cohort (graduation year)
await prisma.cohort.create({
  data: {
    schoolId: school.id,
    year: 2025,
    name: "Class of 2025",
    visible: false,
    lettersEnabled: false,
  },
});

Find Schools by Email Domain

import { findSchoolsByDomain } from "$lib/domains";

const schools = await findSchoolsByDomain("student@psdr3.org");
console.log(schools);
// [{ id: '...', name: 'Point Loma High School', ... }]

Check Permissions in Page

// +page.server.ts
import { error } from "@sveltejs/kit";
import { canApproveUsers } from "$lib/permissions";

export const load = async ({ locals, params }) => {
  if (!locals.user) {
    throw error(401, "Not logged in");
  }

  const schoolId = params.schoolId;

  if (!canApproveUsers(locals.user, schoolId)) {
    throw error(403, "You cannot manage users at this school");
  }

  // ... rest of your code
};

Get User's Accessible Schools

import { getUserSchools } from "$lib/permissions";

const schools = await getUserSchools(locals.user.id);
// Returns all schools the user has access to based on their roles

Testing Different Scenarios

Scenario 1: User with Multiple Roles

// Create a user who is both a sponsor and a student
const user = await prisma.user.create({
  data: {
    name: "Parker Hasenkamp",
    email: "parker@psdr3.org",
    schoolEmail: "parker@psdr3.org",
    passwordHash: await hashPassword("password"),
    status: "ACTIVE",
    schoolVerifiedAt: new Date(),
  },
});

// Grant multiple roles
await grantRole(user.id, RoleType.GLOBAL_SPONSOR);
await grantRole(user.id, RoleType.SCHOOL_SPONSOR, { schoolId });
await grantRole(user.id, RoleType.STUDENT, { schoolId, cohortId });

// This user can:
// - Manage anything (global sponsor)
// - Participate as a student
// - See both admin controls AND student features

Scenario 2: School Sponsor Managing Cohort

// User is school sponsor
const user = await loadUserWithRoles(userId);

// Can they create a cohort at this school?
if (canManageCohort(user, schoolId)) {
  await prisma.cohort.create({
    data: {
      schoolId,
      year: 2026,
      name: "Class of 2026",
      visible: false,
    },
  });
}

Scenario 3: Student Registration with Auto-Cohort

import { registerUser } from "$lib/auth";

// Student registers with school and graduation year
const result = await registerUser({
  name: "John Doe",
  schoolEmail: "john@student.psdr3.org",
  personalEmail: "john@gmail.com",
  password: "secure-password",
  schoolId: "school-uuid",
  cohortYear: 2026,
});

// User is created with STUDENT role automatically assigned to cohort

Debugging

View User's Roles

-- In SQLite
SELECT
  u.name,
  u.email,
  r.type as role,
  d.name as district,
  s.name as school,
  c.year as cohort_year
FROM User u
LEFT JOIN UserRole r ON u.id = r.userId
LEFT JOIN District d ON r.districtId = d.id
LEFT JOIN School s ON r.schoolId = s.id
LEFT JOIN Cohort c ON r.cohortId = c.id
WHERE u.email = 'parker@psdr3.org';

Check Permissions

// In any component or route
import { isSchoolSponsor, canApproveUsers } from "$lib/permissions";

console.log("Is school sponsor?", isSchoolSponsor(user, schoolId));
console.log("Can approve users?", canApproveUsers(user, schoolId));
console.log("User roles:", user.roles);

List All Invitations

SELECT
  email,
  roleType,
  accepted,
  expiresAt,
  createdAt
FROM SponsorInvitation
ORDER BY createdAt DESC;

Migration from Old Schema

If you have existing users, you'll need to migrate them. Here's a basic script:

// scripts/migrate-users.ts
import { prisma } from "../src/lib/db";
import { RoleType } from "@prisma/client";

async function migrate() {
  const oldUsers = await prisma.$queryRaw`
    SELECT * FROM User WHERE role = 'ADMIN'
  `;

  for (const oldUser of oldUsers) {
    // Create student role with cohort
    // (Assuming old schema had graduationYear)
    const cohort = await prisma.cohort.findFirst({
      where: {
        year: oldUser.graduationYear,
        // Find school by domain somehow
      },
    });

    if (cohort) {
      await prisma.userRole.create({
        data: {
          userId: oldUser.id,
          type: RoleType.STUDENT,
          schoolId: cohort.schoolId,
          cohortId: cohort.id,
        },
      });
    }
  }
}

Next Steps

  1. Build Admin UI - Create the sponsor dashboard
  2. Update Registration - Integrate domain detection
  3. Cohort Management - UI for creating and managing cohorts
  4. Student Profiles - Link to cohorts and visibility
  5. Letters System - Integrate with cohorts and permissions

You now have a fully functional multi-role sponsor system! The foundation is solid and ready for UI development.