Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b189661
Fixes for handling database from api
acbart Sep 23, 2025
7cf06ad
Add build command for database
acbart Sep 23, 2025
29fc606
Merge branch 'UD-CISC474-F25:main' into main
acbart Oct 5, 2025
82f1952
Move up netlify toml
acbart Oct 5, 2025
182f270
Have to actually delete old toml
acbart Oct 5, 2025
41c3fef
Try to make functions explicit
acbart Oct 5, 2025
60cb4fe
No, don't specify functions explicitly
acbart Oct 5, 2025
637fb12
Tiny change to home route to trigger change
acbart Oct 5, 2025
93e972d
Move netlify to the end
acbart Oct 5, 2025
fd68830
Try adding explicit redirect
acbart Oct 5, 2025
8c1340e
Remove redirects, try a new order for vite
acbart Oct 5, 2025
fa23013
Trim fat on netlify settings
acbart Oct 5, 2025
1f8e370
Move configs around, add some more files
acbart Oct 5, 2025
0e639d8
Trying cloudflare instead
acbart Oct 5, 2025
37ecb44
Wrangler might need to be toplevel
acbart Oct 5, 2025
d2dce16
Retry wrangler deploy for cloudflare
acbart Oct 5, 2025
9fe22d8
Use virtual main
acbart Oct 5, 2025
6150fc6
Try deploying this way instead
acbart Oct 5, 2025
3b8de85
Reset the deployment since it isn't being used
acbart Oct 5, 2025
56a860e
Minor notes from readme
acbart Oct 5, 2025
f06da10
Merge remote-tracking branch 'upstream/main'
acbart Oct 5, 2025
276ade9
Merge remote-tracking branch 'upstream/main'
acbart Oct 5, 2025
7cd6ad6
NextJS frontend user list
acbart Oct 5, 2025
4aad770
Minor modification to schema from inclass
acbart Oct 5, 2025
8c1b4c3
Merge remote-tracking branch 'upstream/main'
acbart Oct 5, 2025
753b065
Backend for user
acbart Oct 5, 2025
b973561
First pass at docker, not working yet
acbart Oct 5, 2025
e81651a
More sophisticated schema
acbart Oct 5, 2025
d95056b
ChatGPT made me a little faker script
acbart Oct 5, 2025
d432c73
Generated data example
acbart Oct 5, 2025
da67785
Get rid of links stuff
acbart Oct 6, 2025
1dfcd53
Setup DTOs with Zod in packages/api
acbart Oct 6, 2025
b977fab
Get user by email endpoint
acbart Oct 6, 2025
7031ddd
Deprecate links module, and create courses module
acbart Oct 6, 2025
850ae68
Add in mapped types, might not need it
acbart Oct 6, 2025
8b6d4a7
Courses actually wired up to database
acbart Oct 6, 2025
8ee3cd6
Added course creation to service while I was there
acbart Oct 6, 2025
6dce105
Controllers and service getters for submissions, assignments, and groups
acbart Oct 6, 2025
43f32fe
Wired up frontend to backend
acbart Oct 6, 2025
108ae7b
Don't push this package lock
acbart Oct 6, 2025
e8f1b58
Slightly fancier frontend
acbart Oct 6, 2025
90f7e61
Updated readme with some context about the big change
acbart Oct 6, 2025
9b1ae42
Provide helpful links to tanstack docs
acbart Oct 6, 2025
6ba9ebf
Improve courses rendering
acbart Oct 11, 2025
ef670c3
Example for rendering an individual course and creating a new course
acbart Oct 11, 2025
d74fc14
Attempted to update Zod dto stuff
acbart Oct 11, 2025
11fafb0
Improve typing a little for fetcher
acbart Oct 15, 2025
fcf00a8
Reorder imports in frontend
acbart Oct 16, 2025
29c84a2
Create auth basics
acbart Oct 16, 2025
35085ce
Add passport packages
acbart Oct 16, 2025
7508dc9
Kill package lock for now
acbart Oct 16, 2025
db63945
Setup jwt strategy
acbart Oct 16, 2025
425eb75
Guard some backend routes
acbart Oct 16, 2025
a15d446
JWT user handling
acbart Oct 18, 2025
b7f70c8
Pin 20.x as version for deployment
acbart Oct 18, 2025
fcf1787
Instructions for handling authentication
acbart Oct 20, 2025
2bede39
Add link to diff
acbart Oct 20, 2025
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
132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ Then, you can make the first push of your initial database setup: `npx prisma db

Then you can populate the database with an initial row by using: `npx prisma db seed`

- Run studio: `npx prisma studio`
- Validate prisma: `npx prisma validate`

---

## What's inside?
Expand Down Expand Up @@ -100,3 +103,132 @@ This Turborepo has some additional tools already setup for you:
- [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting
- [Prettier](https://prettier.io) for code formatting

## Changes on October 5, 2025

I've gone through and made some changes to the frontend, mostly to move us away from Next.js to Tanstack Start/Router. The new frontend is in `apps/web-start`, and the old one is still in `apps/web`. The scripts in `apps/web` and `apps/docs` have been disabled so that they don't run when you do `npm run dev` from the top-level. You might want to delete them, just so you don't get confused, but I figured some folks might still have some code they want to quickly reference in them.

This repository is my version of the tasks we've finished in this class so far. It has a bunch of tables for users, courses, assignments, and submissions. The backend has been modified to allow CORS from the frontend, and the frontend has been modified to use React Query to fetch data from the backend. The frontend is very bare-bones right now, but it does show a list of courses fetched from the backend. If you'd like to see what I've changed from the original repo, you can look at the commits and files changed in this [pull request](https://github.com/UD-CISC474-F25/individual-project-starter/pull/21/files).

### Getting the new frontend working

1. First, pull my changes from upstream. You can try to do this from the GitHub UI, but I recommend adding my original repo as an upstream and then merging that upstream remote into your branch. It'll look something like this:

```console
git remote add upstream https://github.com/UD-CISC474-F25/individual-project-starter.git
git fetch upstream
git checkout main
get merge upstream/main
```

2. I modified a few files, and you can look over this PR to see what they are. You might get conflicts if you are doing more complicated things. Eventually, you should end up with a new `apps/web-start` folder, and you should also see that the `apps/web/package.json` and `apps/docs/package.json` files have been modified (to disable them from running when you use `npm run dev`).
3. You'll need to `cd apps/web-start` and run `npm install` to actually install the dependencies; you might be able to make it work from the top-level, but it didn't for me.
1. You might also need to `cd packages/api` and run `npm install` to get the shared DTOs working.
4. Now when you run `npm run dev` the first time, Vite will create a necessary file (`apps/web-start/src/routeTree.gen.ts`).
5. Install Tanstack/React Query Devtools extension. You can get links to the extension for your browser here: https://tanstack.com/query/latest/docs/framework/react/devtools (note that you do not need to follow the rest of those instructions)
6. You might need to modify your `.env` file in `apps/web-start` to have the following content:

```
VITE_BACKEND_URL="http://localhost:3000"
```

7. Now you will need to port over your frontend, or at least as much of it as you want. For more information about TanStack Start's routing, see: <https://tanstack.com/start/latest/docs/framework/react/routing>
8. You can use the `backendFetcher` function in `./integrations/fetcher.ts` to fetch data from the backend. It uses the `VITE_BACKEND_URL` environment variable to determine where to send requests. For more information about Tanstack/React Query, see: <https://tanstack.com/query/latest/docs/framework/react/guides/queries>

### Deploying

Once you are ready to deploy, you can use Cloudflare Workers for the frontend. The backend can still be deployed on Render.

1. Go to Cloudflare Workers: https://workers.cloudflare.com/
2. Sign up for a new account with Github.
3. Click "Get Started" next to "Import a Repository"
4. Select Github
4.1. Authorize the application
4.2. Install it on your personal account
4.3. Choose "Only select repositories" and find the 474 repo we are using this semester.
4.4. Install
5. For a second time, click "Get Started" next to "Import a Repository"
6. Choose the 474 repo from the list
7. Choose an appropriate name for your Repository.
8. Change the deploy command to the following: `npx wrangler deploy -c apps/web-start/dist/server/wrangler.json`
9. Click the Okay button at the bottom.
10. The site should deploy; you can use the box with a diagonal arrow to preview the site ("Preview the worker"). You can find this button with no label in the top-right corner.
11. You will also need to add a new VITE_BACKEND_URL environment variable to your **Build** Variables and Secrets (not just the runtime Variables and Secrets), or whatever you choose to use for giving the backend url to the frontend.

I had an error with my Render deploy, but this was fixed after I emptied the cache and redeployed. Make sure you update your backend's secrets to include the new origin URL for your frontend.

You are free to shut down your Vercel frontend. We won't need it anymore.

## Changes on October 19, 2025

Set up an account on Auth0. I recommend using your Github account to sign up.

Register your application. You will be making an API first (since we will do authentication through your Render backend).

- For the Name, you can use something like "F25 CISC474 Username Individual".
- For the Identifier, I recommend using the URL of your deployed Render application (e.g., `https://<whatever>.onrender.com/`)

