Skip to content

Commit 4516f89

Browse files
committed
add basic database schema and auth setup
1 parent b550ca0 commit 4516f89

File tree

10 files changed

+797
-109
lines changed

10 files changed

+797
-109
lines changed

package-lock.json

Lines changed: 527 additions & 106 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@auth/drizzle-adapter": "^0.3.2",
1919
"@hookform/resolvers": "^3.3.2",
2020
"@neondatabase/serverless": "^0.10.2",
21-
"@next/third-parties": "^14.2.15",
21+
"@next/third-parties": "^15.4.6",
2222
"@radix-ui/react-alert-dialog": "^1.0.5",
2323
"@radix-ui/react-dialog": "^1.1.2",
2424
"@radix-ui/react-dropdown-menu": "^2.0.6",
@@ -37,8 +37,8 @@
3737
"dotenv": "^16.0.3",
3838
"drizzle-orm": "^0.44.3",
3939
"drizzle-zod": "^0.8.2",
40-
"next": "^14.2.2",
41-
"next-auth": "^4.23.0",
40+
"next": "^15.4.6",
41+
"next-auth": "^4.24.11",
4242
"postgres": "^3.3.5",
4343
"react": "^18.2.0",
4444
"react-day-picker": "^8.9.1",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import NextAuth from 'next-auth';
2+
3+
import { authOptions } from '@src/server/auth';
4+
5+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
6+
const handler = NextAuth(authOptions);
7+
export { handler as GET, handler as POST };

src/server/auth.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
getServerSession,
3+
type NextAuthOptions,
4+
type DefaultSession,
5+
} from 'next-auth';
6+
import GoogleProvider from 'next-auth/providers/google';
7+
import DiscordProvider from 'next-auth/providers/discord';
8+
import { env } from '@src/env.mjs';
9+
import { DrizzleAdapter } from '@auth/drizzle-adapter';
10+
import { db } from './db';
11+
import { eq } from 'drizzle-orm';
12+
import { type SelectUserMetadata, type InsertUserMetadata } from './db/models';
13+
import { pgTable } from 'drizzle-orm/pg-core';
14+
import { userMetadata } from './db/schema/user';
15+
16+
/**
17+
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
18+
* object and keep type safety.
19+
*
20+
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
21+
*/
22+
declare module 'next-auth' {
23+
interface Session extends DefaultSession {
24+
user: {
25+
id: string;
26+
// ...other properties
27+
// role: UserRole;
28+
} & DefaultSession['user'] &
29+
SelectUserMetadata;
30+
}
31+
32+
// interface User {
33+
// // ...other properties
34+
// // role: UserRole;
35+
// }
36+
}
37+
38+
export interface PreviewUser {
39+
id: string;
40+
name: string;
41+
email: string;
42+
image: string;
43+
}
44+
45+
/**
46+
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
47+
*
48+
* @see https://next-auth.js.org/configuration/options
49+
*/
50+
51+
export const authOptions: NextAuthOptions = {
52+
adapter: DrizzleAdapter(db, pgTable),
53+
callbacks: {
54+
async session({ session, user }) {
55+
let metadata = await db.query.userMetadata.findFirst({
56+
where: (metadata) => eq(metadata.id, user.id),
57+
});
58+
59+
if (!metadata) {
60+
const firstName = user.name?.split(' ')[0] ?? '';
61+
const lastName = user.name?.split(' ')[1] ?? '';
62+
63+
const insert: InsertUserMetadata = {
64+
firstName,
65+
lastName,
66+
id: user.id,
67+
major: 'Computer Science',
68+
};
69+
70+
metadata = (
71+
await db.insert(userMetadata).values(insert).returning()
72+
).at(0);
73+
}
74+
75+
if (session.user) {
76+
session.user = { ...session.user, ...metadata };
77+
// session.user.role = user.role; <-- put other properties on the session here
78+
}
79+
80+
return session;
81+
},
82+
},
83+
pages: {
84+
signIn: '/auth',
85+
},
86+
providers: [
87+
GoogleProvider({
88+
clientId: env.GOOGLE_CLIENT_ID,
89+
clientSecret: env.GOOGLE_CLIENT_SECRET,
90+
}),
91+
DiscordProvider({
92+
clientId: env.DISCORD_CLIENT_ID,
93+
clientSecret: env.DISCORD_CLIENT_SECRET,
94+
}),
95+
],
96+
97+
/**
98+
* ...add more providers here.
99+
*
100+
* Most other providers require a bit more work than the Discord provider. For example, the
101+
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
102+
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
103+
*
104+
* @see https://next-auth.js.org/providers/github
105+
*/
106+
};
107+
108+
/**
109+
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
110+
*
111+
* @see https://next-auth.js.org/configuration/nextjs
112+
*/
113+
export const getServerAuthSession = () => getServerSession(authOptions);

src/server/db/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { drizzle } from 'drizzle-orm/neon-http';
2+
import { env } from '@src/env.mjs';
3+
4+
import * as user from './schema/user';
5+
import * as file from './schema/file';
6+
import * as section from './schema/section';
7+
const schema = {
8+
...file,
9+
...section,
10+
...user,
11+
};
12+
13+
export const db = drizzle(env.DATABASE_URL, {
14+
schema,
15+
});

