mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 12:43:12 +08:00
feat: scaffold monorepo
This commit is contained in:
10
apps/portal/.env.local.example
Normal file
10
apps/portal/.env.local.example
Normal file
@ -0,0 +1,10 @@
|
||||
# Prisma
|
||||
DATABASE_URL=postgresql://postgres:[PASSWORD]@localhost:5432/postgres
|
||||
|
||||
# Next Auth
|
||||
NEXTAUTH_SECRET=any_string_you_wish
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# Next Auth GitHub Provider
|
||||
GITHUB_CLIENT_ID=a5164b1943b5413ff2f5
|
||||
GITHUB_CLIENT_SECRET=
|
8
apps/portal/.eslintrc.js
Normal file
8
apps/portal/.eslintrc.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['tih', 'next/core-web-vitals'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
};
|
40
apps/portal/.gitignore
vendored
Normal file
40
apps/portal/.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
50
apps/portal/README.md
Normal file
50
apps/portal/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Create T3 App
|
||||
|
||||
This is an app bootstrapped according to the [init.tips](https://init.tips) stack, also known as the T3-Stack.
|
||||
|
||||
## Why are there `.js` files in here?
|
||||
|
||||
As per [T3-Axiom #3](https://github.com/t3-oss/create-t3-app/tree/next#3-typesafety-isnt-optional), we take typesafety as a first class citizen. Unfortunately, not all frameworks and plugins support TypeScript which means some of the configuration files have to be `.js` files.
|
||||
|
||||
We try to emphasize that these files are javascript for a reason, by explicitly declaring its type (`cjs` or `mjs`) depending on what's supported by the library it is used by. Also, all the `js` files in this project are still typechecked using a `@ts-check` comment at the top.
|
||||
|
||||
## What's next? How do I make an app with this?
|
||||
|
||||
We try to keep this project as simple as possible, so you can start with the most basic configuration and then move on to more advanced configuration.
|
||||
|
||||
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
|
||||
|
||||
- [Next-Auth.js](https://next-auth.js.org)
|
||||
- [Prisma](https://prisma.io)
|
||||
- [TailwindCSS](https://tailwindcss.com)
|
||||
- [tRPC](https://trpc.io) (using @next version? [see v10 docs here](https://trpc.io/docs/v10/))
|
||||
|
||||
Also checkout these awesome tutorials on `create-t3-app`.
|
||||
|
||||
- [Build a Blog With the T3 Stack - tRPC, TypeScript, Next.js, Prisma & Zod](https://www.youtube.com/watch?v=syEWlxVFUrY)
|
||||
- [Build a Live Chat Application with the T3 Stack - TypeScript, Tailwind, tRPC](https://www.youtube.com/watch?v=dXRRY37MPuk)
|
||||
- [Build a full stack app with create-t3-app](https://www.nexxel.dev/blog/ct3a-guestbook)
|
||||
- [A first look at create-t3-app](https://dev.to/ajcwebdev/a-first-look-at-create-t3-app-1i8f)
|
||||
|
||||
## How do I deploy this?
|
||||
|
||||
### Vercel
|
||||
|
||||
We recommend deploying to [Vercel](https://vercel.com/?utm_source=t3-oss&utm_campaign=oss). It makes it super easy to deploy NextJs apps.
|
||||
|
||||
- Push your code to a GitHub repository.
|
||||
- Go to [Vercel](https://vercel.com/?utm_source=t3-oss&utm_campaign=oss) and sign up with GitHub.
|
||||
- Create a Project and import the repository you pushed your code to.
|
||||
- Add your environment variables.
|
||||
- Click **Deploy**
|
||||
- Now whenever you push a change to your repository, Vercel will automatically redeploy your website!
|
||||
|
||||
### Docker
|
||||
|
||||
You can also dockerize this stack and deploy a container. See the [Docker deployment page](https://create-t3-app-nu.vercel.app/en/deployment/docker) for details.
|
||||
|
||||
## Useful resources
|
||||
|
||||
Here are some resources that we commonly refer to:
|
||||
|
||||
- [Protecting routes with Next-Auth.js](https://next-auth.js.org/configuration/nextjs#unstable_getserversession)
|
5
apps/portal/next-env.d.ts
vendored
Normal file
5
apps/portal/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
18
apps/portal/next.config.mjs
Normal file
18
apps/portal/next.config.mjs
Normal file
@ -0,0 +1,18 @@
|
||||
import { env } from './src/env/server.mjs';
|
||||
|
||||
/**
|
||||
* Don't be scared of the generics here.
|
||||
* All they do is to give us autocompletion when using this.
|
||||
*
|
||||
* @template {import('next').NextConfig} T
|
||||
* @param {T} config - A generic parameter that flows through to the return type
|
||||
* @constraint {{import('next').NextConfig}}
|
||||
*/
|
||||
function defineNextConfig(config) {
|
||||
return config;
|
||||
}
|
||||
|
||||
export default defineNextConfig({
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
});
|
6974
apps/portal/package-lock.json
generated
Normal file
6974
apps/portal/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
apps/portal/package.json
Normal file
44
apps/portal/package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@tih/portal",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"tsc": "tsc",
|
||||
"postinstall": "prisma generate",
|
||||
"prisma": "prisma"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-adapter": "^1.0.4",
|
||||
"@prisma/client": "^4.4.0",
|
||||
"@tih/ui": "*",
|
||||
"@trpc/client": "^9.27.2",
|
||||
"@trpc/next": "^9.27.2",
|
||||
"@trpc/react": "^9.27.2",
|
||||
"@trpc/server": "^9.27.2",
|
||||
"next": "12.3.1",
|
||||
"next-auth": "~4.10.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-query": "^3.39.2",
|
||||
"superjson": "^1.10.0",
|
||||
"zod": "^3.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tih/tsconfig": "*",
|
||||
"@types/node": "18.0.0",
|
||||
"@types/react": "18.0.21",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"postcss": "^8.4.16",
|
||||
"prisma": "^4.4.0",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "4.8.3"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "5.13.1"
|
||||
}
|
||||
}
|
6
apps/portal/postcss.config.cjs
Normal file
6
apps/portal/postcss.config.cjs
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
@ -0,0 +1,73 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Example" (
|
||||
"id" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Example_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Account" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerAccountId" TEXT NOT NULL,
|
||||
"refresh_token" TEXT,
|
||||
"access_token" TEXT,
|
||||
"expires_at" INTEGER,
|
||||
"token_type" TEXT,
|
||||
"scope" TEXT,
|
||||
"id_token" TEXT,
|
||||
"session_state" TEXT,
|
||||
|
||||
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionToken" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"expires" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"email" TEXT,
|
||||
"emailVerified" TIMESTAMP(3),
|
||||
"image" TEXT,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VerificationToken" (
|
||||
"identifier" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"expires" TIMESTAMP(3) NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
3
apps/portal/prisma/migrations/migration_lock.toml
Normal file
3
apps/portal/prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
64
apps/portal/prisma/schema.prisma
Normal file
64
apps/portal/prisma/schema.prisma
Normal file
@ -0,0 +1,64 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
// NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.text annotations in model Account below
|
||||
// Further reading:
|
||||
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
|
||||
// https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Example {
|
||||
id String @id @default(cuid())
|
||||
}
|
||||
|
||||
// Necessary for Next auth
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? //@db.Text
|
||||
access_token String? //@db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? //@db.Text
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
BIN
apps/portal/public/favicon.ico
Normal file
BIN
apps/portal/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
38
apps/portal/src/env/client.mjs
vendored
Normal file
38
apps/portal/src/env/client.mjs
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
// @ts-check
|
||||
import { clientEnv, clientSchema } from './schema.mjs';
|
||||
|
||||
const _clientEnv = clientSchema.safeParse(clientEnv);
|
||||
|
||||
export const formatErrors = (
|
||||
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
|
||||
errors,
|
||||
) =>
|
||||
Object.entries(errors)
|
||||
.map(([name, value]) => {
|
||||
if (value && '_errors' in value)
|
||||
return `${name}: ${value._errors.join(', ')}\n`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (_clientEnv.success === false) {
|
||||
console.error(
|
||||
'❌ Invalid environment variables:\n',
|
||||
...formatErrors(_clientEnv.error.format()),
|
||||
);
|
||||
throw new Error('Invalid environment variables');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that client-side environment variables are exposed to the client.
|
||||
*/
|
||||
for (let key of Object.keys(_clientEnv.data)) {
|
||||
if (!key.startsWith('NEXT_PUBLIC_')) {
|
||||
console.warn(
|
||||
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`,
|
||||
);
|
||||
|
||||
throw new Error('Invalid public environment variable name');
|
||||
}
|
||||
}
|
||||
|
||||
export const env = _clientEnv.data;
|
34
apps/portal/src/env/schema.mjs
vendored
Normal file
34
apps/portal/src/env/schema.mjs
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Specify your server-side environment variables schema here.
|
||||
* This way you can ensure the app isn't built with invalid env vars.
|
||||
*/
|
||||
export const serverSchema = z.object({
|
||||
DATABASE_URL: z.string().url(),
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']),
|
||||
NEXTAUTH_SECRET: z.string(),
|
||||
NEXTAUTH_URL: z.string().url(),
|
||||
GITHUB_CLIENT_ID: z.string(),
|
||||
GITHUB_CLIENT_SECRET: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Specify your client-side environment variables schema here.
|
||||
* This way you can ensure the app isn't built with invalid env vars.
|
||||
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
|
||||
*/
|
||||
export const clientSchema = z.object({
|
||||
// NEXT_PUBLIC_BAR: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* You can't destruct `process.env` as a regular object, so you have to do
|
||||
* it manually here. This is because Next.js evaluates this at build time,
|
||||
* and only used environment variables are included in the build.
|
||||
* @type {{ [k in keyof z.infer<typeof clientSchema>]: z.infer<typeof clientSchema>[k] | undefined }}
|
||||
*/
|
||||
export const clientEnv = {
|
||||
// NEXT_PUBLIC_BAR: process.env.NEXT_PUBLIC_BAR,
|
||||
};
|
30
apps/portal/src/env/server.mjs
vendored
Normal file
30
apps/portal/src/env/server.mjs
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars.
|
||||
* It has to be a `.mjs`-file to be imported there.
|
||||
*/
|
||||
import { serverSchema } from './schema.mjs';
|
||||
import { env as clientEnv, formatErrors } from './client.mjs';
|
||||
|
||||
const _serverEnv = serverSchema.safeParse(process.env);
|
||||
|
||||
if (_serverEnv.success === false) {
|
||||
console.error(
|
||||
'❌ Invalid environment variables:\n',
|
||||
...formatErrors(_serverEnv.error.format()),
|
||||
);
|
||||
throw new Error('Invalid environment variables');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that server-side environment variables are not exposed to the client.
|
||||
*/
|
||||
for (let key of Object.keys(_serverEnv.data)) {
|
||||
if (key.startsWith('NEXT_PUBLIC_')) {
|
||||
console.warn('❌ You are exposing a server-side env-variable:', key);
|
||||
|
||||
throw new Error('You are exposing a server-side env-variable');
|
||||
}
|
||||
}
|
||||
|
||||
export const env = { ..._serverEnv.data, ...clientEnv };
|
76
apps/portal/src/pages/_app.tsx
Normal file
76
apps/portal/src/pages/_app.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import type { AppType } from 'next/app';
|
||||
import type { Session } from 'next-auth';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import superjson from 'superjson';
|
||||
import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
|
||||
import { loggerLink } from '@trpc/client/links/loggerLink';
|
||||
import { withTRPC } from '@trpc/next';
|
||||
|
||||
import type { AppRouter } from '~/server/router';
|
||||
|
||||
import '~/styles/globals.css';
|
||||
|
||||
const MyApp: AppType<{ session: Session | null }> = ({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
}) => {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<Component {...pageProps} />
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const getBaseUrl = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return '';
|
||||
} // Browser should use relative url
|
||||
if (process.env.VERCEL_URL) {
|
||||
return `https://${process.env.VERCEL_URL}`;
|
||||
} // SSR should use vercel url
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`; // Dev SSR should use localhost
|
||||
};
|
||||
|
||||
export default withTRPC<AppRouter>({
|
||||
config({ ctx: _ctx }) {
|
||||
/**
|
||||
* If you want to use SSR, you need to use the server's full URL
|
||||
* @link https://trpc.io/docs/ssr
|
||||
*/
|
||||
const url = `${getBaseUrl()}/api/trpc`;
|
||||
|
||||
return {
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
(opts.direction === 'down' && opts.result instanceof Error),
|
||||
}),
|
||||
httpBatchLink({ url }),
|
||||
],
|
||||
transformer: superjson,
|
||||
url,
|
||||
/**
|
||||
* @link https://react-query.tanstack.com/reference/QueryClient
|
||||
*/
|
||||
// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
|
||||
|
||||
// To use SSR properly you need to forward the client's headers to the server
|
||||
// headers: () => {
|
||||
// if (ctx?.req) {
|
||||
// const headers = ctx?.req?.headers;
|
||||
// delete headers?.connection;
|
||||
// return {
|
||||
// ...headers,
|
||||
// "x-ssr": "1",
|
||||
// };
|
||||
// }
|
||||
// return {};
|
||||
// }
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @link https://trpc.io/docs/ssr
|
||||
*/
|
||||
ssr: false,
|
||||
})(MyApp);
|
31
apps/portal/src/pages/api/auth/[...nextauth].ts
Normal file
31
apps/portal/src/pages/api/auth/[...nextauth].ts
Normal file
@ -0,0 +1,31 @@
|
||||
import NextAuth, { type NextAuthOptions } from 'next-auth';
|
||||
import GitHubProvider from 'next-auth/providers/github';
|
||||
// Prisma adapter for NextAuth, optional and can be removed
|
||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
||||
|
||||
import { env } from '../../../env/server.mjs';
|
||||
import { prisma } from '../../../server/db/client';
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
// Configure one or more authentication providers
|
||||
adapter: PrismaAdapter(prisma),
|
||||
|
||||
// Include user.id on session
|
||||
callbacks: {
|
||||
session({ session, user }) {
|
||||
if (session.user) {
|
||||
session.user.id = user.id;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
providers: [
|
||||
GitHubProvider({
|
||||
clientId: env.GITHUB_CLIENT_ID,
|
||||
clientSecret: env.GITHUB_CLIENT_SECRET,
|
||||
}),
|
||||
// ...add more providers here
|
||||
],
|
||||
};
|
||||
|
||||
export default NextAuth(authOptions);
|
11
apps/portal/src/pages/api/examples.ts
Normal file
11
apps/portal/src/pages/api/examples.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// Src/pages/api/examples.ts
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { prisma } from '../../server/db/client';
|
||||
|
||||
const examples = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const examplesFromDb = await prisma.example.findMany();
|
||||
res.status(200).json(examplesFromDb);
|
||||
};
|
||||
|
||||
export default examples;
|
23
apps/portal/src/pages/api/restricted.ts
Normal file
23
apps/portal/src/pages/api/restricted.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// Example of a restricted endpoint that only authenticated users can access from https://next-auth.js.org/getting-started/example
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { getServerAuthSession } from '../../server/common/get-server-auth-session';
|
||||
|
||||
const restricted = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
|
||||
if (session) {
|
||||
res.send({
|
||||
content:
|
||||
'This is protected content. You can access this content because you are signed in.',
|
||||
});
|
||||
} else {
|
||||
res.send({
|
||||
error:
|
||||
'You must be signed in to view the protected content on this page.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default restricted;
|
18
apps/portal/src/pages/api/trpc/[trpc].ts
Normal file
18
apps/portal/src/pages/api/trpc/[trpc].ts
Normal file
@ -0,0 +1,18 @@
|
||||
// Src/pages/api/trpc/[trpc].ts
|
||||
import { createNextApiHandler } from '@trpc/server/adapters/next';
|
||||
|
||||
import { env } from '../../../env/server.mjs';
|
||||
import { appRouter } from '../../../server/router';
|
||||
import { createContext } from '../../../server/router/context';
|
||||
|
||||
// Export API handler
|
||||
export default createNextApiHandler({
|
||||
createContext,
|
||||
onError:
|
||||
env.NODE_ENV === 'development'
|
||||
? ({ path, error }) => {
|
||||
console.error(`❌ tRPC failed on ${path}: ${error}`);
|
||||
}
|
||||
: undefined,
|
||||
router: appRouter,
|
||||
});
|
91
apps/portal/src/pages/index.tsx
Normal file
91
apps/portal/src/pages/index.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import type { NextPage } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { CounterButton } from '@tih/ui';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const hello = trpc.useQuery(['example.hello', { text: 'from tRPC!' }]);
|
||||
const getAll = trpc.useQuery(['example.getAll']);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create T3 App</title>
|
||||
<meta content="Generated by create-t3-app" name="description" />
|
||||
<link href="/favicon.ico" rel="icon" />
|
||||
</Head>
|
||||
<main className="container mx-auto flex flex-col items-center justify-center min-h-screen p-4">
|
||||
<h1 className="text-5xl md:text-[5rem] leading-normal font-extrabold text-gray-700">
|
||||
Create <span className="text-purple-300">T3</span> App
|
||||
</h1>
|
||||
<CounterButton />
|
||||
<p className="text-2xl text-gray-700">This stack uses:</p>
|
||||
<div className="grid gap-3 pt-3 mt-3 text-center md:grid-cols-2 lg:w-2/3">
|
||||
<TechnologyCard
|
||||
description="The React framework for production"
|
||||
documentation="https://nextjs.org/"
|
||||
name="NextJS"
|
||||
/>
|
||||
<TechnologyCard
|
||||
description="Strongly typed programming language that builds on JavaScript, giving you better tooling at any scale"
|
||||
documentation="https://www.typescriptlang.org/"
|
||||
name="TypeScript"
|
||||
/>
|
||||
<TechnologyCard
|
||||
description="Rapidly build modern websites without ever leaving your HTML"
|
||||
documentation="https://tailwindcss.com/"
|
||||
name="TailwindCSS"
|
||||
/>
|
||||
<TechnologyCard
|
||||
description="End-to-end typesafe APIs made easy"
|
||||
documentation="https://trpc.io/"
|
||||
name="tRPC"
|
||||
/>
|
||||
<TechnologyCard
|
||||
description="Authentication for Next.js"
|
||||
documentation="https://next-auth.js.org/"
|
||||
name="Next-Auth"
|
||||
/>
|
||||
<TechnologyCard
|
||||
description="Build data-driven JavaScript & TypeScript apps in less time"
|
||||
documentation="https://www.prisma.io/docs/"
|
||||
name="Prisma"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-6 text-2xl text-blue-500 flex justify-center items-center w-full">
|
||||
{hello.data ? <p>{hello.data.greeting}</p> : <p>Loading..</p>}
|
||||
</div>
|
||||
<pre className="w-1/2">{JSON.stringify(getAll.data, null, 2)}</pre>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
type TechnologyCardProps = {
|
||||
description: string;
|
||||
documentation: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function TechnologyCard({
|
||||
name,
|
||||
description,
|
||||
documentation,
|
||||
}: TechnologyCardProps) {
|
||||
return (
|
||||
<section className="flex flex-col justify-center p-6 duration-500 border-2 border-gray-500 rounded shadow-xl motion-safe:hover:scale-105">
|
||||
<h2 className="text-lg text-gray-700">{name}</h2>
|
||||
<p className="text-sm text-gray-600">{description}</p>
|
||||
<a
|
||||
className="mt-3 text-sm underline text-violet-500 decoration-dotted underline-offset-2"
|
||||
href={documentation}
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</section>
|
||||
);
|
||||
}
|
15
apps/portal/src/server/common/get-server-auth-session.ts
Normal file
15
apps/portal/src/server/common/get-server-auth-session.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs
|
||||
|
||||
import type { GetServerSidePropsContext } from 'next';
|
||||
// eslint-disable-next-line camelcase
|
||||
import { unstable_getServerSession } from 'next-auth';
|
||||
|
||||
import { authOptions as nextAuthOptions } from '~/pages/api/auth/[...nextauth]';
|
||||
|
||||
// Next API route example - /pages/api/restricted.ts
|
||||
export const getServerAuthSession = async (ctx: {
|
||||
req: GetServerSidePropsContext['req'];
|
||||
res: GetServerSidePropsContext['res'];
|
||||
}) => {
|
||||
return await unstable_getServerSession(ctx.req, ctx.res, nextAuthOptions);
|
||||
};
|
20
apps/portal/src/server/db/client.ts
Normal file
20
apps/portal/src/server/db/client.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// Src/server/db/client.ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { env } from '~/env/server.mjs';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var, init-declarations
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
export const prisma =
|
||||
global.prisma ||
|
||||
new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
|
||||
if (env.NODE_ENV !== 'production') {
|
||||
global.prisma = prisma;
|
||||
}
|
61
apps/portal/src/server/router/context.ts
Normal file
61
apps/portal/src/server/router/context.ts
Normal file
@ -0,0 +1,61 @@
|
||||
// Src/server/router/context.ts
|
||||
import type { Session } from 'next-auth';
|
||||
import * as trpc from '@trpc/server';
|
||||
import type * as trpcNext from '@trpc/server/adapters/next';
|
||||
|
||||
import { getServerAuthSession } from '~/server/common/get-server-auth-session';
|
||||
import { prisma } from '~/server/db/client';
|
||||
|
||||
type CreateContextOptions = {
|
||||
session: Session | null;
|
||||
};
|
||||
|
||||
/** Use this helper for:
|
||||
* - testing, where we dont have to Mock Next.js' req/res
|
||||
* - trpc's `createSSGHelpers` where we don't have req/res
|
||||
**/
|
||||
export const createContextInner = async (opts: CreateContextOptions) => {
|
||||
return {
|
||||
prisma,
|
||||
session: opts.session,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the actual context you'll use in your router
|
||||
* @link https://trpc.io/docs/context
|
||||
**/
|
||||
export const createContext = async (
|
||||
opts: trpcNext.CreateNextContextOptions,
|
||||
) => {
|
||||
const { req, res } = opts;
|
||||
|
||||
// Get the session from the server using the unstable_getServerSession wrapper function
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
|
||||
return await createContextInner({
|
||||
session,
|
||||
});
|
||||
};
|
||||
|
||||
type Context = trpc.inferAsyncReturnType<typeof createContext>;
|
||||
|
||||
export const createRouter = () => trpc.router<Context>();
|
||||
|
||||
/**
|
||||
* Creates a tRPC router that asserts all queries and mutations are from an authorized user. Will throw an unauthorized error if a user is not signed in.
|
||||
**/
|
||||
export function createProtectedRouter() {
|
||||
return createRouter().middleware(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.session.user) {
|
||||
throw new trpc.TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
...ctx,
|
||||
// Infers that `session` is non-nullable to downstream resolvers
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
23
apps/portal/src/server/router/example.ts
Normal file
23
apps/portal/src/server/router/example.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createRouter } from './context';
|
||||
|
||||
export const exampleRouter = createRouter()
|
||||
.query('hello', {
|
||||
input: z
|
||||
.object({
|
||||
text: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
resolve({ input }) {
|
||||
return {
|
||||
greeting: `Hello ${input?.text ?? 'world'}`,
|
||||
};
|
||||
},
|
||||
})
|
||||
.query('getAll', {
|
||||
async resolve({ ctx }) {
|
||||
const items = await ctx.prisma.example.findMany();
|
||||
return items;
|
||||
},
|
||||
});
|
14
apps/portal/src/server/router/index.ts
Normal file
14
apps/portal/src/server/router/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// Src/server/router/index.ts
|
||||
import superjson from 'superjson';
|
||||
|
||||
import { createRouter } from './context';
|
||||
import { exampleRouter } from './example';
|
||||
import { protectedExampleRouter } from './protected-example-router';
|
||||
|
||||
export const appRouter = createRouter()
|
||||
.transformer(superjson)
|
||||
.merge('example.', exampleRouter)
|
||||
.merge('auth.', protectedExampleRouter);
|
||||
|
||||
// Export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
14
apps/portal/src/server/router/protected-example-router.ts
Normal file
14
apps/portal/src/server/router/protected-example-router.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { createProtectedRouter } from './context';
|
||||
|
||||
// Example router with queries that can only be hit if the user requesting is signed in
|
||||
export const protectedExampleRouter = createProtectedRouter()
|
||||
.query('getSession', {
|
||||
resolve({ ctx }) {
|
||||
return ctx.session;
|
||||
},
|
||||
})
|
||||
.query('getSecretMessage', {
|
||||
resolve({ ctx: _ctx }) {
|
||||
return 'He who asks a question is a fool for five minutes; he who does not ask a question remains a fool forever.';
|
||||
},
|
||||
});
|
3
apps/portal/src/styles/globals.css
Normal file
3
apps/portal/src/styles/globals.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
12
apps/portal/src/types/next-auth.d.ts
vendored
Normal file
12
apps/portal/src/types/next-auth.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
import type { DefaultSession } from 'next-auth';
|
||||
|
||||
declare module 'next-auth' {
|
||||
/**
|
||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||
*/
|
||||
type Session = {
|
||||
user?: DefaultSession['user'] & {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
}
|
27
apps/portal/src/utils/trpc.ts
Normal file
27
apps/portal/src/utils/trpc.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// Src/utils/trpc.ts
|
||||
import { createReactQueryHooks } from '@trpc/react';
|
||||
import type { inferProcedureInput, inferProcedureOutput } from '@trpc/server';
|
||||
|
||||
import type { AppRouter } from '~/server/router';
|
||||
|
||||
export const trpc = createReactQueryHooks<AppRouter>();
|
||||
|
||||
/**
|
||||
* These are helper types to infer the input and output of query resolvers
|
||||
* @example type HelloOutput = inferQueryOutput<'hello'>
|
||||
*/
|
||||
export type inferQueryOutput<
|
||||
TRouteKey extends keyof AppRouter['_def']['queries'],
|
||||
> = inferProcedureOutput<AppRouter['_def']['queries'][TRouteKey]>;
|
||||
|
||||
export type inferQueryInput<
|
||||
TRouteKey extends keyof AppRouter['_def']['queries'],
|
||||
> = inferProcedureInput<AppRouter['_def']['queries'][TRouteKey]>;
|
||||
|
||||
export type inferMutationOutput<
|
||||
TRouteKey extends keyof AppRouter['_def']['mutations'],
|
||||
> = inferProcedureOutput<AppRouter['_def']['mutations'][TRouteKey]>;
|
||||
|
||||
export type inferMutationInput<
|
||||
TRouteKey extends keyof AppRouter['_def']['mutations'],
|
||||
> = inferProcedureInput<AppRouter['_def']['mutations'][TRouteKey]>;
|
8
apps/portal/tailwind.config.cjs
Normal file
8
apps/portal/tailwind.config.cjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
13
apps/portal/tsconfig.json
Normal file
13
apps/portal/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"exclude": ["node_modules"],
|
||||
"extends": "@tih/tsconfig/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"~/*": ["*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "next-env.d.ts"]
|
||||
}
|
Reference in New Issue
Block a user