Go to the Permissions tab, and also add a new permission (scope). For now, you can just make it something like `read:courses`. This will become important if you want to add finer-grained authorization to your application, but we'll keep things simple for now.

### Backend

Save, and now head back to VS Code. We need to create an authentication module, controller, and service in the `apps/api` package. CD into that folder and run the following commands:

```
nest g module auth
nest g controller auth
nest g service auth
```

You'll also need to install some new modules:

```
npm install @nestjs/jwt @nestjs/passport passport passport-auth0 passport-jwt jwks-rsa
npm install -D @types/passport-auth0 @types/passport-jwt @types/passport-jwt
```

Then copy over the `jwt.strategy.ts` file and the `current-user.decorator.ts` from the `apps/api/src/auth/` folder.

You'll need to add some new environment variables to your `apps/api/.env` file and your top-level `./.env` (and on Render itself, when you deploy) for `AUTH0_ISSUER_URL` and `AUTH0_AUDIENCE`. You can find their values in the example code they provide for the QuickStart. I recommend you look at the Node.JS tab and then get them from the `audience` and `issuerBaseURL` fields. Make sure that both of them have the trailing slash. These URLS _must_ match the ones that get sent by the frontend, so precision matters.

You'll need to modify your `auth.module.ts` file to have the appropriate import, providers, and exports.