src/server/db/models.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { type z } from 'zod';
2+
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
3+
import { userMetadata } from './schema/user';
4+
5+
// Schema types for userMetadata
6+
export const insertUserMetadata = createInsertSchema(userMetadata);
7+
export const selectUserMetadata = createSelectSchema(userMetadata);
8+
9+
export type InsertUserMetadata = z.infer<typeof insertUserMetadata>;
10+
export type SelectUserMetadata = z.infer<typeof selectUserMetadata>;

src/server/db/schema/file.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { relations, sql } from 'drizzle-orm';
2+
import { pgTable, text } from 'drizzle-orm/pg-core';
3+
import { user } from './user';
4+
import { section } from './section';
5+
6+
export const file = pgTable('file', {
7+
id: text('id')
8+
.default(sql`nanoid(20)`)
9+
.primaryKey(),
10+
authorId: text('author_id')
11+
.notNull()
12+
.references(() => user.id),
13+
sectionId: text('section_id').references(() => section.id),
14+
file_url: text('file_url').notNull(),
15+
});
16+
17+
export const fileRelations = relations(file, ({ one }) => ({
18+
author: one(user),
19+
section: one(section),
20+
}));

src/server/db/schema/section.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { relations, sql } from 'drizzle-orm';
2+
import { pgTable, text } from 'drizzle-orm/pg-core';
3+
import { file } from './file';
4+
5+
export const section = pgTable('section', {
6+
id: text('id')
7+
.default(sql`nanoid(20)`)
8+
.primaryKey(),
9+
});
10+
11+
export const sectionRelations = relations(section, ({ many }) => ({
12+
files: many(file),
13+
}));

src/server/db/schema/user.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { relations } from 'drizzle-orm';
2+
import {
3+
integer,
4+
pgEnum,
5+
pgTable,
6+
primaryKey,
7+
text,
8+
timestamp,
9+
} from 'drizzle-orm/pg-core';
10+
import { type AdapterAccount } from 'next-auth/adapters';
11+
import { file } from './file';
12+
13+
export const yearEnum = pgEnum('year', [
14+
'Freshman',
15+
'Sophomore',
16+
'Junior',
17+
'Senior',
18+
'Grad Student',
19+
]);
20+
21+
export const user = pgTable('user', {
22+
id: text('id').notNull().primaryKey(),
23+
name: text('name'),
24+
email: text('email').notNull(),
25+
emailVerified: timestamp('emailVerified', { mode: 'date' }),
26+
image: text('image'),
27+
});
28+
29+
export const userMetadata = pgTable('user_metadata', {
30+
id: text('id').notNull().primaryKey(),
31+
firstName: text('first_name').notNull(),
32+
lastName: text('last_name').notNull(),
33+
major: text('major').notNull(),
34+
minor: text('minor'),
35+
year: yearEnum('year')
36+
.$default(() => 'Freshman')
37+
.notNull(),
38+
});
39+
40+
export const accounts = pgTable(
41+
'account',
42+
{
43+
userId: text('userId')
44+
.notNull()
45+
.references(() => user.id, { onDelete: 'cascade' }),
46+
type: text('type').$type<AdapterAccount['type']>().notNull(),
47+
provider: text('provider').notNull(),
48+
providerAccountId: text('providerAccountId').notNull(),
49+
refresh_token: text('refresh_token'),
50+
access_token: text('access_token'),
51+
expires_at: integer('expires_at'),
52+
token_type: text('token_type'),
53+
scope: text('scope'),
54+
id_token: text('id_token'),
55+
session_state: text('session_state'),
56+
},
57+
(t) => [primaryKey({ columns: [t.provider, t.providerAccountId] })],
58+
);
59+
60+
export const sessions = pgTable('session', {
61+
sessionToken: text('sessionToken').notNull().primaryKey(),
62+
userId: text('userId')
63+
.notNull()
64+
.references(() => user.id, { onDelete: 'cascade' }),
65+
expires: timestamp('expires', { mode: 'date' }).notNull(),
66+
});
67+
68+
export const verificationTokens = pgTable(
69+
'verificationToken',
70+
{
71+
identifier: text('identifier').notNull(),
72+
token: text('token').notNull(),
73+
expires: timestamp('expires', { mode: 'date' }).notNull(),
74+
},
75+
(vt) => [primaryKey({ columns: [vt.identifier, vt.token] })],
76+
);
77+
export const userMetadataRelations = relations(user, ({ many }) => ({
78+
files: many(file),
79+
}));

src/utils/redirect.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { headers } from 'next/headers';
2+
3+
export async function signInRoute(route: string) {
4+
const headerz = await headers();
5+
const host = headerz.get('X-Forwarded-Host');
6+
const proto = headerz.get('X-Forwarded-Proto');
7+
return `/auth?callbackUrl=${encodeURIComponent(
8+
`${proto}://${host}/${route}`,
9+
)}`;
10+
}

0 commit comments

Comments
 (0)