├── .env-example
├── .eslintrc.js
├── .gitignore
├── README.md
├── apps
├── docs
│ ├── .eslintrc.js
│ ├── README.md
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ │ ├── api
│ │ │ └── example.ts
│ │ └── index.tsx
│ └── tsconfig.json
├── mobile
│ ├── .env.example
│ ├── .eslintrc.json
│ ├── .expo-shared
│ │ └── assets.json
│ ├── .gitignore
│ ├── app.json
│ ├── assets
│ │ ├── adaptive-icon.png
│ │ ├── favicon.png
│ │ ├── icon.png
│ │ └── splash.png
│ ├── babel.config.js
│ ├── index.js
│ ├── metro.config.js
│ ├── package.json
│ ├── src
│ │ ├── App.tsx
│ │ ├── components
│ │ │ ├── LoginOptions.tsx
│ │ │ ├── Provider.tsx
│ │ │ └── index.ts
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ ├── useAppReady.tsx
│ │ │ └── useAuth.tsx
│ │ ├── screens
│ │ │ ├── LoginScreen.tsx
│ │ │ └── index.ts
│ │ ├── store
│ │ │ └── index.ts
│ │ ├── types
│ │ │ └── global.ts
│ │ └── utils
│ │ │ ├── ignore-warnings.ts
│ │ │ ├── secure-store.ts
│ │ │ └── trpc.ts
│ └── tsconfig.json
└── web
│ ├── .env-example
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ └── favicon.ico
│ ├── src
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── api
│ │ │ ├── auth
│ │ │ │ └── [...nextauth].ts
│ │ │ ├── examples.ts
│ │ │ ├── jwt.ts
│ │ │ ├── restricted.ts
│ │ │ └── trpc
│ │ │ │ └── [trpc].ts
│ │ └── index.tsx
│ ├── styles
│ │ └── globals.css
│ ├── types
│ │ └── next-auth.ts
│ └── utils
│ │ └── trpc.ts
│ ├── tailwind.config.js
│ └── tsconfig.json
├── package.json
├── packages
├── api
│ ├── .gitignore
│ ├── db
│ │ └── index.ts
│ ├── expo-auth
│ │ ├── apple-auth.ts
│ │ ├── constants.ts
│ │ ├── github-auth.ts
│ │ ├── google-auth.ts
│ │ ├── index.ts
│ │ ├── prisma-auth.ts
│ │ └── zod.ts
│ ├── index.ts
│ ├── next-auth
│ │ └── index.ts
│ ├── package.json
│ ├── router
│ │ ├── appRouter.ts
│ │ ├── context.ts
│ │ ├── example.ts
│ │ ├── expoAuth.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── types
│ │ └── global.ts
├── eslint-config-custom
│ ├── index.js
│ └── package.json
├── hooks
│ ├── index.ts
│ ├── package.json
│ ├── trpc.ts
│ └── tsconfig.json
├── tsconfig
│ ├── README.md
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ └── react-library.json
└── ui-web
│ ├── index.tsx
│ ├── package.json
│ ├── src
│ └── Button.tsx
│ └── tsconfig.json
├── prisma
└── schema.prisma
├── scripts
└── clean.sh
├── turbo.json
└── yarn.lock
/.env-example:
--------------------------------------------------------------------------------
1 | # Prisma
2 | DATABASE_URL=
3 |
4 | # NEXT AUTH
5 | NEXTAUTH_URL=http://localhost:3000
6 | NEXTAUTH_SECRET=
7 |
8 | ## Github
9 | GITHUB_ID=
10 | GITHUB_SECRET=
11 |
12 | ## Google
13 | GOOGLE_CLIENT_ID=
14 | GOOGLE_CLIENT_SECRET=
15 |
16 | BASE_URL=http://localhost:3000
17 |
18 | ## Email
19 | EMAIL_SERVER_USER=
20 | EMAIL_SERVER_REFRESH_TOKEN=
21 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | // This tells ESLint to load the config from the package `eslint-config-custom`
4 | extends: ["custom"],
5 | settings: {
6 | next: {
7 | rootDir: ["apps/*/"],
8 | },
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # testing
9 | coverage
10 |
11 | # next.js
12 | .next/
13 | out/
14 | build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .pnpm-debug.log*
25 |
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # turbo
33 | .turbo
34 |
35 | # SQLite
36 | db.sqlite
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Turbo-t3-expo
2 |
3 | This is an example monorepo for create-t3-app and expo. If you want to create your own from scratch, make sure all of your react, react-dom, @types/react, and @types/react-dom are at the same version as the expo app in other apps.
4 |
5 | ## What's inside?
6 |
7 | This turborepo uses [Yarn](https://classic.yarnpkg.com/lang/en/) as a package manager. It includes the following packages/apps:
8 |
9 | ### Apps and Packages
10 |
11 | - `docs`: a [Next.js](https://nextjs.org) app
12 | - `web`: a [create-t3-app](https://github.com/t3-oss/create-t3-app) app
13 | - `api`: a shared library for all apps. Includes a prisma client, a trpc router, next-auth options, and expo-auth functionalities
14 | - `ui-web`: a stub React component library shared by both `web` and `docs` applications
15 | - `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
16 | - `tsconfig`: `tsconfig.json`s used throughout the monorepo
17 | - `prisma`: a `schema.prisma` file with optional `db.sqlite`
18 |
19 | ## Next-auth with Expo
20 |
21 | This example app uses a lot of logic from Next-auth to implement the same/similar features for mobile. To make this possible, no cookies are used for authentication. Here's how it works:
22 |
23 | 1. From the Expo app, a request is made and the response is verified.
24 | 2. The Expo verfied response, is sent to a tRCP endpoint and verfied once again by the `.expo-auth` router.
25 | 3. Once successful, a JWT token is generated, the user and account is updated accordingly, and the new JWT and user is sent to the Expo app.
26 |
27 | ## Setup
28 |
29 | This repository can be cloned from https://github.com/mrzachnugent/turbo-t3-expo.
30 |
31 | ```
32 | git clone git@github.com:mrzachnugent/turbo-t3-expo.git
33 | cd turbo-t3-expo
34 | yarn
35 | ```
36 |
37 | ### Build
38 |
39 | To build all apps and packages, run the following command in the root folder:
40 |
41 | ```
42 | yarn run build
43 | ```
44 |
45 | ### Develop
46 |
47 | To develop all apps and packages, run the following command:
48 |
49 | ```
50 | cd turbo-t3-expo
51 | yarn run dev
52 | ```
53 |
54 | ### Remote Caching
55 |
56 | Turborepo can use a technique known as [Remote Caching](https://turborepo.org/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
57 |
58 | By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup), then enter the following commands:
59 |
60 | ```
61 | cd my-turborepo
62 | npx turbo login
63 | ```
64 |
65 | This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
66 |
67 | Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your turborepo:
68 |
69 | ```
70 | npx turbo link
71 | ```
72 |
73 | ## Useful Links
74 |
75 | Learn more about the power of Turborepo:
76 |
77 | - [Pipelines](https://turborepo.org/docs/core-concepts/pipelines)
78 | - [Caching](https://turborepo.org/docs/core-concepts/caching)
79 | - [Remote Caching](https://turborepo.org/docs/core-concepts/remote-caching)
80 | - [Scoped Tasks](https://turborepo.org/docs/core-concepts/scopes)
81 | - [Configuration Options](https://turborepo.org/docs/reference/configuration)
82 | - [CLI Usage](https://turborepo.org/docs/reference/command-line-reference)
83 |
--------------------------------------------------------------------------------
/apps/docs/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ["custom"],
4 | };
5 |
--------------------------------------------------------------------------------
/apps/docs/README.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | First, run the development server:
4 |
5 | ```bash
6 | yarn dev
7 | ```
8 |
9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
10 |
11 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
12 |
13 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
14 |
15 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
16 |
17 | ## Learn More
18 |
19 | To learn more about Next.js, take a look at the following resources:
20 |
21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
23 |
24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
25 |
26 | ## Deploy on Vercel
27 |
28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js.
29 |
30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
31 |
--------------------------------------------------------------------------------
/apps/docs/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/docs/next.config.js:
--------------------------------------------------------------------------------
1 | const withTM = require('next-transpile-modules')(['api', 'ui-web']);
2 |
3 | module.exports = withTM({
4 | reactStrictMode: true,
5 | });
6 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --port 3001",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "next": "12.0.8",
13 | "react": "17.0.2",
14 | "react-dom": "17.0.2",
15 | "ui-web": "*",
16 | "api": "*"
17 | },
18 | "devDependencies": {
19 | "eslint": "7.32.0",
20 | "eslint-config-custom": "*",
21 | "next-transpile-modules": "9.0.0",
22 | "tsconfig": "*",
23 | "@types/node": "^17.0.12",
24 | "@types/react": "17.0.37",
25 | "typescript": "^4.5.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/apps/docs/pages/api/example.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import { prisma } from 'api';
3 |
4 | const examples = async (req: NextApiRequest, res: NextApiResponse) => {
5 | const examples = await prisma.example.findMany();
6 | res.status(200).json(examples);
7 | };
8 |
9 | export default examples;
10 |
--------------------------------------------------------------------------------
/apps/docs/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from 'ui-web';
2 |
3 | export default function Docs() {
4 | return (
5 |
6 |
Docs
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/nextjs.json",
3 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
4 | "exclude": ["node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/apps/mobile/.env.example:
--------------------------------------------------------------------------------
1 | # NEXT AUTH
2 | NEXT_API_URL=http://0.0.0.0:3000
3 |
4 | ## Github
5 | GITHUB_ID=
6 | GITHUB_SECRET=
7 |
8 | ## Google
9 | GOOGLE_CLIENT_ID=
10 | GOOGLE_CLIENT_SECRET=
11 |
12 | # EXPO
13 | SECURE_STORE_JWT_KEY=
--------------------------------------------------------------------------------
/apps/mobile/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": ["custom"],
4 | "rules": {
5 | "jsx-a11y/alt-text": [0]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/mobile/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/apps/mobile/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | .env
--------------------------------------------------------------------------------
/apps/mobile/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "mobile",
4 | "scheme": "mobile.monolith",
5 | "slug": "mobile",
6 | "version": "1.0.0",
7 | "orientation": "portrait",
8 | "icon": "./assets/icon.png",
9 | "userInterfaceStyle": "light",
10 | "splash": {
11 | "image": "./assets/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "updates": {
16 | "fallbackToCacheTimeout": 0
17 | },
18 | "assetBundlePatterns": ["**/*"],
19 | "ios": {
20 | "supportsTablet": true
21 | },
22 | "android": {
23 | "adaptiveIcon": {
24 | "foregroundImage": "./assets/adaptive-icon.png",
25 | "backgroundColor": "#FFFFFF"
26 | }
27 | },
28 | "web": {
29 | "favicon": "./assets/favicon.png"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/apps/mobile/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/mobile/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/apps/mobile/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/mobile/assets/favicon.png
--------------------------------------------------------------------------------
/apps/mobile/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/mobile/assets/icon.png
--------------------------------------------------------------------------------
/apps/mobile/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/mobile/assets/splash.png
--------------------------------------------------------------------------------
/apps/mobile/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: [
6 | [
7 | 'inline-dotenv',
8 | {
9 | path: './.env',
10 | },
11 | ],
12 | ],
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/apps/mobile/index.js:
--------------------------------------------------------------------------------
1 | import { registerRootComponent } from 'expo';
2 |
3 | import App from './src/App';
4 |
5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
6 | // It also ensures that whether you load the app in Expo Go or in a native build,
7 | // the environment is set up appropriately
8 | registerRootComponent(App);
9 |
--------------------------------------------------------------------------------
/apps/mobile/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require('expo/metro-config');
2 | const path = require('path');
3 |
4 | // Find the workspace root, this can be replaced with `find-yarn-workspace-root`
5 | const workspaceRoot = path.resolve(__dirname, '../..');
6 | const projectRoot = __dirname;
7 |
8 | const config = getDefaultConfig(projectRoot);
9 |
10 | // 1. Watch all files within the monorepo
11 | config.watchFolders = [workspaceRoot];
12 | // 2. Let Metro know where to resolve packages, and in what order
13 | config.resolver.nodeModulesPaths = [
14 | path.resolve(projectRoot, 'node_modules'),
15 | path.resolve(workspaceRoot, 'node_modules'),
16 | ];
17 |
18 | config.resolver.sourceExts = ['jsx', 'js', 'ts', 'tsx', 'cjs'];
19 |
20 | module.exports = config;
21 |
--------------------------------------------------------------------------------
/apps/mobile/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mobile",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "dev": "expo start --ios",
7 | "dev:android": "expo start --android",
8 | "start:clean": "expo start -c",
9 | "eject": "expo eject"
10 | },
11 | "dependencies": {
12 | "@expo-google-fonts/poppins": "^0.2.2",
13 | "@react-native-community/netinfo": "8.2.0",
14 | "@trpc/client": "^9.26.0",
15 | "@trpc/react": "^9.26.0",
16 | "@trpc/server": "^9.26.0",
17 | "api": "*",
18 | "babel-plugin-inline-dotenv": "^1.7.0",
19 | "expo": "~45.0.0",
20 | "expo-apple-authentication": "^4.2.1",
21 | "expo-application": "^4.1.0",
22 | "expo-auth-session": "^3.6.1",
23 | "expo-font": "^10.1.0",
24 | "expo-haptics": "^11.2.0",
25 | "expo-network": "^4.2.0",
26 | "expo-random": "^12.2.0",
27 | "expo-secure-store": "^11.2.0",
28 | "expo-splash-screen": "^0.15.1",
29 | "expo-status-bar": "~1.3.0",
30 | "expo-web-browser": "^10.2.1",
31 | "react": "17.0.2",
32 | "react-dom": "17.0.2",
33 | "react-native": "0.68.2",
34 | "react-native-safe-area-context": "4.2.4",
35 | "react-query": "^3.39.1",
36 | "zustand": "^4.0.0-rc.3"
37 | },
38 | "devDependencies": {
39 | "@babel/core": "^7.12.9",
40 | "@types/react": "~17.0.21",
41 | "@types/react-native": "~0.67.6"
42 | },
43 | "private": true
44 | }
45 |
--------------------------------------------------------------------------------
/apps/mobile/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { Provider } from './components';
3 | import { useAppReady } from './hooks';
4 | import { LoginScreen } from './screens';
5 | import { useStore } from './store';
6 | import './utils/ignore-warnings';
7 |
8 | const App: FC = () => {
9 | const { isAppReady } = useStore();
10 | useAppReady();
11 |
12 | if (!isAppReady) {
13 | return null;
14 | }
15 | return (
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/LoginOptions.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dimensions,
3 | Text,
4 | TouchableOpacity,
5 | View,
6 | ViewStyle,
7 | Platform,
8 | Alert,
9 | } from 'react-native';
10 | import { useAuth } from '../hooks';
11 | import MaterialIcon from '@expo/vector-icons/MaterialCommunityIcons';
12 | import { FC } from 'react';
13 | import * as Haptics from 'expo-haptics';
14 | import * as AppleAuthentication from 'expo-apple-authentication';
15 |
16 | export const LoginOptions = () => {
17 | const { googleSignIn, githubSignIn } = useAuth();
18 |
19 | return (
20 | <>
21 |
26 |
27 |
32 |
33 | {Platform.OS === 'ios' && }
34 | >
35 | );
36 | };
37 |
38 | const BUTTON_ROOT: ViewStyle = {
39 | borderWidth: 1,
40 | borderColor: '#00000050',
41 | borderRadius: 5,
42 | };
43 |
44 | const INNER_BOUTTON: ViewStyle = {
45 | height: 50,
46 | width: Dimensions.get('screen').width - 24,
47 | maxWidth: 350,
48 | justifyContent: 'center',
49 | alignItems: 'center',
50 | flexDirection: 'row',
51 | };
52 |
53 | interface OAuthLoginButtonProps {
54 | onPress(): void;
55 | disabled: boolean;
56 | provider: 'google' | 'github' | 'apple';
57 | }
58 |
59 | const OAuthLoginButton: FC = ({
60 | onPress,
61 | disabled,
62 | provider,
63 | }) => {
64 | function handleOnPress() {
65 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
66 | onPress();
67 | }
68 | return (
69 |
70 |
75 |
76 |
77 |
78 | Continue with{' '}
79 | {provider}
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | const AppleLoginButton = () => {
87 | // const { appleSignIn } = useAuth();
88 |
89 | function signIn() {
90 | Alert.alert('NOT IMPLEMENTED');
91 | return;
92 | }
93 | return (
94 |
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/Provider.tsx:
--------------------------------------------------------------------------------
1 | import { StatusBar } from 'expo-status-bar';
2 | import { FC, useState } from 'react';
3 | import {
4 | initialWindowMetrics,
5 | SafeAreaProvider,
6 | } from 'react-native-safe-area-context';
7 | import { QueryClient, QueryClientProvider } from 'react-query';
8 | import { useStore } from '../store';
9 | import { getJWT } from '../utils/secure-store';
10 | import { transformer, trpc } from '../utils/trpc';
11 |
12 | export const Provider: FC = ({ children }) => {
13 | const { token, setToken } = useStore();
14 | const [queryClient] = useState(() => new QueryClient());
15 | const [trpcClient] = useState(() =>
16 | trpc.createClient({
17 | url: `${process.env.NEXT_API_URL}/api/trpc`,
18 | async headers() {
19 | if (token) {
20 | return {
21 | Authorization: token,
22 | };
23 | }
24 | try {
25 | const localToken = await getJWT();
26 | if (localToken) {
27 | setToken(localToken);
28 | return {
29 | Authorization: localToken,
30 | };
31 | }
32 |
33 | return {
34 | Authorization: '',
35 | };
36 | } catch (err) {
37 | console.log({ CREATE_TRPC_CLIENT_HEADER: err });
38 | return {
39 | Authorization: '',
40 | };
41 | }
42 | },
43 | transformer,
44 | })
45 | );
46 |
47 | return (
48 |
49 |
50 |
51 |
52 | {children}
53 |
54 |
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Provider';
2 | export * from './LoginOptions';
3 |
--------------------------------------------------------------------------------
/apps/mobile/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useAuth';
2 | export * from './useAppReady';
3 |
--------------------------------------------------------------------------------
/apps/mobile/src/hooks/useAppReady.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Poppins_400Regular,
3 | Poppins_500Medium,
4 | Poppins_600SemiBold,
5 | Poppins_700Bold,
6 | Poppins_800ExtraBold,
7 | useFonts,
8 | } from '@expo-google-fonts/poppins';
9 | import * as SplashScreen from 'expo-splash-screen';
10 | import { useEffect } from 'react';
11 | import { useStore } from '../store';
12 |
13 | SplashScreen.preventAutoHideAsync();
14 |
15 | export const useAppReady = () => {
16 | const { setIsAppReady, isAppReady } = useStore();
17 | const customFonts = useFonts({
18 | poppins: Poppins_400Regular,
19 | poppins500: Poppins_500Medium,
20 | poppins600: Poppins_600SemiBold,
21 | poppins700: Poppins_700Bold,
22 | poppins800: Poppins_800ExtraBold,
23 | });
24 |
25 | useEffect(() => {
26 | if (customFonts[1]) {
27 | console.error({ fontsLoadingError: customFonts[1] });
28 | }
29 | if (customFonts[0] && !isAppReady) {
30 | setIsAppReady(true);
31 | SplashScreen.hideAsync();
32 | }
33 | }, [customFonts]);
34 | };
35 |
--------------------------------------------------------------------------------
/apps/mobile/src/hooks/useAuth.tsx:
--------------------------------------------------------------------------------
1 | import * as AppleAuthentication from 'expo-apple-authentication';
2 | import {
3 | AuthSessionResult,
4 | makeRedirectUri,
5 | useAuthRequest,
6 | } from 'expo-auth-session';
7 | import * as Google from 'expo-auth-session/providers/google';
8 | import { useEffect } from 'react';
9 | import { Alert } from 'react-native';
10 | import { useStore } from '../store';
11 | import { clearToken, saveJWT } from '../utils/secure-store';
12 | import { inferMutationInput, trpc } from '../utils/trpc';
13 |
14 | const redirectToExpoAppUri = makeRedirectUri({
15 | useProxy: true,
16 | });
17 |
18 | type SignInResponseInput = inferMutationInput<'expo-auth.signIn'>['response'];
19 | type SignInProvider = inferMutationInput<'expo-auth.signIn'>['provider'];
20 |
21 | export const useAuth = () => {
22 | const { setSession, setToken, setLoadingSession } = useStore();
23 |
24 | const googleSignIn = useGoogleAuth();
25 | const githubSignIn = useGithubAuth();
26 | const appleSignIn = useAppleAuth();
27 |
28 | const utils = trpc.useContext();
29 | trpc.useQuery(['expo-auth.getSession'], {
30 | onSuccess(data) {
31 | setSession(data);
32 | },
33 | onError(err) {
34 | if (err.message === 'Token expired') {
35 | console.log(err);
36 | signOut();
37 | }
38 | },
39 | });
40 |
41 | async function signOut() {
42 | setLoadingSession(true);
43 | try {
44 | await clearToken();
45 | utils.queryClient.resetQueries(); // or utils.invalidateQueries()
46 | setSession(null);
47 | setToken(null);
48 | } catch (err) {
49 | console.log({ SIGN_OUT_ERROR: err });
50 | } finally {
51 | setLoadingSession(false);
52 | }
53 | }
54 |
55 | return {
56 | googleSignIn,
57 | githubSignIn,
58 | appleSignIn,
59 | signOut,
60 | };
61 | };
62 |
63 | const useGoogleAuth = () => {
64 | const { signIn } = useSignIn();
65 | const [request, response, promptAsync] = Google.useAuthRequest({
66 | expoClientId: process.env.GOOGLE_CLIENT_ID,
67 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
68 | scopes: ['openid', 'profile', 'email'],
69 | redirectUri: redirectToExpoAppUri,
70 | });
71 |
72 | useEffect(() => {
73 | if (response?.type === 'cancel') {
74 | console.log('useGoogleAuth', 'CANCEL');
75 | }
76 | if (response?.type === 'error') {
77 | console.log('useGoogleAuth', 'ERROR');
78 | }
79 | if (response?.type === 'locked') {
80 | console.log('useGoogleAuth', 'LOCKED');
81 | }
82 |
83 | if (response?.type === 'success') {
84 | signIn(response, 'google');
85 | }
86 | }, [response]);
87 |
88 | return { isDisabled: !request, promptAsync: () => promptAsync() };
89 | };
90 |
91 | const GITHUB_DISCOVERY = {
92 | authorizationEndpoint: 'https://github.com/login/oauth/authorize',
93 | tokenEndpoint: 'https://github.com/login/oauth/access_token',
94 | revocationEndpoint: `https://github.com/settings/connections/applications/${process.env.GITHUB_ID}`,
95 | };
96 |
97 | const useGithubAuth = () => {
98 | const { signIn } = useSignIn();
99 | const [request, response, promptAsync] = useAuthRequest(
100 | {
101 | clientId: process.env.GITHUB_ID,
102 | scopes: ['read:user', 'user:email'],
103 | redirectUri: redirectToExpoAppUri,
104 | },
105 | GITHUB_DISCOVERY
106 | );
107 |
108 | useEffect(() => {
109 | if (response?.type === 'success') {
110 | signIn(response, 'github');
111 | }
112 | }, [response]);
113 |
114 | return {
115 | isDisabled: !request,
116 | promptAsync: () => promptAsync({ useProxy: true }),
117 | };
118 | };
119 |
120 | const useAppleAuth = () => {
121 | const { signIn } = useSignIn();
122 |
123 | async function promptAsync() {
124 | try {
125 | // type AppleAuthenticationCredential
126 | const credential: AppleAuthenticationCredential =
127 | await AppleAuthentication.signInAsync({
128 | requestedScopes: [
129 | AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
130 | AppleAuthentication.AppleAuthenticationScope.EMAIL,
131 | ],
132 | });
133 |
134 | signIn(
135 | {
136 | type: 'success',
137 | authentication: { accessToken: credential.authorizationCode },
138 | params: credential,
139 | },
140 | 'apple'
141 | );
142 | } catch (err) {
143 | console.log('useAppleAuth', err);
144 | }
145 | }
146 |
147 | return {
148 | isDisabled: false,
149 | promptAsync,
150 | };
151 | };
152 |
153 | type AppleAuthenticationCredential = {
154 | user: string;
155 | state: string | null;
156 | fullName: AppleAuthentication.AppleAuthenticationFullName | null;
157 | email: string | null;
158 | realUserStatus: AppleAuthentication.AppleAuthenticationUserDetectionStatus;
159 | identityToken: string | null;
160 | authorizationCode: string | null;
161 | };
162 |
163 | interface AppleAuthSessionResult {
164 | type: 'success' | 'error';
165 | authentication: { accessToken: string | null };
166 | params: AppleAuthentication.AppleAuthenticationCredential;
167 | }
168 |
169 | const useSignIn = () => {
170 | const { setLoadingSession, setSession, setToken } = useStore();
171 |
172 | const signInMutation = trpc.useMutation(['expo-auth.signIn'], {
173 | onError(error, variables, context) {
174 | if (error.message === 'Failed to authenticate') {
175 | console.log({
176 | HANDLE_THIS_AUTH_ERROR: error,
177 | variables,
178 | context,
179 | });
180 | } else {
181 | console.log({
182 | HANDLE_ALL_OTHER_AUTH_ERRORS: error,
183 | variables,
184 | context,
185 | });
186 | }
187 | },
188 | });
189 |
190 | async function signIn(
191 | response: AuthSessionResult | AppleAuthSessionResult,
192 | provider: SignInProvider
193 | ) {
194 | setLoadingSession(true);
195 | try {
196 | const result = await signInMutation.mutateAsync({
197 | response: response as SignInResponseInput,
198 | provider,
199 | });
200 | if (!result?.jwt) {
201 | Alert.alert('ERROR', 'Unable to login at this time.');
202 | return;
203 | }
204 | setSession(result?.currentUser);
205 | setToken(result?.jwt);
206 | saveJWT(result?.jwt);
207 | } catch (err) {
208 | console.error('Error: useSignIn', err);
209 | } finally {
210 | setLoadingSession(false);
211 | }
212 | }
213 |
214 | return { signIn };
215 | };
216 |
--------------------------------------------------------------------------------
/apps/mobile/src/screens/LoginScreen.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect } from 'react';
2 | import { Button, Image, Text, View, ViewStyle } from 'react-native';
3 | import { LoginOptions } from '../components';
4 | import { useAuth } from '../hooks';
5 | import { useStore } from '../store';
6 | import { trpc } from '../utils/trpc';
7 | import NetInfo from '@react-native-community/netinfo';
8 | import { MaterialCommunityIcons } from '@expo/vector-icons';
9 | import { useSafeAreaInsets } from 'react-native-safe-area-context';
10 |
11 | const CENTER_CENTER: ViewStyle = {
12 | alignItems: 'center',
13 | justifyContent: 'center',
14 | };
15 |
16 | const ROOT: ViewStyle = {
17 | flex: 1,
18 | backgroundColor: '#fff',
19 | ...CENTER_CENTER,
20 | };
21 |
22 | export const LoginScreen: FC = () => {
23 | const { bottom } = useSafeAreaInsets();
24 | const {
25 | session,
26 | loadingSession,
27 | hasInternetConnection,
28 | setHasInternetConnection,
29 | } = useStore();
30 | const hello = trpc.useQuery(['example.hello', { text: 'from tRPC' }]);
31 | const { signOut } = useAuth();
32 |
33 | useEffect(() => {
34 | const unsubscribe = NetInfo.addEventListener((state) => {
35 | setHasInternetConnection(Boolean(state.isConnected));
36 | });
37 |
38 | return () => {
39 | unsubscribe();
40 | };
41 | }, []);
42 |
43 | if (loadingSession)
44 | return (
45 |
46 | Loading session...
47 |
48 | );
49 |
50 | return (
51 |
52 |
53 |
54 |
59 |
65 | Turbo-t3-expo
66 |
67 |
68 | {!hello.data ? 'Loading tRPC query...' : hello.data.greeting}
69 |
70 |
71 |
72 | {!session ? (
73 |
74 | ) : (
75 |
76 | Successfully authenticated!
77 | Name: {session.name}
78 | Email: {session.email}
79 | ID: {session.id}
80 |
81 |
82 |
87 |
88 |
93 |
94 |
95 | )}
96 |
97 |
104 | {hasInternetConnection
105 | ? 'You are connected to the internet'
106 | : 'You are not connected to the internet'}
107 |
108 |
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/apps/mobile/src/screens/index.ts:
--------------------------------------------------------------------------------
1 | export * from './LoginScreen';
2 |
--------------------------------------------------------------------------------
/apps/mobile/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import create from 'zustand';
2 |
3 | interface IStore {
4 | isAppReady: boolean;
5 | setIsAppReady(isAppReady: boolean): void;
6 | hasInternetConnection: boolean;
7 | setHasInternetConnection(hasInternetConnection: boolean): void;
8 | token: string | null;
9 | setToken(token: string | null): void;
10 | session: any;
11 | setSession(session: any): void;
12 | loadingSession: boolean;
13 | setLoadingSession(loadingSession: boolean): void;
14 | }
15 |
16 | export const useStore = create((set) => ({
17 | // App
18 | isAppReady: false,
19 | setIsAppReady: (isAppReady) => {
20 | set({ isAppReady });
21 | },
22 | // Device
23 | hasInternetConnection: false,
24 | setHasInternetConnection: (hasInternetConnection) => {
25 | set({ hasInternetConnection });
26 | },
27 | // Token
28 | token: null,
29 | setToken: (token) => {
30 | set({ token });
31 | },
32 | // Session
33 | session: null,
34 | setSession: (session) => {
35 | set({ session });
36 | },
37 | loadingSession: false,
38 | setLoadingSession: (loadingSession) => {
39 | set({ loadingSession });
40 | },
41 | }));
42 |
--------------------------------------------------------------------------------
/apps/mobile/src/types/global.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace NodeJS {
3 | interface ProcessEnv {
4 | GITHUB_ID: string;
5 | GITHUB_SECRET: string;
6 | GOOGLE_CLIENT_ID: string;
7 | GOOGLE_CLIENT_SECRET: string;
8 | SECURE_STORE_JWT_KEY: string;
9 | NEXT_API_URL: string;
10 | }
11 | }
12 | }
13 |
14 | export default {};
15 |
--------------------------------------------------------------------------------
/apps/mobile/src/utils/ignore-warnings.ts:
--------------------------------------------------------------------------------
1 | import { LogBox } from 'react-native';
2 |
3 | LogBox.ignoreLogs(['Missing token']);
4 |
--------------------------------------------------------------------------------
/apps/mobile/src/utils/secure-store.ts:
--------------------------------------------------------------------------------
1 | import * as SecureStore from 'expo-secure-store';
2 | import { Alert } from 'react-native';
3 |
4 | export async function saveJWT(jwt: string) {
5 | try {
6 | SecureStore.setItemAsync(process.env.SECURE_STORE_JWT_KEY, jwt);
7 | } catch (err) {
8 | Alert.alert('ERROR', 'Unable to sign in.');
9 | }
10 | }
11 |
12 | export async function getJWT() {
13 | try {
14 | return await SecureStore.getItemAsync(process.env.SECURE_STORE_JWT_KEY);
15 | } catch (err) {
16 | Alert.alert('UH OH', 'Something unexpected happened.');
17 | return null;
18 | }
19 | }
20 |
21 | export async function clearToken() {
22 | try {
23 | SecureStore.setItemAsync(process.env.SECURE_STORE_JWT_KEY, '');
24 | } catch (err) {
25 | Alert.alert('ERROR', 'Cannot log out at this time.');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/apps/mobile/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | export * from 'hooks/trpc';
2 |
--------------------------------------------------------------------------------
/apps/mobile/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "jsx": "preserve",
5 | "lib": ["dom", "esnext"],
6 | "moduleResolution": "node",
7 | "noEmit": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true,
10 | "strict": true,
11 | "target": "esnext",
12 | "allowJs": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "isolatedModules": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"],
20 | "extends": "expo/tsconfig.base"
21 | }
22 |
--------------------------------------------------------------------------------
/apps/web/.env-example:
--------------------------------------------------------------------------------
1 | # Note that not all variables here might be in use for your selected configuration
2 |
3 | # Prisma
4 |
5 | DATABASE_URL=
6 |
7 | # Next Auth
8 |
9 | NEXTAUTH_SECRET=
10 | NEXTAUTH_URL=
11 |
12 | # Next Auth Github Provider
13 |
14 | GITHUB_ID=
15 | GITHUB_SECRET=
16 |
17 | ## Email
18 |
19 | EMAIL_SERVER_USER=
20 | EMAIL_SERVER_REFRESH_TOKEN=
21 |
--------------------------------------------------------------------------------
/apps/web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": ["custom"]
4 | }
5 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | # Create T3 App
2 |
3 | This is an app bootstrapped according to the [init.tips](https://init.tips) stack, also known as the T3-Stack.
4 |
--------------------------------------------------------------------------------
/apps/web/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | const withTM = require('next-transpile-modules')(['api', 'hooks', 'ui-web']);
2 |
3 | /** @type {import('next').NextConfig} */
4 | const nextConfig = {
5 | reactStrictMode: true,
6 | };
7 |
8 | module.exports = withTM(nextConfig);
9 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@next-auth/prisma-adapter": "^1.0.3",
13 | "@prisma/client": "^4.0.0",
14 | "@trpc/client": "^9.25.3",
15 | "@trpc/next": "^9.25.3",
16 | "@trpc/react": "^9.25.3",
17 | "@trpc/server": "^9.25.3",
18 | "api": "*",
19 | "hooks": "*",
20 | "next": "12.2.0",
21 | "next-auth": "^4.10.0",
22 | "react": "17.0.2",
23 | "react-dom": "17.0.2",
24 | "react-query": "^3.39.1",
25 | "superjson": "^1.9.1",
26 | "ui-web": "*",
27 | "zod": "^3.17.3"
28 | },
29 | "devDependencies": {
30 | "@types/node": "18.0.0",
31 | "@types/react": "~17.0.21",
32 | "@types/react-dom": "17.0.17",
33 | "autoprefixer": "^10.4.7",
34 | "eslint": "8.18.0",
35 | "eslint-config-custom": "*",
36 | "eslint-config-next": "12.1.6",
37 | "postcss": "^8.4.14",
38 | "prisma": "^4.0.0",
39 | "tailwindcss": "^3.1.6",
40 | "tsconfig": "*",
41 | "typescript": "4.7.4"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrzachnugent/turbo-t3-expo/6682d89641456200724d3946c4c3fe738b27587c/apps/web/public/favicon.ico
--------------------------------------------------------------------------------
/apps/web/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { withTRPC } from '@trpc/next';
2 | import type { AppType } from 'next/dist/shared/lib/utils';
3 | import { transformer, AppRouter } from '../utils/trpc';
4 | import { SessionProvider } from 'next-auth/react';
5 | import '../styles/globals.css';
6 |
7 | const MyApp: AppType = ({
8 | Component,
9 | pageProps: { session, ...pageProps },
10 | }) => {
11 | return (
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | const getBaseUrl = () => {
19 | if (typeof window !== 'undefined') {
20 | return '';
21 | }
22 | if (process.browser) return ''; // Browser should use current path
23 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
24 |
25 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
26 | };
27 |
28 | export default withTRPC({
29 | config({ ctx }) {
30 | /**
31 | * If you want to use SSR, you need to use the server's full URL
32 | * @link https://trpc.io/docs/ssr
33 | */
34 | const url = `${getBaseUrl()}/api/trpc`;
35 |
36 | return {
37 | url,
38 | transformer,
39 | /**
40 | * @link https://react-query.tanstack.com/reference/QueryClient
41 | */
42 | // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
43 | };
44 | },
45 | /**
46 | * @link https://trpc.io/docs/ssr
47 | */
48 | ssr: false,
49 | })(MyApp);
50 |
--------------------------------------------------------------------------------
/apps/web/src/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from 'api';
2 | import NextAuth from 'next-auth';
3 |
4 | export default NextAuth(authOptions);
5 |
--------------------------------------------------------------------------------
/apps/web/src/pages/api/examples.ts:
--------------------------------------------------------------------------------
1 | // src/pages/api/examples.ts
2 | import type { NextApiRequest, NextApiResponse } from 'next';
3 | import { prisma } from 'api';
4 |
5 | const examples = async (req: NextApiRequest, res: NextApiResponse) => {
6 | const examples = await prisma.example.findMany();
7 | res.status(200).json(examples);
8 | };
9 |
10 | export default examples;
11 |
--------------------------------------------------------------------------------
/apps/web/src/pages/api/jwt.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { getToken } from 'next-auth/jwt';
3 |
4 | const secret = process.env.NEXTAUTH_SECRET;
5 | export const handleJWT = async (req: NextApiRequest, res: NextApiResponse) => {
6 | const token = await getToken({ req, secret, raw: true });
7 | res.send({ token });
8 | };
9 |
10 | export default handleJWT;
11 |
--------------------------------------------------------------------------------
/apps/web/src/pages/api/restricted.ts:
--------------------------------------------------------------------------------
1 | // Example of a restricted endpoint that only authenticated users can access from https://next-auth.js.org/getting-started/example
2 |
3 | import { NextApiRequest, NextApiResponse } from 'next';
4 | import { unstable_getServerSession as getServerSession } from 'next-auth';
5 | import { authOptions as nextAuthOptions } from 'api';
6 |
7 | const restricted = async (req: NextApiRequest, res: NextApiResponse) => {
8 | const session = await getServerSession(req, res, nextAuthOptions);
9 |
10 | if (session) {
11 | res.status(200).json({
12 | content:
13 | 'This is protected content. You can access this content because you are signed in.',
14 | });
15 | } else {
16 | res.status(400).json({
17 | error: 'You must be sign in to view the protected content on this page.',
18 | });
19 | }
20 | };
21 |
22 | export default restricted;
23 |
--------------------------------------------------------------------------------
/apps/web/src/pages/api/trpc/[trpc].ts:
--------------------------------------------------------------------------------
1 | // src/pages/api/trpc/[trpc].ts
2 | import { createNextApiHandler } from '@trpc/server/adapters/next';
3 | import { appRouter, createContext } from 'api';
4 |
5 | // export API handler
6 | export default createNextApiHandler({
7 | router: appRouter,
8 | createContext: createContext,
9 | });
10 |
--------------------------------------------------------------------------------
/apps/web/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from 'next';
2 | import Head from 'next/head';
3 | import { trpc } from '../utils/trpc';
4 | import { Button } from 'ui-web';
5 | import { signIn, signOut, useSession } from 'next-auth/react';
6 |
7 | const Home: NextPage = () => {
8 | const hello = trpc.useQuery(['example.hello', { text: 'from tRPC' }]);
9 | const session = useSession();
10 |
11 | return (
12 | <>
13 |
14 | Create T3 App
15 |
16 |
17 |
18 |
19 |
20 |
21 | Create T3 App
22 |
23 | {session.data ? (
24 |
27 | ) : (
28 |
29 | )}
30 |
This stack uses
31 |
32 |
46 |
47 |
TypeScript
48 |
49 | Strongly typed programming language that builds on JavaScript,
50 | giving you better tooling at any scale
51 |
52 |
58 | Documentation
59 |
60 |
61 |
62 |
TailwindCSS
63 |
64 | Rapidly build modern websites without ever leaving your HTML
65 |
66 |
72 | Documentation
73 |
74 |
75 |
89 |
90 |
91 | {hello.data ?
{hello.data.greeting}
:
Loading..
}
92 |
93 |
94 |
95 | >
96 | );
97 | };
98 |
99 | export default Home;
100 |
--------------------------------------------------------------------------------
/apps/web/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/apps/web/src/types/next-auth.ts:
--------------------------------------------------------------------------------
1 | import { DefaultUser } from 'next-auth';
2 |
3 | declare module 'next-auth' {
4 | /**
5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
6 | */
7 | interface Session {
8 | user: DefaultUser & {
9 | id: unknown;
10 | };
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | export * from 'hooks/trpc';
2 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | module.exports = {
4 | content: ["./src/**/*.{js,ts,jsx,tsx}"],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | };
10 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/nextjs.json",
3 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
4 | "exclude": ["node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monolith",
3 | "version": "0.0.0",
4 | "private": true,
5 | "workspaces": {
6 | "packages": [
7 | "apps/*",
8 | "packages/*"
9 | ]
10 | },
11 | "scripts": {
12 | "build": "turbo run build",
13 | "dev": "turbo run dev --parallel",
14 | "dev:web": "turbo run dev --filter=web",
15 | "dev:mobile": "turbo run dev --filter=mobile",
16 | "lint": "turbo run lint",
17 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
18 | "clean": "scripts/clean.sh",
19 | "postinstall": "prisma generate"
20 | },
21 | "devDependencies": {
22 | "eslint-config-custom": "*",
23 | "prettier": "latest",
24 | "turbo": "latest"
25 | },
26 | "engines": {
27 | "npm": ">=7.0.0",
28 | "node": ">=14.0.0"
29 | },
30 | "dependencies": {
31 | "@prisma/client": "^4.0.0",
32 | "prisma": "^4.0.0"
33 | },
34 | "packageManager": "yarn@1.22.18"
35 | }
36 |
--------------------------------------------------------------------------------
/packages/api/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | # Keep environment variables out of version control
3 | .env
4 |
--------------------------------------------------------------------------------
/packages/api/db/index.ts:
--------------------------------------------------------------------------------
1 | export * from '@prisma/client';
2 |
3 | import { PrismaClient } from '@prisma/client';
4 |
5 | let prisma: PrismaClient;
6 |
7 | declare global {
8 | var __db: PrismaClient | undefined;
9 | }
10 |
11 | // this is needed because in development we don't want to restart
12 | // the server with every change, but we want to make sure we don't
13 | // create a new connection to the DB with every change either.
14 | if (process.env.NODE_ENV === 'production') {
15 | prisma = new PrismaClient({ log: ['error'] });
16 | // prisma.$connect();
17 | } else {
18 | if (!global.__db) {
19 | global.__db = new PrismaClient({ log: ['query', 'error', 'warn'] });
20 | // global.__db.$connect();
21 | }
22 | prisma = global.__db;
23 | }
24 |
25 | export { prisma };
26 |
--------------------------------------------------------------------------------
/packages/api/expo-auth/apple-auth.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AppleAuthenticationFullName,
3 | AppleAuthenticationUserDetectionStatus,
4 | } from 'expo-apple-authentication';
5 | import * as jwt from 'jsonwebtoken';
6 | import { AuthResponse } from './zod';
7 | import { TRPCError } from '@trpc/server';
8 | import { PrismaAuth } from './prisma-auth';
9 | import jwtDecode, { JwtPayload } from 'jwt-decode';
10 | import { encode, JWT } from 'next-auth/jwt';
11 | import { SECRET } from './constants';
12 | import { AdapterUser } from 'next-auth/adapters';
13 | import { Account } from 'next-auth/core/types';
14 |
15 | const IS_TESTING = true;
16 |
17 | const privateKey = process.env.PRIVATE_KEY as jwt.Secret;
18 |
19 | type AppleAuthenticationCredential = {
20 | user: string;
21 | state: string | null;
22 | fullName: AppleAuthenticationFullName | null;
23 | email: string | null;
24 | realUserStatus: AppleAuthenticationUserDetectionStatus;
25 | identityToken: string | null;
26 | authorizationCode: string | null;
27 | };
28 |
29 | interface DecodedJwtPayload extends JwtPayload {
30 | name: string;
31 | email: string;
32 | picture: string;
33 | }
34 |
35 | export async function signInWithApple(response: AuthResponse) {
36 | const params = response?.params as AppleAuthenticationCredential;
37 |
38 | if (!params.authorizationCode || !params.identityToken) {
39 | throw new TRPCError({
40 | code: 'FORBIDDEN',
41 | message: 'Failed to authenticate',
42 | cause: 'Missing oAuth Information',
43 | });
44 | }
45 |
46 | // To generate a signed JWT:
47 | // Create the JWT header.
48 | const header = {
49 | alg: 'ES256',
50 | kid: process.env.KEY_ID,
51 | };
52 | // Create the JWT payload.
53 | const payload = {
54 | iss: process.env.TEAM_ID,
55 | iat: Math.floor(Date.now() / 1000),
56 | exp: 30 * 24 * 60 * 60, // 30 days
57 | aud: 'https://appleid.apple.com',
58 | sub: process.env.APPLE_CLIENT_ID,
59 | };
60 |
61 | // Sign the JWT.
62 | const getClientSecret = () =>
63 | jwt.sign(payload, privateKey, {
64 | algorithm: 'ES256',
65 | header,
66 | expiresIn: '30d',
67 | });
68 |
69 | // Verify authorizationCode
70 | const verifyAuthCode = await fetch('https://appleid.apple.com/auth/token', {
71 | method: 'POST',
72 | headers: {
73 | Accept: 'application/json',
74 | 'Content-Type': 'application/json',
75 | },
76 | body: JSON.stringify({
77 | client_id: process.env.APPLE_CLIENT_ID, // bundler ex: com.company.product_name
78 | client_secret: getClientSecret(), // This is a JWT token signed with the .p8 file generated while creating the Sign in with Apple key
79 | code: params.authorizationCode,
80 | grant_type: 'authorization_code',
81 | redirect_uri: '', // can be an empty string
82 | }),
83 | });
84 |
85 | const verifiedResult = await verifyAuthCode.json();
86 | console.log({ verifiedResult });
87 | // access_token - (Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access. Valid for an hour.
88 | // expires_in - The amount of time, in seconds, before the access token expires.
89 | // id_token - A JSON Web Token that contains the user’s identity information.
90 | // refresh_token - The refresh token used to regenerate new access tokens. Store this token securely on your server.
91 | // token_type - The type of access token. It will always be "bearer".
92 |
93 | // id_token from Expo App
94 | const decodedIdToken = jwtDecode(verifiedResult.id_token);
95 | console.log({ decodedIdToken });
96 | if (!decodedIdToken.sub) {
97 | throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid Response' });
98 | }
99 |
100 | if (IS_TESTING) {
101 | throw new TRPCError({
102 | code: 'CONFLICT',
103 | message: 'Testing so throw error here',
104 | });
105 | }
106 | // Check if user already has a Apple oAuth Account
107 | // If they do, create new JWT for authentication and return user
108 | const currentUserByAccount = await PrismaAuth.getUserByAccount({
109 | providerAccountId: decodedIdToken.sub,
110 | provider: 'apple',
111 | });
112 |
113 | if (currentUserByAccount) {
114 | decodedIdToken.sub = currentUserByAccount.id;
115 | const newJwt = await encode({
116 | token: decodedIdToken as JWT,
117 | secret: SECRET,
118 | });
119 | return {
120 | currentUser: currentUserByAccount,
121 | jwt: newJwt,
122 | };
123 | } else {
124 | // Check if user without Apple oAuth Account is already a user in our database
125 | // If they are, link Apple oAuth Account with existing user, create new JWT for authentication and return user
126 | const currentUserByEmail = await PrismaAuth.getUserByEmail(
127 | (decodedIdToken as DecodedJwtPayload).email
128 | );
129 | if (currentUserByEmail) {
130 | const newJwt = await encode({
131 | token: decodedIdToken as JWT,
132 | secret: SECRET,
133 | });
134 | const newAccount: Account = {
135 | provider: 'apple',
136 | providerAccountId: decodedIdToken.sub,
137 | type: 'oauth',
138 | userId: currentUserByEmail.id,
139 | access_token: newJwt,
140 | id_token: verifiedResult.id_token,
141 | token_type: 'bearer',
142 | scope: 'name,email',
143 | };
144 | await PrismaAuth.linkAccount(newAccount);
145 | return {
146 | currentUser: currentUserByEmail,
147 | jwt: newJwt,
148 | };
149 | } else {
150 | // Create new user, create new JWT for authentication and return new user
151 | const newUser: Omit = {
152 | name: (decodedIdToken as DecodedJwtPayload).name,
153 | email: (decodedIdToken as DecodedJwtPayload).email,
154 | emailVerified: new Date(),
155 | };
156 |
157 | const user = await PrismaAuth.createUser(newUser);
158 | if (user) {
159 | const newJwt = await encode({
160 | token: decodedIdToken as JWT,
161 | secret: SECRET,
162 | });
163 | const newAccount: Account = {
164 | provider: 'apple',
165 | providerAccountId: decodedIdToken.sub,
166 | type: 'oauth',
167 | userId: user.id,
168 | access_token: newJwt,
169 | id_token: verifiedResult.id_token,
170 | token_type: 'bearer',
171 | scope: 'name,email',
172 | };
173 | await PrismaAuth.linkAccount(newAccount);
174 |
175 | return {
176 | currentUser: user,
177 | jwt: newJwt,
178 | };
179 | }
180 |
181 | throw new TRPCError({
182 | code: 'BAD_REQUEST',
183 | message: 'Unable to create new user',
184 | });
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/packages/api/expo-auth/constants.ts:
--------------------------------------------------------------------------------
1 | export const MY_EXPO_URLS = ['exp://10.0.0.95:19000'];
2 | export const ISS_GOOGLE_VALUES = [
3 | 'https://accounts.google.com',
4 | 'accounts.google.com',
5 | ];
6 |
7 | export const SECRET = process.env.NEXTAUTH_SECRET;
8 |
--------------------------------------------------------------------------------
/packages/api/expo-auth/github-auth.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from '@trpc/server';
2 | import { AdapterUser } from 'next-auth/adapters';
3 | import { Account } from 'next-auth/core/types';
4 | import { encode, JWT } from 'next-auth/jwt';
5 | import { GithubEmail } from 'next-auth/providers/github';
6 | import { PrismaAuth } from '.';
7 | import { MY_EXPO_URLS, SECRET } from './constants';
8 | import { AuthResponse } from './zod';
9 |
10 | interface IGithubUser {
11 | id: number;
12 | name?: string | null;
13 | avatar_url?: string | null;
14 | email?: string | null;
15 | }
16 |
17 | interface IAccessTokenRequest {
18 | access_token: string;
19 | token_type: 'bearer';
20 | scope: 'read:user,user:email';
21 | }
22 |
23 | interface GithubUserJwt extends JWT {
24 | sub: string;
25 | }
26 |
27 | export async function signInWithGithub(response: AuthResponse) {
28 | // Check Response is from Expo and provides a github temporary code
29 | if (
30 | !response?.params?.code ||
31 | !MY_EXPO_URLS.some((url) => response?.url.startsWith(url))
32 | ) {
33 | throw new TRPCError({
34 | code: 'FORBIDDEN',
35 | message: 'Failed to authenticate',
36 | cause: 'Missing oAuth Information',
37 | });
38 | }
39 | const requestAccessToken = await fetch(
40 | `https://github.com/login/oauth/access_token?client_id=${process.env.GITHUB_ID}&client_secret=${process.env.GITHUB_SECRET}&code=${response.params.code}`,
41 | {
42 | method: 'POST',
43 | headers: {
44 | Accept: 'application/json',
45 | },
46 | }
47 | );
48 |
49 | if (!requestAccessToken.ok) {
50 | throw new TRPCError({
51 | code: 'FORBIDDEN',
52 | message: 'Failed to authenticate',
53 | cause: 'Missing oAuth Information',
54 | });
55 | }
56 |
57 | const accessTokenResponse: IAccessTokenRequest =
58 | await requestAccessToken.json();
59 |
60 | const getGithubUser = await fetch('https://api.github.com/user', {
61 | method: 'GET',
62 | headers: {
63 | Authorization: `token ${accessTokenResponse.access_token}`,
64 | },
65 | });
66 |
67 | if (!getGithubUser.ok) {
68 | throw new TRPCError({
69 | code: 'FORBIDDEN',
70 | message: 'Failed to authenticate',
71 | cause: 'Missing oAuth Information',
72 | });
73 | }
74 |
75 | const githubUser: IGithubUser = await getGithubUser.json();
76 |
77 | const formattedGithubUser: GithubUserJwt = {
78 | name: githubUser.name,
79 | picture: githubUser.avatar_url,
80 | email: githubUser.email,
81 | sub: githubUser.id.toString(),
82 | };
83 |
84 | // Check if user already has a Github oAuth Account
85 | // If they do, create new JWT for authentication and return user
86 | const currentUserByAccount = await PrismaAuth.getUserByAccount({
87 | providerAccountId: formattedGithubUser.sub,
88 | provider: 'github',
89 | });
90 |
91 | if (currentUserByAccount) {
92 | formattedGithubUser.sub = currentUserByAccount.id;
93 | const newJwt = await encode({
94 | token: formattedGithubUser as JWT,
95 | secret: SECRET,
96 | });
97 | return {
98 | currentUser: currentUserByAccount,
99 | jwt: newJwt,
100 | };
101 | } else {
102 | // Check if user without Github oAuth Account is already a user in our database
103 | // If they are, link Github oAuth Account with existing user, create new JWT for authentication and return user
104 |
105 | if (!formattedGithubUser.email) {
106 | // If the user does not have a public email, get another via the GitHub API
107 | // See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user
108 | const res = await fetch('https://api.github.com/user/emails', {
109 | headers: { Authorization: `token ${accessTokenResponse.access_token}` },
110 | });
111 |
112 | if (res.ok) {
113 | const emails: GithubEmail[] = await res.json();
114 | formattedGithubUser.email = (
115 | emails.find((e) => e.primary) ?? emails[0]
116 | ).email;
117 | } else {
118 | throw new TRPCError({
119 | code: 'BAD_REQUEST',
120 | message: 'Cannot fetch user email',
121 | });
122 | }
123 | }
124 |
125 | const currentUserByEmail = await PrismaAuth.getUserByEmail(
126 | formattedGithubUser.email
127 | );
128 | if (currentUserByEmail) {
129 | const newJwt = await encode({
130 | token: formattedGithubUser,
131 | secret: SECRET,
132 | });
133 | const newAccount: Account = {
134 | provider: 'github',
135 | providerAccountId: formattedGithubUser.sub,
136 | type: 'oauth',
137 | userId: currentUserByEmail.id,
138 | access_token: newJwt,
139 | id_token: accessTokenResponse.access_token,
140 | token_type: accessTokenResponse?.token_type,
141 | scope: accessTokenResponse?.scope,
142 | };
143 | await PrismaAuth.linkAccount(newAccount);
144 | return {
145 | currentUser: currentUserByEmail,
146 | jwt: newJwt,
147 | };
148 | } else {
149 | // Create new user, create new JWT for authentication and return new user
150 | const newUser: Omit = {
151 | name: formattedGithubUser.name,
152 | email: formattedGithubUser.email,
153 | image: formattedGithubUser.picture,
154 | emailVerified: new Date(),
155 | };
156 | const user = await PrismaAuth.createUser(newUser);
157 | if (user) {
158 | const newJwt = await encode({
159 | token: formattedGithubUser,
160 | secret: SECRET,
161 | });
162 | const newAccount: Account = {
163 | provider: 'github',
164 | providerAccountId: formattedGithubUser.sub,
165 | type: 'oauth',
166 | userId: user.id,
167 | access_token: newJwt,
168 | id_token: accessTokenResponse.access_token,
169 | token_type: accessTokenResponse?.token_type,
170 | scope: accessTokenResponse?.scope,
171 | };
172 | await PrismaAuth.linkAccount(newAccount);
173 |
174 | return {
175 | currentUser: user,
176 | jwt: newJwt,
177 | };
178 | }
179 |
180 | throw new TRPCError({
181 | code: 'BAD_REQUEST',
182 | message: 'Unable to create new user',
183 | });
184 | }
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/packages/api/expo-auth/google-auth.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from '@trpc/server';
2 | import jwtDecode, { JwtPayload } from 'jwt-decode';
3 | import { encode, JWT } from 'next-auth/jwt';
4 | import { PrismaAuth } from '../expo-auth';
5 | import { ISS_GOOGLE_VALUES, MY_EXPO_URLS, SECRET } from './constants';
6 | import { AuthResponse } from './zod';
7 | import { AdapterUser } from 'next-auth/adapters';
8 | import { Account } from 'next-auth/core/types';
9 |
10 | interface DecodedJwtPayload extends JwtPayload {
11 | name: string;
12 | email: string;
13 | picture: string;
14 | }
15 | export async function signInWithGoogle(response: AuthResponse) {
16 | if (
17 | !response?.authentication?.idToken ||
18 | !response.authentication.accessToken
19 | ) {
20 | throw new TRPCError({
21 | code: 'FORBIDDEN',
22 | message: 'Failed to authenticate',
23 | cause: 'Missing oAuth Information',
24 | });
25 | }
26 |
27 | const verifyJwtFromExpoAuthGoogle = await fetch(
28 | `https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${response.authentication.accessToken}`
29 | );
30 |
31 | // Verified Result from Google
32 | const verifiedResult: JwtPayload = await verifyJwtFromExpoAuthGoogle.json();
33 |
34 | // id_token from Expo App
35 | const decodedIdToken = jwtDecode(response.authentication.idToken);
36 |
37 | // Check JwtPayloads from Google and from Expo match and are valid
38 | if (
39 | !ISS_GOOGLE_VALUES.some((iss) => decodedIdToken.iss === iss) ||
40 | !(
41 | decodedIdToken?.aud === verifiedResult?.aud &&
42 | decodedIdToken?.aud === process.env.GOOGLE_CLIENT_ID
43 | ) ||
44 | decodedIdToken.sub !== verifiedResult.sub ||
45 | !MY_EXPO_URLS.some((url) => response?.url.startsWith(url))
46 | ) {
47 | throw new TRPCError({ code: 'FORBIDDEN' });
48 | }
49 |
50 | if (!decodedIdToken.sub) {
51 | throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid Response' });
52 | }
53 |
54 | // Check if user already has a Google oAuth Account
55 | // If they do, create new JWT for authentication and return user
56 | const currentUserByAccount = await PrismaAuth.getUserByAccount({
57 | providerAccountId: decodedIdToken.sub,
58 | provider: 'google',
59 | });
60 |
61 | if (currentUserByAccount) {
62 | decodedIdToken.sub = currentUserByAccount.id;
63 | const newJwt = await encode({
64 | token: decodedIdToken as JWT,
65 | secret: SECRET,
66 | });
67 | return {
68 | currentUser: currentUserByAccount,
69 | jwt: newJwt,
70 | };
71 | } else {
72 | // Check if user without Google oAuth Account is already a user in our database
73 | // If they are, link Google oAuth Account with existing user, create new JWT for authentication and return user
74 | const currentUserByEmail = await PrismaAuth.getUserByEmail(
75 | (decodedIdToken as DecodedJwtPayload).email
76 | );
77 | if (currentUserByEmail) {
78 | const newJwt = await encode({
79 | token: decodedIdToken as JWT,
80 | secret: SECRET,
81 | });
82 | const newAccount: Account = {
83 | provider: 'google',
84 | providerAccountId: decodedIdToken.sub,
85 | type: 'oauth',
86 | userId: currentUserByEmail.id,
87 | access_token: newJwt,
88 | id_token: response.authentication.idToken,
89 | token_type: 'Bearer',
90 | scope:
91 | 'https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email',
92 | };
93 | await PrismaAuth.linkAccount(newAccount);
94 | return {
95 | currentUser: currentUserByEmail,
96 | jwt: newJwt,
97 | };
98 | } else {
99 | // Create new user, create new JWT for authentication and return new user
100 | const newUser: Omit = {
101 | name: (decodedIdToken as DecodedJwtPayload).name,
102 | email: (decodedIdToken as DecodedJwtPayload).email,
103 | image: (decodedIdToken as DecodedJwtPayload).picture,
104 | emailVerified: new Date(),
105 | };
106 | const user = await PrismaAuth.createUser(newUser);
107 | if (user) {
108 | const newJwt = await encode({
109 | token: decodedIdToken as JWT,
110 | secret: SECRET,
111 | });
112 | const newAccount: Account = {
113 | provider: 'google',
114 | providerAccountId: decodedIdToken.sub,
115 | type: 'oauth',
116 | userId: user.id,
117 | access_token: newJwt,
118 | id_token: response.authentication.idToken,
119 | token_type: 'Bearer',
120 | scope:
121 | 'https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email',
122 | };
123 | await PrismaAuth.linkAccount(newAccount);
124 |
125 | return {
126 | currentUser: user,
127 | jwt: newJwt,
128 | };
129 | }
130 |
131 | throw new TRPCError({
132 | code: 'BAD_REQUEST',
133 | message: 'Unable to create new user',
134 | });
135 | }
136 | }
137 | }
138 |
139 | // const verifiedResult = {
140 | // azp: 'blablablabla-blablabla.apps.googleusercontent.com', // GOOGLE_CLIENT_ID
141 | // aud: 'blablablabla-blablabla.apps.googleusercontent.com',// my GOOGLE_CLIENT_ID
142 | // sub: '12345678901234', // userId
143 | // scope: 'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
144 | // exp: '1658118254',
145 | // expires_in: '3599',
146 | // email: 'email@gmail.com', // same email
147 | // email_verified: 'true',
148 | // access_type: 'online'
149 | // }
150 | //
151 | // const decodedIdToken = {
152 | // iss: 'https://accounts.google.com', // this exact iss or accounts.google.com
153 | // azp: 'blablablabla-blablabla.apps.googleusercontent.com', // GOOGLE_CLIENT_ID
154 | // aud: 'blablablabla-blablabla.apps.googleusercontent.com', // GOOGLE_CLIENT_ID
155 | // sub: '12345678901234', // userId
156 | // email: 'email@gmail.com',
157 | // email_verified: true,
158 | // at_hash: 'vowjv9wev9ewv9uev',
159 | // name: 'User Name',
160 | // picture: 'https://lh3.googleusercontent.com/a/Ablahblah-blahablh-blah',
161 | // given_name: 'User',
162 | // family_name: 'Name',
163 | // locale: 'en',
164 | // iat: 1658114654,
165 | // exp: 1658118254
166 | // }
167 |
--------------------------------------------------------------------------------
/packages/api/expo-auth/index.ts:
--------------------------------------------------------------------------------
1 | import { signInWithGoogle } from './google-auth';
2 | import { NextAuthProviderIds } from '../next-auth';
3 | import { signInWithGithub } from './github-auth';
4 | import { AuthResponse } from './zod';
5 | import { signInWithApple } from './apple-auth';
6 |
7 | export const Providers: Record<
8 | typeof NextAuthProviderIds[number],
9 | SignInFunction
10 | > = {
11 | google: signInWithGoogle,
12 | github: signInWithGithub,
13 | apple: signInWithApple,
14 | };
15 |
16 | export * from './prisma-auth';
17 | export * from './zod';
18 |
19 | type AsyncReturnType Promise> = T extends (
20 | ...args: any
21 | ) => Promise
22 | ? R
23 | : any;
24 |
25 | type SignInResponse = AsyncReturnType;
26 |
27 | type SignInFunction = (response: AuthResponse) => Promise;
28 |
--------------------------------------------------------------------------------
/packages/api/expo-auth/prisma-auth.ts:
--------------------------------------------------------------------------------
1 | import { Prisma, prisma } from '../db';
2 | import type { Adapter } from 'next-auth/adapters';
3 |
4 | export const PrismaAuth: Adapter = {
5 | createUser: (data) => prisma.user.create({ data }),
6 | getUser: (id) => prisma.user.findUnique({ where: { id } }),
7 | getUserByEmail: (email) => prisma.user.findUnique({ where: { email } }),
8 | async getUserByAccount(provider_providerAccountId) {
9 | const account = await prisma.account.findUnique({
10 | where: { provider_providerAccountId },
11 | select: { user: true },
12 | });
13 | return account?.user ?? null;
14 | },
15 | updateUser: (data) => prisma.user.update({ where: { id: data.id }, data }),
16 | deleteUser: (id) => prisma.user.delete({ where: { id } }),
17 | linkAccount: (data) => prisma.account.create({ data }) as any,
18 | unlinkAccount: (provider_providerAccountId) =>
19 | prisma.account.delete({ where: { provider_providerAccountId } }) as any,
20 | async getSessionAndUser(sessionToken) {
21 | const userAndSession = await prisma.session.findUnique({
22 | where: { sessionToken },
23 | include: { user: true },
24 | });
25 | if (!userAndSession) return null;
26 | const { user, ...session } = userAndSession;
27 | return { user, session };
28 | },
29 | createSession: (data) => prisma.session.create({ data }),
30 | updateSession: (data) =>
31 | prisma.session.update({ data, where: { sessionToken: data.sessionToken } }),
32 | deleteSession: (sessionToken) =>
33 | prisma.session.delete({ where: { sessionToken } }),
34 | createVerificationToken: (data) => prisma.verificationToken.create({ data }),
35 | async useVerificationToken(identifier_token) {
36 | try {
37 | return await prisma.verificationToken.delete({
38 | where: { identifier_token },
39 | });
40 | } catch (error) {
41 | // If token already used/deleted, just return null
42 | // https://www.prisma.io/docs/reference/api-reference/error-reference#p2025
43 | if ((error as Prisma.PrismaClientKnownRequestError).code === 'P2025')
44 | return null;
45 | throw error;
46 | }
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/packages/api/expo-auth/zod.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const AuthErrorShape = z.object({
4 | code: z.string(),
5 | description: z.string().nullable().optional(),
6 | message: z.string().nullable().optional(),
7 | name: z.string().nullable().optional(),
8 | params: z.record(z.string().min(1), z.any()).nullable().optional(),
9 | stack: z.string().nullable().optional(),
10 | state: z.string().nullable().optional(),
11 | uri: z.string().nullable().optional(),
12 | });
13 | const AuthTokenResponseShape = z.object({
14 | accessToken: z.string().nullable().optional(),
15 | expiresIn: z.number().nullable().optional(),
16 | getRequestConfig: z.function().nullable().optional(),
17 | idToken: z.string().nullable().optional(),
18 | issuedAt: z.number().nullable().optional(),
19 | refreshAsync: z.function().nullable().optional(),
20 | refreshToken: z.string().nullable().optional(),
21 | scope: z.string().nullable().optional(),
22 | shouldRefresh: z.boolean().nullable().optional(),
23 | state: z.string().nullable().optional(),
24 | tokenType: z.string().nullable().optional(),
25 | });
26 |
27 | export const SignInResponseInput = z
28 | .object({
29 | type: z.string(),
30 | errorCode: z.string().nullable().optional(),
31 | error: AuthErrorShape.nullable().optional(),
32 | params: z.record(z.string(), z.any()).nullable().optional(),
33 | authentication: AuthTokenResponseShape.nullable().optional(),
34 | url: z.string(),
35 | })
36 | .nullable();
37 |
38 | export type AuthResponse = z.infer;
39 |
--------------------------------------------------------------------------------
/packages/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './db';
2 | export * from './router';
3 | export * from './next-auth';
4 |
--------------------------------------------------------------------------------
/packages/api/next-auth/index.ts:
--------------------------------------------------------------------------------
1 | import { type NextAuthOptions } from 'next-auth';
2 | import GithubProvider from 'next-auth/providers/github';
3 | import GoogleProvider from 'next-auth/providers/google';
4 | import AppleProvider from 'next-auth/providers/apple';
5 | import EmailProvider from 'next-auth/providers/email';
6 |
7 | import { PrismaAdapter } from '@next-auth/prisma-adapter';
8 | import { prisma } from '../db';
9 |
10 | const providers = [
11 | GithubProvider({
12 | clientId: process.env.GITHUB_ID,
13 | clientSecret: process.env.GITHUB_SECRET,
14 | }),
15 | GoogleProvider({
16 | clientId: process.env.GOOGLE_CLIENT_ID,
17 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
18 | }),
19 | EmailProvider({
20 | server: {
21 | host: 'smtp.gmail.com',
22 | port: 465,
23 | auth: {
24 | type: 'OAuth2',
25 | user: process.env.EMAIL_SERVER_USER,
26 | clientId: process.env.GOOGLE_CLIENT_ID,
27 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
28 | refreshToken: process.env.EMAIL_SERVER_REFRESH_TOKEN,
29 | },
30 | },
31 | from: process.env.EMAIL_SERVER_USER,
32 | }),
33 | AppleProvider({
34 | // @TODO
35 | clientId: '',
36 | clientSecret: '',
37 | }),
38 | ];
39 |
40 | export const NextAuthProviderIds = ['github', 'google', 'apple'] as const; // Manually ajdust
41 |
42 | export const authOptions: NextAuthOptions = {
43 | adapter: PrismaAdapter(prisma),
44 | providers,
45 | callbacks: {
46 | jwt: async ({ user, token }) => {
47 | if (user) {
48 | token.uid = user.id;
49 | }
50 | return token;
51 | },
52 | session: async ({ session, token }) => {
53 | if (session?.user && typeof token.uid === 'string') {
54 | session.user.id = token.uid;
55 | }
56 | return session;
57 | },
58 | },
59 | secret: process.env.NEXTAUTH_SECRET,
60 | session: {
61 | strategy: 'jwt',
62 | },
63 | };
64 |
--------------------------------------------------------------------------------
/packages/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api",
3 | "version": "0.0.0",
4 | "main": "./index.ts",
5 | "types": "./index.ts",
6 | "license": "MIT",
7 | "dependencies": {
8 | "jsonwebtoken": "^8.5.1",
9 | "jwt-decode": "^3.1.2",
10 | "nodemailer": "^6.7.7"
11 | },
12 | "devDependencies": {
13 | "@types/jsonwebtoken": "^8.5.8",
14 | "@types/nodemailer": "^6.4.4"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/api/router/appRouter.ts:
--------------------------------------------------------------------------------
1 | import { createRouter } from './context';
2 | import superjson from 'superjson';
3 |
4 | import { exampleRouter } from './example';
5 | import { expoAuthRouter } from './expoAuth';
6 |
7 | export const appRouter = createRouter()
8 | .transformer(superjson)
9 | .merge('example.', exampleRouter)
10 | .merge('expo-auth.', expoAuthRouter);
11 |
12 | // export type definition of API
13 | export type AppRouter = typeof appRouter;
14 |
--------------------------------------------------------------------------------
/packages/api/router/context.ts:
--------------------------------------------------------------------------------
1 | import * as trpc from '@trpc/server';
2 | import * as trpcNext from '@trpc/server/adapters/next';
3 | import { decode, getToken } from 'next-auth/jwt';
4 | import { prisma } from '../db';
5 | import { PrismaAuth } from '../expo-auth';
6 |
7 | const secret = process.env.NEXTAUTH_SECRET;
8 |
9 | export const createContext = async (
10 | opts?: trpcNext.CreateNextContextOptions
11 | ) => {
12 | const req = opts?.req;
13 | const res = opts?.res;
14 | let decodedToken;
15 | let user;
16 | let jwt;
17 | try {
18 | const webToken =
19 | req && !req?.headers.authorization
20 | ? await getToken({ req, secret, raw: true })
21 | : undefined;
22 |
23 | const mobileToken = req?.headers.authorization;
24 |
25 | jwt = webToken || mobileToken;
26 | decodedToken = await decode({ token: jwt, secret });
27 |
28 | if (decodedToken && decodedToken.sub) {
29 | user = await PrismaAuth.getUser(decodedToken.sub);
30 | }
31 | } catch (e) {
32 | console.log(e);
33 | }
34 | return {
35 | req,
36 | res,
37 | session: user,
38 | jwt,
39 | prisma,
40 | };
41 | };
42 |
43 | type Context = trpc.inferAsyncReturnType;
44 |
45 | export const createRouter = () => trpc.router();
46 |
--------------------------------------------------------------------------------
/packages/api/router/example.ts:
--------------------------------------------------------------------------------
1 | import { createRouter } from "./context";
2 | import { z } from "zod";
3 |
4 | export const exampleRouter = createRouter()
5 | .query("hello", {
6 | input: z
7 | .object({
8 | text: z.string().nullish(),
9 | })
10 | .nullish(),
11 | resolve({ input }) {
12 | return {
13 | greeting: `Hello ${input?.text ?? "world"}`,
14 | };
15 | },
16 | })
17 | .query("getAll", {
18 | async resolve({ ctx }) {
19 | return await ctx.prisma.example.findMany();
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/packages/api/router/expoAuth.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from '@trpc/server';
2 | import { decode, JWT } from 'next-auth/jwt';
3 | import { SendVerificationRequestParams } from 'next-auth/providers/email';
4 | import { createTransport } from 'nodemailer';
5 | import SMTPTransport from 'nodemailer/lib/smtp-transport';
6 | import { z } from 'zod';
7 | import { Providers, SignInResponseInput } from '../expo-auth';
8 | import { NextAuthProviderIds } from '../next-auth';
9 | import { createRouter } from './context';
10 |
11 | interface ExpoAuthJWT extends JWT {
12 | exp?: number;
13 | }
14 |
15 | const secret = process.env.NEXTAUTH_SECRET;
16 |
17 | export const expoAuthRouter = createRouter()
18 | .mutation('signIn', {
19 | input: z.object({
20 | response: SignInResponseInput,
21 | provider: z.enum(NextAuthProviderIds),
22 | }),
23 | async resolve({ input }) {
24 | try {
25 | return await Providers[input.provider](input.response);
26 | } catch (err) {
27 | console.error(err);
28 | }
29 | },
30 | })
31 | .middleware(async ({ ctx, next }) => {
32 | if (!ctx.jwt) {
33 | throw new TRPCError({
34 | code: 'UNAUTHORIZED',
35 | message: 'Missing token',
36 | cause: {
37 | jwt: ctx.jwt,
38 | },
39 | });
40 | }
41 | // Verify JWT token is not expired
42 | const decodedToken = (await decode({
43 | token: ctx.jwt,
44 | secret,
45 | })) as ExpoAuthJWT;
46 | if (decodedToken?.exp && new Date(decodedToken.exp * 1000) < new Date()) {
47 | throw new TRPCError({
48 | code: 'UNAUTHORIZED',
49 | message: 'Token expired',
50 | cause: {
51 | expiredToken: new Date(decodedToken.exp * 1000) < new Date(),
52 | now: new Date(),
53 | expiration: new Date(decodedToken.exp * 1000),
54 | },
55 | });
56 | }
57 | return next();
58 | })
59 | .query('getSession', {
60 | resolve({ ctx }) {
61 | return ctx.session;
62 | },
63 | });
64 |
65 | const server: SMTPTransport['options'] = {
66 | host: 'smtp.gmail.com',
67 | port: 465,
68 | auth: {
69 | type: 'OAuth2',
70 | user: process.env.EMAIL_SERVER_USER,
71 | clientId: process.env.GOOGLE_CLIENT_ID,
72 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
73 | refreshToken: process.env.EMAIL_SERVER_REFRESH_TOKEN,
74 | },
75 | };
76 |
77 | // @TODO implement
78 | // 1. sendVerificationRequest
79 | // 2. verify token from login and provide new token
80 |
81 | // In expo app
82 | // const redirectUrl = Linking.createURL('path/into/app', {
83 | // queryParams: { hello: 'world' },
84 | // });
85 | async function sendVerificationRequest(params: SendVerificationRequestParams) {
86 | const {
87 | identifier,
88 | url = 'exp://127.0.0.1:19000/--/path/into/app?hello=world',
89 | } = params;
90 | const { host } = new URL(url);
91 | const transport = createTransport(server);
92 | const result = await transport.sendMail({
93 | to: identifier,
94 | from: process.env.EMAIL_SERVER_USER,
95 | subject: `Sign in to ${host}`,
96 | text: text({ url, host }),
97 | html: html({ url, host }),
98 | });
99 | const failed = result.rejected.concat(result.pending).filter(Boolean);
100 | if (failed.length) {
101 | throw new Error(`Email(s) (${failed.join(', ')}) could not be sent`);
102 | }
103 | }
104 |
105 | function html(params: { url: string; host: string }) {
106 | const { url, host } = params;
107 |
108 | const escapedHost = host.replace(/\./g, '.');
109 |
110 | const brandColor = '#346df1';
111 | const buttonText = '#fff';
112 |
113 | const color = {
114 | background: '#f9f9f9',
115 | text: '#444',
116 | mainBackground: '#fff',
117 | buttonBackground: brandColor,
118 | buttonBorder: brandColor,
119 | buttonText,
120 | };
121 |
122 | return `
123 |
124 |
126 |
127 |
129 | Sign in to ${escapedHost}
130 | |
131 |
132 |
133 |
134 |
142 | |
143 |
144 |
145 |
147 | If you did not request this email you can safely ignore it.
148 | |
149 |
150 |
151 |
152 | `;
153 | }
154 |
155 | /** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */
156 | function text({ url, host }: { url: string; host: string }) {
157 | return `Sign in to ${host}\n${url}\n\n`;
158 | }
159 |
160 | // HTML tag attributes
161 | // http://localhost:3000/api/auth/callback/email?
162 | // callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F&
163 | // token=deb45fa3baa3f7eb151c2263bfc53dbb3abcea5497aa9d3e0690b3797fbb008f&
164 | // email=example%40gmail.com"
165 |
166 | // target="_blank"
167 |
168 | // data-saferedirecturl=
169 | // "https://www.google.com/url?
170 | // q=http://localhost:3000/api/auth/callback/email?
171 | // callbackUrl%3Dhttp%253A%252F%252Flocalhost%253A3000%252F%26token%3Ddeb45fa3baa3f7eb151c2263bfc53dbb3abcea5497aa9d3e0690b3797fbb008f%26email%3Dexample%2540gmail.com&
172 | // source=gmail&
173 | // ust=1658799806415000&
174 | // usg=AOvVaw3I_ovZ9lRPyC3S46fkBwZM"
175 |
--------------------------------------------------------------------------------
/packages/api/router/index.ts:
--------------------------------------------------------------------------------
1 | export * from './appRouter';
2 | export * from './context';
3 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | "include": ["."]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/api/types/global.ts:
--------------------------------------------------------------------------------
1 | import { DefaultUser } from 'next-auth';
2 |
3 | declare module 'next-auth' {
4 | /**
5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
6 | */
7 | interface Session {
8 | user: DefaultUser & {
9 | id: unknown;
10 | };
11 | }
12 | }
13 |
14 | declare global {
15 | namespace NodeJS {
16 | interface ProcessEnv {
17 | GITHUB_AUTH_TOKEN: string;
18 | GITHUB_ID: string;
19 | GITHUB_SECRET: string;
20 | GOOGLE_CLIENT_ID: string;
21 | GOOGLE_CLIENT_SECRET: string;
22 | PORT?: string;
23 | BASE_URL: string;
24 | NEXTAUTH_SECRET: string;
25 | EMAIL_SERVER_USER: string;
26 | EMAIL_SERVER_REFRESH_TOKEN: string;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["next", "prettier"],
3 | rules: {
4 | "@next/next/no-html-link-for-pages": "off",
5 | "react/jsx-key": "off",
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-config-custom",
3 | "version": "0.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "eslint-config-next": "^12.0.8",
8 | "eslint-config-prettier": "^8.3.0",
9 | "eslint-plugin-react": "7.28.0"
10 | },
11 | "publishConfig": {
12 | "access": "public"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './trpc';
2 |
--------------------------------------------------------------------------------
/packages/hooks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hooks",
3 | "version": "0.0.0",
4 | "main": "./index.ts",
5 | "types": "./index.ts",
6 | "license": "MIT",
7 | "devDependencies": {
8 | "api": "*",
9 | "tsconfig": "*"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/hooks/trpc.ts:
--------------------------------------------------------------------------------
1 | import { createReactQueryHooks } from '@trpc/react';
2 | // ℹ️ Type-only import:
3 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
4 | import type { AppRouter } from 'api';
5 | import type { inferProcedureOutput, inferProcedureInput } from '@trpc/server';
6 | import superjson from 'superjson';
7 | /**
8 | * A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`.
9 | * @link https://trpc.io/docs/react#3-create-trpc-hooks
10 | */
11 |
12 | export const trpc = createReactQueryHooks();
13 | export const transformer = superjson;
14 |
15 | /**
16 | * This is a helper method to infer the output of a query resolver
17 | * @example type HelloOutput = inferQueryOutput<'hello'>
18 | */
19 | export type inferQueryOutput<
20 | TRouteKey extends keyof AppRouter['_def']['queries']
21 | > = inferProcedureOutput;
22 |
23 | export type inferQueryInput<
24 | TRouteKey extends keyof AppRouter['_def']['queries']
25 | > = inferProcedureInput;
26 |
27 | export type inferMutationOutput<
28 | TRouteKey extends keyof AppRouter['_def']['mutations']
29 | > = inferProcedureOutput;
30 |
31 | export type inferMutationInput<
32 | TRouteKey extends keyof AppRouter['_def']['mutations']
33 | > = inferProcedureInput;
34 |
35 | export type { AppRouter };
36 |
--------------------------------------------------------------------------------
/packages/hooks/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | "include": ["."]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/tsconfig/README.md:
--------------------------------------------------------------------------------
1 | # `tsconfig`
2 |
3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from.
4 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "moduleResolution": "node",
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "preserveWatchOutput": true,
16 | "skipLibCheck": true,
17 | "strict": true
18 | },
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/tsconfig/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "target": "es5",
7 | "lib": ["dom", "dom.iterable", "esnext"],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "incremental": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "allowSyntheticDefaultImports" : true
20 | },
21 | "include": ["src", "next-env.d.ts"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.0.0",
4 | "private": true,
5 | "main": "index.js",
6 | "files": [
7 | "base.json",
8 | "nextjs.json",
9 | "react-library.json"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "jsx": "react-jsx"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/ui-web/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export * from './src/Button';
3 |
--------------------------------------------------------------------------------
/packages/ui-web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui-web",
3 | "version": "0.0.0",
4 | "main": "./index.tsx",
5 | "types": "./index.tsx",
6 | "license": "MIT",
7 | "scripts": {
8 | "lint": "eslint *.ts*"
9 | },
10 | "devDependencies": {
11 | "@types/react": "~17.0.21",
12 | "@types/react-dom": "17.0.17",
13 | "eslint": "^7.32.0",
14 | "eslint-config-custom": "*",
15 | "react": "17.0.2",
16 | "tsconfig": "*",
17 | "typescript": "^4.5.2"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/ui-web/src/Button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export const Button = () => {
3 | return ;
4 | };
5 |
--------------------------------------------------------------------------------
/packages/ui-web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "sqlite"
10 | url = "file:./db.sqlite"
11 | // url = env("DATABASE_URL")
12 | }
13 |
14 | model Example {
15 | id String @id @default(cuid())
16 | }
17 |
18 | // Necessary for Next auth
19 | model Account {
20 | id String @id @default(cuid())
21 | userId String
22 | type String
23 | provider String
24 | providerAccountId String
25 | refresh_token String?
26 | access_token String?
27 | expires_at Int?
28 | token_type String?
29 | scope String?
30 | id_token String?
31 | session_state String?
32 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
33 |
34 | @@unique([provider, providerAccountId])
35 | }
36 |
37 | model Session {
38 | id String @id @default(cuid())
39 | sessionToken String @unique
40 | userId String
41 | expires DateTime
42 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
43 | }
44 |
45 | model User {
46 | id String @id @default(cuid())
47 | name String?
48 | email String? @unique
49 | emailVerified DateTime?
50 | image String?
51 | accounts Account[]
52 | sessions Session[]
53 | }
54 |
55 | model VerificationToken {
56 | identifier String
57 | token String @unique
58 | expires DateTime
59 |
60 | @@unique([identifier, token])
61 | }
62 |
--------------------------------------------------------------------------------
/scripts/clean.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo "[1/2] Deleting all node_modules..."
3 | find . -type d -name "node_modules" -exec rm -rf '{}' +
4 |
5 | echo "[2/2] Deleting yarn.lock..."
6 | rm -rf yarn.lock
7 |
8 | echo "Cleanup Done! ✅"
9 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "pipeline": {
3 | "build": {
4 | "dependsOn": ["^build"],
5 | "outputs": ["dist/**", ".next/**"]
6 | },
7 | "lint": {
8 | "outputs": []
9 | },
10 | "dev": {
11 | "cache": false
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------