Attach guards to all the Courses endpoints, for now. This let's you verify that you've set things up correctly. If you have, then you won't be able to access any of the API routes due to authorization errors. Keep in mind that the verb you use affects whether you are accessing a given route (e.g., the `courses/create/` route is expecting POST, not GET, so you cannot easily access it from your regular browser navigation).

You'll also be able to now attach the `@CurrentUser() user: JwtUser` parameter to endpoints, so that you can access the current user information.

One critical other route to add is `users/me` over in `users.controller.ts`. This let's you let a user get their current information.

### Frontend

Now we need to set up the client application for the frontend SPA. Back on the Auth0 dashboard, go to Applications and create a new Application of the type "Single Page Application".

In the following menu, go to Settings. Then go down to "Application URIs". For the Allowed Callback URLs, you will need to provide at least your frontend's localhost and Cloudflare backend, something like:

http://localhost:3001/home,https://cisc474-f25-acbart.acbart.workers.dev/home

Notice we are using the frontend, not the backend, since that is where the redirects are being handled.

You can use the same values for the Allowed Logout URLs.

For the Allowed Web Origins, you will need to provide just the localhost and CW frontend domains. Make sure you include the port for localhost, and no trailing slashes:

http://localhost:3001,https://cisc474-f25-acbart.acbart.workers.dev

Save, and let's go back to VS Code. This time we'll be in the frontend (apps/web-start). I followed the Quickstart instructions for React, but I'll give you the summarized version here. We'll need to install the auth0 sdk for React.

```
npm install @auth0/auth0-react
```

We're going to need to inject an `Auth0Provider` into the context of our application's root. So open up `router.tsx` and wrap the provider around the existing `TanStackQuery.Provider`. Note that this means you need environment variables for your frontend. You can get the values from the Settings menu.

