├── .github └── workflows │ ├── codeql-analysis.yml │ └── deploy-to-cloud-run-production.yml ├── README.md ├── api ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Dockerfile.local ├── Dockerfile.production ├── nest-cli.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20211001101403_init │ │ │ └── migration.sql │ │ ├── 20211002101217_remove_password │ │ │ └── migration.sql │ │ ├── 20211003044322_stripe_custmer_id │ │ │ └── migration.sql │ │ ├── 20211003072426_add_stripe_column │ │ │ └── migration.sql │ │ ├── 20211003082528_auth0_s_ub │ │ │ └── migration.sql │ │ ├── 20211005134842_stripe │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── auth │ │ ├── auth.module.ts │ │ └── jwt.strategy.ts │ ├── config │ │ └── configuration.ts │ ├── main.ts │ ├── prisma.service.ts │ ├── stripe │ │ ├── stripe.module.ts │ │ └── stripe.service.ts │ └── user │ │ ├── user.service.ts │ │ ├── users.controller.ts │ │ └── users.module.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock ├── client ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── components │ ├── atoms │ │ └── .gitkeep │ ├── molecules │ │ ├── check-form.js │ │ └── user-menu.tsx │ └── organisms │ │ ├── footer.tsx │ │ ├── header.tsx │ │ └── layout.tsx ├── libs │ ├── constants.ts │ └── style-utils.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.js │ └── index.js ├── postcss.config.js ├── public │ ├── favicon.ico │ └── vercel.svg ├── styles │ ├── globals.css │ ├── tailwind-utils.css │ └── tailwind.css ├── tailwind.config.js ├── tsconfig.json └── yarn.lock └── docker-compose.yml /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '25 9 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-cloud-run-production.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - deploy/prod 5 | - main 6 | 7 | name: Build and Deploy a Container to Cloud Run 8 | env: 9 | PROJECT_ID: ${{ secrets.GCP_PROJECT }} 10 | SERVICE: nestjs-prisma-nextjs 11 | REGION: asia-northeast1 12 | IMAGE: gcr.io/${{ secrets.GCP_PROJECT }}/nestjs-prisma-nextjs:${{ github.sha }} 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup Cloud SDK 22 | uses: google-github-actions/setup-gcloud@v0.2.0 23 | with: 24 | project_id: ${{ env.PROJECT_ID }} 25 | service_account_key: ${{ secrets.GCP_SA_KEY }} 26 | export_default_credentials: true # Set to true to authenticate the Cloud Run action 27 | 28 | - name: Authorize Docker push 29 | run: gcloud auth configure-docker 30 | 31 | - name: Build and Push Container 32 | run: |- 33 | docker build -t ${{ env.IMAGE }} api/ -f api/Dockerfile.production \ 34 | --build-arg database_url=${{ secrets.DATABASE_URL }} \ 35 | --build-arg shadow_database_url=${{ secrets.SHADOW_DATABASE_URL }} \ 36 | --build-arg auth0_domain=${{ secrets.AUTH0_DOMAIN }} \ 37 | --build-arg auth0_audience=${{ secrets.AUTH0_AUDIENCE }} \ 38 | --build-arg auth0_issuer_url=${{ secrets.AUTH0_ISSUER_URL }} \ 39 | --build-arg auth0_client_id=${{ secrets.AUTH0_CLIENT_ID }} \ 40 | --build-arg auth0_client_secret=${{ secrets.AUTH0_CLIENT_SECRET }} \ 41 | --build-arg stripe_secret_key=${{ secrets.STRIPE_SECRET_KEY }} 42 | docker push ${{ env.IMAGE }} 43 | 44 | - name: Deploy to Cloud Run 45 | run: |- 46 | gcloud run deploy ${{ env.SERVICE }} \ 47 | --region ${{ env.REGION }} \ 48 | --image ${{ env.IMAGE }} \ 49 | --platform managed \ 50 | --allow-unauthenticated 51 | 52 | - name: Show Output 53 | run: echo ${{ steps.deploy.outputs.url }} 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💎 NestJS Prisma NextJS 2 | 3 | Saas Template 4 | 5 | ## ⚡️ Tech Stack 6 | 7 | ### Open Source 8 | 9 | - [Next.js](https://nextjs.org/) 10 | - [Nest.js](https://nestjs.com/) 11 | - [PostgreSQL](https://www.postgresql.org/) 12 | - [Prisma](https://www.prisma.io/) 13 | 14 | ### Infra Services 15 | 16 | - [Auth0](https://auth0.com/jp/) 17 | - [Stripe](https://stripe.com/jp/) 18 | - [SendGrid](https://sendgrid.com/) 19 | - [Vercel](https://vercel.com/docs/concepts/) 20 | - [Cloud Run](https://cloud.google.com/run/) 21 | - [Cloud SQL](https://cloud.google.com/sql/) 22 | - [Cloud Storage](https://cloud.google.com/storage/) 23 | 24 | ## 👀 確認バージョン 25 | 26 | - Node.js: 14.x + 27 | 28 | ## 💡 開発環境セットアップ 29 | 30 | ### API と DB の起動 31 | 32 | `.env` をセットアップします。 33 | `AUTH0_ISSUER_URL` を正しい値に変更する。 34 | 35 | ``` bash 36 | $ cp api/.env.example api/.env 37 | ``` 38 | 39 | コンテナを起動します。 40 | 41 | ```bash 42 | $ docker compose up 43 | ``` 44 | 45 | Prisma で Database を migrate します。 46 | 47 | ``` bash 48 | $ docker compose exec api npx prisma migrate dev 49 | ``` 50 | 51 | `docker compose up` を実行すると、 `start:dev` から NestJS の API が起動します。 52 | 53 | ### フロントアプリの起動 54 | 55 | ``` bash 56 | $ cp client/.env.example client/.env 57 | $ cd client 58 | $ yarn # 依存関係を解消します 59 | $ yarn dev # http://localhost:3000 で起動されます 60 | ``` 61 | 62 | ### 起動アプリケーション一覧 63 | 64 | |URL|解説| 65 | |---|---| 66 | |http://localhost:8080|api server| 67 | |http://localhost:3000|client appication| 68 | |postgresql://postgres:password@db:5432/nestjs-prisma-nextjs|postgresql server| 69 | 70 | > 💡 PostgreSQLのDBクライアントは、 [TablePlus](https://tableplus.com/) を使っています。 71 | 72 | ## ⛵️ デプロイ 73 | 74 | GCP Cloud Run にデプロイされます。 75 | 76 | ### アーキテクチャ 77 | 78 | github -> cloud build -> cloud run 79 | 80 | ## ⚡️ APIリファレンス 81 | 82 | APIの確認は、 [curl](https://curl.se/docs/manpage.html) もしくは、 [Postman API Platform](https://www.postman.com/) をオススメします。 83 | 複雑な API は、 Postman が良いです。 84 | 85 | ### 汎用的な `curl` オプション 86 | 87 | |オプション|解説| 88 | |---|---| 89 | |出力にHTTP応答ヘッダーを含めます|`-i` or `-include`| 90 | |HTTPメソッドの指定|`-X` or `--header`| 91 | |ヘッダーの指定|`-H` or `--request`| 92 | |データ指定|`-d` or `--data`| 93 | 94 | ## User 95 | 96 | ### GET `/users` 97 | 98 | `$ACCESS_TOKEN`はAuth0から取得します。 99 | 100 | ```bash 101 | curl -X GET http://localhost:8080/users/ \ 102 | -H "Content-Type: application/json" \ 103 | -H "Authorization: Bearer $ACCESS_TOKEN" 104 | ``` 105 | 106 | ### PUT `/users` 107 | 108 | ```bash 109 | curl -X PUT http://localhost:8080/users/ \ 110 | -H "Content-Type: application/json" \ 111 | -H "Authorization: Bearer $ACCESS_TOKEN" \ 112 | -d '{"email": "example@gmail.com"}' 113 | ``` 114 | 115 | ### DELETE `/users` 116 | 117 | ```bash 118 | curl -X DELETE http://localhost:8080/users/ \ 119 | -H "Content-Type: application/json" \ 120 | -H "Authorization: Bearer $ACCESS_TOKEN" 121 | ``` 122 | -------------------------------------------------------------------------------- /api/.env.example: -------------------------------------------------------------------------------- 1 | # Basic 2 | DATABASE_URL="postgresql://postgres:password@db:5432/nestjs-prisma-nextjs" 3 | SHADOW_DATABASE_URL="" 4 | PORT="8080" 5 | # Auth0 6 | AUTH0_DOMAIN="xxxxx.jp.auth0.com" 7 | AUTH0_AUDIENCE="http://localhost:8080" 8 | AUTH0_ISSUER_URL="https://xxxxx.jp.auth0.com/" 9 | AUTH0_CLIENT_ID="" 10 | AUTH0_CLIENT_SECRET="" 11 | # Stripe 12 | STRIPE_SECRET_KEY="sk-xxx" -------------------------------------------------------------------------------- /api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | node_modules 37 | # Keep environment variables out of version control 38 | .env 39 | -------------------------------------------------------------------------------- /api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /api/Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /usr/src/app 4 | -------------------------------------------------------------------------------- /api/Dockerfile.production: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /usr/src/app 4 | COPY . . 5 | 6 | ARG database_url 7 | ENV DATABASE_URL=$database_url 8 | ARG shadow_database_url 9 | ENV SHADOW_DATABASE_URL=$shadow_database_url 10 | ARG auth0_domain 11 | ENV AUTH0_DOMAIN=$auth0_domain 12 | ARG auth0_audience 13 | ENV AUTH0_AUDIENCE=$auth0_audience 14 | ARG auth0_issuer_url 15 | ENV AUTH0_ISSUER_URL=$auth0_issuer_url 16 | ARG auth0_client_id 17 | ENV AUTH0_CLIENT_ID=$auth0_client_id 18 | ARG auth0_client_secret 19 | ENV AUTH0_CLIENT_SECRET=$auth0_client_secret 20 | ARG stripe_secret_key 21 | ENV STRIPE_SECRET_KEY=$stripe_secret_key 22 | 23 | RUN yarn 24 | RUN npx prisma generate 25 | RUN npx prisma migrate dev 26 | 27 | CMD [ "yarn", "start:dev" ] -------------------------------------------------------------------------------- /api/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\"", 12 | "start": "nest start -p ${PORT}", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^8.0.0", 25 | "@nestjs/config": "^1.0.1", 26 | "@nestjs/core": "^8.0.0", 27 | "@nestjs/jwt": "^8.0.0", 28 | "@nestjs/passport": "^8.0.1", 29 | "@nestjs/platform-express": "^8.0.0", 30 | "@prisma/client": "^2.29.1", 31 | "bcrypt": "^5.0.1", 32 | "dotenv": "^10.0.0", 33 | "jwks-rsa": "^2.0.4", 34 | "passport": "^0.5.0", 35 | "passport-jwt": "^4.0.0", 36 | "prisma": "^2.29.1", 37 | "reflect-metadata": "^0.1.13", 38 | "rimraf": "^3.0.2", 39 | "rxjs": "^7.2.0", 40 | "stripe": "^8.178.0" 41 | }, 42 | "devDependencies": { 43 | "@nestjs/cli": "^8.0.0", 44 | "@nestjs/schematics": "^8.0.0", 45 | "@nestjs/testing": "^8.0.0", 46 | "@types/bcrypt": "^5.0.0", 47 | "@types/express": "^4.17.13", 48 | "@types/jest": "^26.0.24", 49 | "@types/node": "^16.0.0", 50 | "@types/passport-jwt": "^3.0.6", 51 | "@types/supertest": "^2.0.11", 52 | "@typescript-eslint/eslint-plugin": "^4.28.2", 53 | "@typescript-eslint/parser": "^4.28.2", 54 | "eslint": "^7.30.0", 55 | "eslint-config-prettier": "^8.3.0", 56 | "eslint-plugin-prettier": "^3.4.0", 57 | "jest": "27.0.6", 58 | "prettier": "^2.3.2", 59 | "supertest": "^6.1.3", 60 | "ts-jest": "^27.0.3", 61 | "ts-loader": "^9.2.3", 62 | "ts-node": "^10.0.0", 63 | "tsconfig-paths": "^3.10.1", 64 | "typescript": "^4.3.5" 65 | }, 66 | "jest": { 67 | "moduleFileExtensions": [ 68 | "js", 69 | "json", 70 | "ts" 71 | ], 72 | "rootDir": "src", 73 | "testRegex": ".*\\.spec\\.ts$", 74 | "transform": { 75 | "^.+\\.(t|j)s$": "ts-jest" 76 | }, 77 | "collectCoverageFrom": [ 78 | "**/*.(t|j)s" 79 | ], 80 | "coverageDirectory": "../coverage", 81 | "testEnvironment": "node" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /api/prisma/migrations/20211001101403_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "email" TEXT NOT NULL, 5 | "password" TEXT, 6 | 7 | PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); 12 | -------------------------------------------------------------------------------- /api/prisma/migrations/20211002101217_remove_password/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `password` on the `User` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" DROP COLUMN "password"; 9 | -------------------------------------------------------------------------------- /api/prisma/migrations/20211003044322_stripe_custmer_id/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "stripeCustomerId" TEXT; 3 | -------------------------------------------------------------------------------- /api/prisma/migrations/20211003072426_add_stripe_column/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "stripeCardBrand" TEXT, 3 | ADD COLUMN "stripeCardLastFour" TEXT; 4 | -------------------------------------------------------------------------------- /api/prisma/migrations/20211003082528_auth0_s_ub/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[auth0Sub]` on the table `User` will be added. If there are existing duplicate values, this will fail. 5 | - Added the required column `auth0Sub` to the `User` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "User" ADD COLUMN "auth0Sub" TEXT NOT NULL; 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "User.auth0Sub_unique" ON "User"("auth0Sub"); 13 | -------------------------------------------------------------------------------- /api/prisma/migrations/20211005134842_stripe/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "stripeSubscriptionId" TEXT; 3 | -------------------------------------------------------------------------------- /api/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /api/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | shadowDatabaseUrl = env("SHADOW_DATABASE_URL") 5 | } 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | model User { 12 | id Int @id @default(autoincrement()) 13 | email String @unique 14 | auth0Sub String @unique 15 | stripeCustomerId String? 16 | stripeCardBrand String? 17 | stripeCardLastFour String? 18 | stripeSubscriptionId String? 19 | } 20 | -------------------------------------------------------------------------------- /api/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class AppController { 5 | constructor() {} 6 | } 7 | -------------------------------------------------------------------------------- /api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ConfigModule } from '@nestjs/config'; 5 | import configuration from './config/configuration'; 6 | import { AuthModule } from './auth/auth.module'; 7 | import { UsersModule } from './user/users.module'; 8 | import { StripeModule } from './stripe/stripe.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | AuthModule, 13 | UsersModule, 14 | StripeModule, 15 | ConfigModule.forRoot({ 16 | isGlobal: true, 17 | envFilePath: '.env', 18 | load: [configuration], 19 | }), 20 | ], 21 | controllers: [AppController], 22 | providers: [AppService], 23 | }) 24 | export class AppModule {} 25 | -------------------------------------------------------------------------------- /api/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService {} 5 | -------------------------------------------------------------------------------- /api/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { JwtStrategy } from './jwt.strategy'; 4 | 5 | @Module({ 6 | imports: [PassportModule.register({ defaultStrategy: 'jwt' })], 7 | providers: [JwtStrategy], 8 | exports: [PassportModule], 9 | }) 10 | export class AuthModule {} 11 | -------------------------------------------------------------------------------- /api/src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | import { passportJwtSecret } from 'jwks-rsa'; 5 | import { ConfigService } from '@nestjs/config'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor(private readonly configService: ConfigService) { 10 | super({ 11 | secretOrKeyProvider: passportJwtSecret({ 12 | cache: true, 13 | rateLimit: true, 14 | jwksRequestsPerMinute: 5, 15 | jwksUri: `${configService.get( 16 | 'AUTH0_ISSUER_URL', 17 | )}.well-known/jwks.json`, 18 | }), 19 | 20 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 21 | audience: configService.get('AUTH0_AUDIENCE'), 22 | issuer: `${configService.get('AUTH0_ISSUER_URL')}`, 23 | algorithms: ['RS256'], 24 | }); 25 | } 26 | 27 | validate(payload: unknown): unknown { 28 | return payload; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/config/configuration.ts: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | DATABASE_URL: process.env.DATABASE_URL, 3 | SHADOW_DATABASE_URL: process.env.SHADOW_DATABASE_URL, 4 | PORT: parseInt(process.env.PORT, 10) || 8080, 5 | AUTH0_DOMAIN: process.env.AUTH0_DOMAIN, 6 | AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE, 7 | AUTH0_ISSUER_URL: process.env.AUTH0_ISSUER_URL, 8 | AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, 9 | AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, 10 | STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, 11 | }); 12 | -------------------------------------------------------------------------------- /api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | const configService = app.get(ConfigService); 8 | app.enableCors(); 9 | await app.listen(configService.get('PORT')); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /api/src/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INestApplication, 3 | Injectable, 4 | OnModuleInit, 5 | OnModuleDestroy, 6 | } from '@nestjs/common'; 7 | import { PrismaClient } from '@prisma/client'; 8 | 9 | @Injectable() 10 | export class PrismaService extends PrismaClient implements OnModuleInit { 11 | async onModuleInit() { 12 | await this.$connect(); 13 | } 14 | 15 | async enableShutdownHooks(app: INestApplication) { 16 | this.$on('beforeExit', async () => { 17 | await app.close(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api/src/stripe/stripe.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { StripeService } from './stripe.service'; 3 | 4 | @Module({ 5 | imports: [], 6 | controllers: [], 7 | providers: [StripeService], 8 | exports: [], 9 | }) 10 | export class StripeModule {} 11 | -------------------------------------------------------------------------------- /api/src/stripe/stripe.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import Stripe from 'stripe'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | @Injectable() 6 | export class StripeService { 7 | private stripe: Stripe; 8 | constructor(private readonly configService: ConfigService) { 9 | this.stripe = new Stripe(configService.get('STRIPE_SECRET_KEY'), { 10 | apiVersion: '2020-08-27', 11 | }); 12 | } 13 | 14 | // Customer 15 | async createCustomer(email: string): Promise { 16 | const customer = await this.stripe.customers.create({ 17 | email: email, 18 | }); 19 | return customer?.id; 20 | } 21 | 22 | async updateCustomer( 23 | paymentMethodId: string, 24 | stripeCustomerId: string, 25 | ): Promise { 26 | // デフォルトの決済方法を更新する 27 | const customer = await this.stripe.customers.update(stripeCustomerId, { 28 | invoice_settings: { 29 | default_payment_method: paymentMethodId, 30 | }, 31 | }); 32 | return customer; 33 | } 34 | 35 | // PaymentMethod 36 | async attachPaymentMethod( 37 | paymentMethodId: string, 38 | stripeCustomerId: string, 39 | ): Promise { 40 | const paymentMethod = await this.stripe.paymentMethods.attach( 41 | paymentMethodId, 42 | { customer: stripeCustomerId }, 43 | ); 44 | return paymentMethod; 45 | } 46 | 47 | // Plan 48 | // https://stripe.com/docs/api/plans/retrieve 49 | async retrievePlan(priceId: string): Promise { 50 | const plan = await this.stripe.plans.retrieve(priceId); 51 | return plan; 52 | } 53 | 54 | // Subscription 55 | // https://stripe.com/docs/api/subscriptions/create 56 | async createSubscription( 57 | priceId: string, 58 | stripeCustomerId: string, 59 | ): Promise { 60 | const subscription = await this.stripe.subscriptions.create({ 61 | customer: stripeCustomerId, 62 | items: [{ price: priceId }], 63 | }); 64 | return subscription; 65 | } 66 | 67 | // https://stripe.com/docs/api/subscriptions/update 68 | async updateSubscription( 69 | stripeSubscriptionId: string, 70 | params: any, 71 | ): Promise { 72 | const subscription = await this.stripe.subscriptions.update( 73 | stripeSubscriptionId, 74 | params, 75 | ); 76 | return 'success'; 77 | } 78 | 79 | // https://stripe.com/docs/api/subscriptions/cancel 80 | async cancelSubscription(subscriptionId: string): Promise { 81 | const deleted = await this.stripe.subscriptions.del(subscriptionId); 82 | return deleted; 83 | } 84 | 85 | // https://stripe.com/docs/api/subscriptions/retrieve 86 | async retrieveSubscription(subscriptionId: string): Promise { 87 | const subscription = await this.stripe.subscriptions.retrieve( 88 | subscriptionId, 89 | ); 90 | return subscription; 91 | } 92 | 93 | // TODO: invoiceで請求書を発行する際に、TAXを含める 94 | } 95 | -------------------------------------------------------------------------------- /api/src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from '../prisma.service'; 3 | import { User, Prisma } from '@prisma/client'; 4 | import { ConnectableObservable } from 'rxjs'; 5 | 6 | @Injectable() 7 | export class UserService { 8 | constructor(private prisma: PrismaService) {} 9 | 10 | async user( 11 | userWhereUniqueInput: Prisma.UserWhereUniqueInput, 12 | ): Promise { 13 | return this.prisma.user.findUnique({ 14 | where: userWhereUniqueInput, 15 | }); 16 | } 17 | 18 | async users(params: { 19 | skip?: number; 20 | take?: number; 21 | cursor?: Prisma.UserWhereUniqueInput; 22 | where?: Prisma.UserWhereInput; 23 | orderBy?: Prisma.UserOrderByInput; 24 | }): Promise { 25 | const { skip, take, cursor, where, orderBy } = params; 26 | return this.prisma.user.findMany({ 27 | skip, 28 | take, 29 | cursor, 30 | where, 31 | orderBy, 32 | }); 33 | } 34 | 35 | async createUser(data: Prisma.UserCreateInput): Promise { 36 | return this.prisma.user.create({ 37 | data, 38 | }); 39 | } 40 | 41 | async updateUser(params: { 42 | where: Prisma.UserWhereUniqueInput; 43 | data: Prisma.UserUpdateInput; 44 | }): Promise { 45 | const { where, data } = params; 46 | return this.prisma.user.update({ 47 | data, 48 | where, 49 | }); 50 | } 51 | 52 | async deleteUser(where: Prisma.UserWhereUniqueInput): Promise { 53 | return this.prisma.user.delete({ 54 | where, 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /api/src/user/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Put, 5 | Body, 6 | UseGuards, 7 | Request, 8 | Post, 9 | } from '@nestjs/common'; 10 | import { UserService } from './user.service'; 11 | import { User as UserModel } from '@prisma/client'; 12 | import { AuthGuard } from '@nestjs/passport'; 13 | import { StripeService } from 'src/stripe/stripe.service'; 14 | 15 | @Controller('users') 16 | export class UsersController { 17 | constructor( 18 | private readonly userService: UserService, 19 | private readonly stripeService: StripeService, 20 | ) {} 21 | 22 | @Get('public') 23 | async public() { 24 | return { message: '✅ Public API Test!' }; 25 | } 26 | 27 | @UseGuards(AuthGuard('jwt')) 28 | @Get('private') 29 | async private(@Request() req) { 30 | return { message: '✅ Private API Test!', payload: req.user }; 31 | } 32 | 33 | @UseGuards(AuthGuard('jwt')) 34 | @Post() 35 | async getUser(@Request() req): Promise { 36 | const email = req.user['https://example.com/email']; 37 | const auth0Sub = req.user.sub; // Auth0のユーザーID 38 | let user = await this.userService.user({ auth0Sub: auth0Sub }); 39 | 40 | // Userが存在しない場合は作成 41 | if (!user) { 42 | // stripeCustomerIdの作成 43 | const stripeCustomerId = await this.stripeService.createCustomer(email); 44 | const user = await this.userService.createUser({ 45 | email: req.user['https://example.com/email'], 46 | auth0Sub: req.user.sub, 47 | stripeCustomerId: stripeCustomerId, 48 | }); 49 | return user; 50 | } else { 51 | // stripeCustomerIdのが無い場合、作成 52 | if (!user.stripeCustomerId) { 53 | const stripeCustomerId = await this.stripeService.createCustomer(email); 54 | user = await this.userService.updateUser({ 55 | where: { id: Number(user.id) }, 56 | data: { stripeCustomerId: stripeCustomerId }, 57 | }); 58 | } 59 | return user; 60 | } 61 | } 62 | 63 | @UseGuards(AuthGuard('jwt')) 64 | @Put() 65 | async updateUser( 66 | @Request() req: any, 67 | @Body() updateData: { email: string }, 68 | ): Promise { 69 | const auth0Sub = req.user.sub; // Auth0のユーザーID 70 | return this.userService.updateUser({ 71 | where: { auth0Sub: auth0Sub }, 72 | data: updateData, 73 | }); 74 | } 75 | 76 | @UseGuards(AuthGuard('jwt')) 77 | @Post('delete') 78 | async deleteUser(@Request() req): Promise { 79 | const auth0Sub = req.user.sub; // Auth0のユーザーID 80 | return this.userService.deleteUser({ auth0Sub: auth0Sub }); 81 | } 82 | 83 | @UseGuards(AuthGuard('jwt')) 84 | @Post('attach-payment-method') 85 | async attachPaymentMethod( 86 | @Request() req, 87 | @Body() postData: { paymentMethod: any }, 88 | ): Promise { 89 | const email = req.user['https://example.com/email']; 90 | let user = await this.userService.user({ email: email }); 91 | 92 | const stripePaymentMethod = await this.stripeService.attachPaymentMethod( 93 | postData.paymentMethod.id, 94 | user.stripeCustomerId, 95 | ); 96 | 97 | // デフォルトの決済方法を更新する 98 | const customer = await this.stripeService.updateCustomer( 99 | stripePaymentMethod.id, 100 | user.stripeCustomerId, 101 | ); 102 | 103 | user = await this.userService.updateUser({ 104 | where: { email: user.email }, 105 | data: { 106 | stripeCardBrand: stripePaymentMethod.card.brand, 107 | stripeCardLastFour: stripePaymentMethod.card.last4, 108 | }, 109 | }); 110 | 111 | return user; 112 | } 113 | 114 | @UseGuards(AuthGuard('jwt')) 115 | @Post('change-plan') 116 | async changePlan( 117 | @Request() req, 118 | @Body() postData: { priceId: string }, 119 | ): Promise { 120 | const auth0Sub = req.user.sub; 121 | const plan = await this.stripeService.retrievePlan(postData.priceId); 122 | if (!plan) return; 123 | 124 | let user = await this.userService.user({ auth0Sub: auth0Sub }); 125 | let subscription; 126 | if (user.stripeSubscriptionId) { 127 | subscription = await this.stripeService.retrieveSubscription( 128 | user.stripeSubscriptionId, 129 | ); 130 | 131 | if (postData.priceId != subscription.plan.id) { 132 | const params = { 133 | items: [ 134 | { 135 | id: subscription.items.data[0].id, 136 | plan: postData.priceId, 137 | }, 138 | ], 139 | }; 140 | subscription = await this.stripeService.updateSubscription( 141 | user.stripeSubscriptionId, 142 | params, 143 | ); 144 | } else { 145 | return '同じプランです'; 146 | } 147 | } else { 148 | // TODO: エラーハンドリング デフォルトの決済方法がない場合 149 | subscription = await this.stripeService.createSubscription( 150 | postData.priceId, 151 | user.stripeCustomerId, 152 | ); 153 | } 154 | 155 | // subscriptionIDを保存する 156 | user = await this.userService.updateUser({ 157 | where: { auth0Sub: auth0Sub }, 158 | data: { 159 | stripeSubscriptionId: subscription.id, 160 | }, 161 | }); 162 | 163 | return user; 164 | } 165 | 166 | @UseGuards(AuthGuard('jwt')) 167 | @Get('cancel-plan') 168 | async cancelPlan(@Request() req): Promise { 169 | const auth0Sub = req.user.sub; 170 | let user = await this.userService.user({ auth0Sub: auth0Sub }); 171 | const subscription = await this.stripeService.cancelSubscription( 172 | user.stripeSubscriptionId, 173 | ); 174 | 175 | // subscriptionIDを削除する 176 | user = await this.userService.updateUser({ 177 | where: { auth0Sub: auth0Sub }, 178 | data: { 179 | stripeSubscriptionId: '', 180 | }, 181 | }); 182 | return 'success'; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /api/src/user/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from 'src/prisma.service'; 3 | import { UsersController } from './users.controller'; 4 | import { UserService } from './user.service'; 5 | import { StripeService } from 'src/stripe/stripe.service'; 6 | 7 | @Module({ 8 | imports: [], 9 | controllers: [UsersController], 10 | providers: [PrismaService, UserService, StripeService], 11 | exports: [UserService], 12 | }) 13 | export class UsersModule {} 14 | -------------------------------------------------------------------------------- /api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strict": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | # Auth0 2 | NEXT_PUBLIC_AUTH0_DOMAIN="xxxxx.jp.auth0.com" 3 | NEXT_PUBLIC_AUTH0_CLIENT_ID="xxxxx" 4 | NEXT_PUBLIC_AUTH0_REDIRECT_URI="http://localhost:3000" 5 | NEXT_PUBLIC_AUTH0_AUDIENCE="http://localhost:8080" 6 | # Stripe 7 | NEXT_PUBLIC_STRIPE_PUBLIC_KEY="" 8 | NEXT_PUBLIC_STRIPE_STANDARD_PLAN_ID="" 9 | NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_ID="" -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "plugin:prettier/recommended"], 3 | rules: { 4 | "@next/next/no-img-element": "off", 5 | "jsx-a11y/alt-text": "off", 6 | "import/no-anonymous-default-export": "off", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /client/.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 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [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`. 18 | 19 | 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. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /client/components/atoms/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanigacy/nestjs-prisma-nextjs/aed2a54aa887f0c8586000f9c3480300a31ef916/client/components/atoms/.gitkeep -------------------------------------------------------------------------------- /client/components/molecules/check-form.js: -------------------------------------------------------------------------------- 1 | import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; 2 | import axios from 'axios'; 3 | import { useAuth0 } from '@auth0/auth0-react'; 4 | 5 | export default function CheckoutForm() { 6 | const stripe = useStripe(); 7 | const elements = useElements(); 8 | const { user, getAccessTokenSilently } = useAuth0(); 9 | 10 | const attachPaymentMethod = async (paymentMethod) => { 11 | try { 12 | const accessToken = await getAccessTokenSilently({ 13 | audience: process.env.NEXT_PUBLIC_AUTH0_AUDIENCE, 14 | scope: 'read:current_user', 15 | }); 16 | 17 | await axios.post( 18 | 'http://localhost:8080/users/attach-payment-method', 19 | { 20 | paymentMethod: paymentMethod, 21 | }, 22 | { 23 | headers: { 24 | Authorization: `Bearer ${accessToken}`, 25 | }, 26 | } 27 | ); 28 | } catch (e) { 29 | console.log(e.message); 30 | } 31 | }; 32 | 33 | const handleSubmit = async (event) => { 34 | event.preventDefault(); 35 | 36 | if (!stripe || !elements) { 37 | return; 38 | } 39 | 40 | const cardElement = elements.getElement(CardElement); 41 | 42 | const { error, paymentMethod } = await stripe.createPaymentMethod({ 43 | type: 'card', 44 | card: cardElement, 45 | }); 46 | 47 | if (error) { 48 | console.log('[error]', error); 49 | } else { 50 | console.log('✅ PaymentMethod', paymentMethod); 51 | await attachPaymentMethod(paymentMethod); 52 | } 53 | }; 54 | 55 | return ( 56 |
57 | 78 | 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /client/components/molecules/user-menu.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { Menu, Transition } from '@headlessui/react'; 3 | import { classNames } from '@/libs/style-utils'; 4 | 5 | export default function UserMenu() { 6 | return ( 7 | 8 | {({ open }) => ( 9 | <> 10 |
11 | 12 | User options 13 | {/* {session?.user?.name */} 18 | 19 |
20 | 21 | 31 | 35 |
36 | 37 | {/* {({ active }) => ( 38 |
45 | ログアウト 46 |
47 | )} */} 48 |
49 |
50 |
51 |
52 | 53 | )} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /client/components/organisms/footer.tsx: -------------------------------------------------------------------------------- 1 | import { APP_NAME } from '@/libs/constants'; 2 | 3 | export default function Footer() { 4 | return ( 5 |
6 |
7 |

8 | © {`2021 ${APP_NAME}. All rights reserved.`} 9 |

10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /client/components/organisms/header.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import Link from 'next/link'; 3 | import { Popover, Transition } from '@headlessui/react'; 4 | import { MenuIcon, XIcon } from '@heroicons/react/outline'; 5 | import UserMenu from '@/components/molecules/user-menu'; 6 | 7 | export default function Header() { 8 | return ( 9 | 10 | {({ open }) => ( 11 | <> 12 |
13 |
14 | 15 | 16 | Workflow 17 | 18 | 19 | 20 |
21 |
22 | 23 | Open menu 24 | 26 |
27 |
28 |
29 | NestJS Prisma NextJS 30 |
31 |
32 | {/*
33 | {session ? ( 34 | 35 | ) : ( 36 | 37 | 38 | はじめる 39 | 40 | 41 | )} 42 |
*/} 43 |
44 |
45 | 46 | 56 | 61 |
62 |
63 |
64 |
65 | Workflow 70 |
71 |
72 | 73 | Close menu 74 | 76 |
77 |
78 |
79 |
80 |
81 |
82 |

83 | Existing customer?{' '} 84 | 88 | Sign in 89 | 90 |

91 |
92 |
93 |
94 |
95 |
96 | 97 | )} 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /client/components/organisms/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from '@/components/organisms/header'; 2 | import Footer from '@/components/organisms/footer'; 3 | 4 | export default function Layout({ children }: any) { 5 | return ( 6 | <> 7 |
8 |
9 |
10 |
{children}
11 |