I've made some modifications to my `backendFetcher` to turn it into `api.ts`, and it now handles all the authentication stuff for us (including getting the token). If you take advantage of my changes, you'll now see errors when you try to navigate to `localhost:3001/courses/` (because we're not authenticated yet). It might take a while, because the `useQuery` function will helpfully try a few times before it gives up.

Next you'll need to add a new LoginButton component. I added this in a new `apps/web-start/components/` directory. Then, I incorporate that button into my `apps/web-start/routes/index.tsx` page. I also created a LogoutButton at the same time.

I also needed to create a `apps/web-start/routes/home.tsx` page, which will be able to take advantage of the information I get from the login process. Normally, you would then use that information to populate the database record in the backend properly. But you will see that I am going to instead just leave that information client-side. You could create a form that let's a user update their information, which can then be pulled with the `users/me` endpoint.

The flow will be that the user clicks the login button, that takes them to Auth0 for login, that redirects to `home` on the frontend, and then they can access routes that have guarded backends.

Here's the diff of all the things I ended up doing: <https://github.com/acbart/cisc474-f25-individual-project-starter/compare/fcf00a8a0e8c5db0aaa1cedb025cd0386f885f4d...main>
16 changes: 15 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@
"@isaacs/brace-expansion": "^5.0.0",
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/jwt": "^11.0.1",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.0",
"@repo/api": "*",
"ansis": "^4.1.0",
"jwks-rsa": "^3.2.0",
"passport": "^0.7.0",
"passport-auth0": "^1.4.4",
"passport-google-oauth2": "^0.2.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
Expand All @@ -36,6 +46,10 @@
"@types/express": "^4.17.17",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/passport-auth0": "^1.0.9",
"@types/passport-google-oauth2": "^0.1.10",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^6.0.0",
"jest": "^29.7.0",
"source-map-support": "^0.5.21",
Expand Down
11 changes: 7 additions & 4 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Module } from '@nestjs/common';

import { LinksModule } from './links/links.module';

import { AppService } from './app.service';
import { AppController } from './app.controller';
import { UsersModule } from './users/users.module';
import { CoursesModule } from './courses/courses.module';
import { AssignmentsModule } from './assignments/assignments.module';
import { GroupsModule } from './groups/groups.module';
import { SubmissionsModule } from './submissions/submissions.module';
import { AuthModule } from './auth/auth.module';

@Module({
imports: [LinksModule],
imports: [UsersModule, CoursesModule, AssignmentsModule, GroupsModule, SubmissionsModule, AuthModule],
controllers: [AppController],
providers: [AppService],
})
Expand Down
20 changes: 20 additions & 0 deletions apps/api/src/assignments/assignments.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AssignmentsController } from './assignments.controller';
import { AssignmentsService } from './assignments.service';

describe('AssignmentsController', () => {
let controller: AssignmentsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AssignmentsController],
providers: [AssignmentsService],
}).compile();

controller = module.get<AssignmentsController>(AssignmentsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
16 changes: 16 additions & 0 deletions apps/api/src/assignments/assignments.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Controller, Get } from '@nestjs/common';
import { AssignmentsService } from './assignments.service';

@Controller('assignments')
export class AssignmentsController {
constructor(private readonly assignmentsService: AssignmentsService) {}

@Get()
findAll() {
return this.assignmentsService.findAll();
}
@Get(':id')
findOne(id: string) {
return this.assignmentsService.findOne(id);
}
}
10 changes: 10 additions & 0 deletions apps/api/src/assignments/assignments.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AssignmentsService } from './assignments.service';
import { AssignmentsController } from './assignments.controller';
import { PrismaService } from 'src/prisma.service';

@Module({
controllers: [AssignmentsController],
providers: [AssignmentsService, PrismaService],
})
export class AssignmentsModule {}
18 changes: 18 additions & 0 deletions apps/api/src/assignments/assignments.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AssignmentsService } from './assignments.service';

describe('AssignmentsService', () => {
let service: AssignmentsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AssignmentsService],
}).compile();

service = module.get<AssignmentsService>(AssignmentsService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
17 changes: 17 additions & 0 deletions apps/api/src/assignments/assignments.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma.service';

@Injectable()
export class AssignmentsService {
constructor(private prisma: PrismaService) {}

findAll() {
return this.prisma.assignment.findMany();
}

findOne(id: string) {
return this.prisma.assignment.findUnique({
where: { id },
});
}
}
18 changes: 18 additions & 0 deletions apps/api/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';

describe('AuthController', () => {
let controller: AuthController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();

controller = module.get<AuthController>(AuthController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
4 changes: 4 additions & 0 deletions apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';

@Controller('auth')
export class AuthController {}
14 changes: 14 additions & 0 deletions apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { PrismaService } from 'src/prisma.service';

@Module({
imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, PrismaService],
exports: [PassportModule],
})
export class AuthModule {}
18 changes: 18 additions & 0 deletions apps/api/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';

describe('AuthService', () => {
let service: AuthService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();

service = module.get<AuthService>(AuthService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
4 changes: 4 additions & 0 deletions apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class AuthService {}
9 changes: 9 additions & 0 deletions apps/api/src/auth/current-user.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { JwtUser } from './jwt.strategy';

export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext): JwtUser => {
const req = ctx.switchToHttp().getRequest();
return req.user as JwtUser;
},
);
Loading