├── .devcontainer ├── .gitignore ├── policy.json ├── Dockerfile.minio ├── .env ├── Dockerfile ├── docker-compose.yml └── devcontainer.json ├── .gitattributes ├── packages ├── docs │ ├── static │ │ ├── .nojekyll │ │ └── img │ │ │ ├── favicon.ico │ │ │ ├── docusaurus.png │ │ │ ├── docusaurus-social-card.jpg │ │ │ └── logo.svg │ ├── babel.config.js │ ├── docs │ │ ├── tutorial-extras │ │ │ ├── img │ │ │ │ ├── localeDropdown.png │ │ │ │ └── docsVersionDropdown.png │ │ │ ├── _category_.json │ │ │ ├── manage-docs-versions.md │ │ │ └── translate-your-site.md │ │ ├── tutorial-basics │ │ │ ├── _category_.json │ │ │ ├── deploy-your-site.md │ │ │ ├── create-a-blog-post.md │ │ │ ├── congratulations.md │ │ │ ├── create-a-page.md │ │ │ ├── create-a-document.md │ │ │ └── markdown-features.mdx │ │ └── intro.md │ ├── src │ │ ├── pages │ │ │ ├── markdown-page.md │ │ │ ├── index.module.css │ │ │ └── index.js │ │ ├── components │ │ │ └── HomepageFeatures │ │ │ │ ├── styles.module.css │ │ │ │ └── index.js │ │ └── css │ │ │ └── custom.css │ ├── .gitignore │ ├── sidebars.js │ ├── README.md │ ├── package.json │ └── docusaurus.config.js ├── backend │ ├── .eslintignore │ ├── .npmrc │ ├── src │ │ ├── lib │ │ │ ├── generated │ │ │ │ └── .gitignore │ │ │ ├── security │ │ │ │ ├── armor.ts │ │ │ │ ├── authn.ts │ │ │ │ └── authz.ts │ │ │ ├── scalars │ │ │ │ ├── BodyString.ts │ │ │ │ ├── TitleString.ts │ │ │ │ ├── BioString.ts │ │ │ │ ├── ScreenNameString.ts │ │ │ │ └── HandleString.ts │ │ │ ├── error │ │ │ │ ├── handling.ts │ │ │ │ └── error.ts │ │ │ ├── webhook │ │ │ │ └── webhook.ts │ │ │ └── plugins │ │ │ │ └── useAuthMock.ts │ │ ├── context.ts │ │ ├── schema.ts │ │ ├── resolvers │ │ │ ├── types │ │ │ │ ├── postType.ts │ │ │ │ └── userType.ts │ │ │ ├── queries │ │ │ │ └── panelQuery.ts │ │ │ └── mutations │ │ │ │ └── panelMutation.ts │ │ ├── env.ts │ │ ├── resolver.ts │ │ └── index.ts │ ├── prisma │ │ ├── migrations │ │ │ ├── 20230806124217_ │ │ │ │ └── migration.sql │ │ │ ├── 20230806124111_ │ │ │ │ └── migration.sql │ │ │ ├── migration_lock.toml │ │ │ ├── 20230711134019_20230711_dev │ │ │ │ └── migration.sql │ │ │ ├── 20230925101132_dev │ │ │ │ └── migration.sql │ │ │ ├── 20231028023451_ │ │ │ │ └── migration.sql │ │ │ ├── 20230711073849_20230711_dev │ │ │ │ └── migration.sql │ │ │ ├── 20230704065830_init │ │ │ │ └── migration.sql │ │ │ └── 20230806070641_ │ │ │ │ └── migration.sql │ │ ├── schema.prisma │ │ └── seed.ts │ ├── .dockerignore │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── .swcrc │ ├── .gitignore │ ├── codegen.ts │ └── package.json ├── frontend │ ├── src │ │ ├── lib │ │ │ ├── generated │ │ │ │ ├── .gitignore │ │ │ │ ├── index.ts │ │ │ │ └── fragment-masking.ts │ │ │ ├── provider │ │ │ │ ├── authn │ │ │ │ │ ├── useAuthn.ts │ │ │ │ │ └── AuthnProvider.tsx │ │ │ │ └── urql │ │ │ │ │ └── UrqlProvider.tsx │ │ │ └── route │ │ │ │ └── ProtectedRouter.tsx │ │ ├── vite-env.d.ts │ │ ├── index.css │ │ ├── 404.tsx │ │ ├── config.ts │ │ ├── features │ │ │ ├── index │ │ │ │ └── pages │ │ │ │ │ └── IndexPage.tsx │ │ │ ├── users │ │ │ │ ├── pages │ │ │ │ │ ├── UsersPage.tsx │ │ │ │ │ └── UserDetailPage.tsx │ │ │ │ └── components │ │ │ │ │ ├── UserDetailProfileImageInput.tsx │ │ │ │ │ ├── PostCardForUser.tsx │ │ │ │ │ ├── UserDetailBioInput.tsx │ │ │ │ │ ├── UserDetailScreenNameInput.tsx │ │ │ │ │ ├── UserDetailHandleInput.tsx │ │ │ │ │ ├── UserCard.tsx │ │ │ │ │ └── UserDetailCard.tsx │ │ │ └── posts │ │ │ │ ├── pages │ │ │ │ ├── PostsPage.tsx │ │ │ │ └── PostDetailPage.tsx │ │ │ │ └── components │ │ │ │ ├── UserCardForPost.tsx │ │ │ │ ├── PostDetailCard.tsx │ │ │ │ └── PostCard.tsx │ │ ├── callback.tsx │ │ ├── env.ts │ │ ├── main.tsx │ │ ├── route.tsx │ │ └── _document.tsx │ ├── .npmrc │ ├── public │ │ └── callstack.png │ ├── postcss.config.js │ ├── .dockerignore │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── index.html │ ├── .gitignore │ ├── .eslintrc.cjs │ ├── .storybook │ │ ├── main.ts │ │ └── preview.ts │ ├── tsconfig.json │ ├── codegen.ts │ ├── vite.config.ts.timestamp-1694775590419-7203cb0b93586.mjs │ ├── tailwind.config.js │ └── package.json ├── infra │ ├── config │ │ ├── minio │ │ │ └── policy.json │ │ └── nginx-prod │ │ │ └── default.conf.template │ └── Dockerfiles │ │ ├── Minio-Dockerfile │ │ ├── Backend-Dockerfile │ │ └── Frontend-Dockerfile └── graphql │ └── schemas │ └── schema.graphql ├── .npmrc ├── .prettierignore ├── pnpm-workspace.yaml ├── .prettierrc ├── .commitlintrc.json ├── .gitignore ├── callstack.png ├── .changeset ├── two-elephants-roll.md ├── kind-shirts-cross.md ├── wet-seahorses-kiss.md ├── clean-olives-attack.md ├── many-cobras-dress.md ├── metal-adults-care.md ├── angry-schools-smoke.md ├── small-pumpkins-divide.md ├── config.json └── README.md ├── env_files ├── .gitignore ├── postgres.dev.env ├── postgres.prod.env.example ├── logto.prod.env.sample ├── codespace.dev.env ├── backend.prod.env.example └── prod.env.example ├── .husky └── commit-msg ├── .huskyrc.json ├── graphql.config.ts ├── turbo.json ├── package.json ├── LICENSE ├── .github └── workflows │ ├── ci.yml │ └── gh-pages.yml ├── README.md └── docker-compose.yml /.devcontainer/.gitignore: -------------------------------------------------------------------------------- 1 | !.env -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /packages/docs/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | store-dir=node_modules/.pnpm-store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | pnpm-lock.yaml -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' -------------------------------------------------------------------------------- /packages/backend/.eslintignore: -------------------------------------------------------------------------------- 1 | src/resolvers/generated/* -------------------------------------------------------------------------------- /packages/backend/.npmrc: -------------------------------------------------------------------------------- 1 | store-dir=node_modules/.pnpm-store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 160 4 | } -------------------------------------------------------------------------------- /packages/backend/src/lib/generated/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /packages/frontend/src/lib/generated/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ./* 3 | !.gitignore -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } -------------------------------------------------------------------------------- /packages/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | *.key 3 | node_modules 4 | packages/*/secret/* 5 | node-jiti -------------------------------------------------------------------------------- /callstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calloc134/callstack/HEAD/callstack.png -------------------------------------------------------------------------------- /.changeset/two-elephants-roll.md: -------------------------------------------------------------------------------- 1 | --- 2 | "backend": patch 3 | --- 4 | 5 | api のパスを変更 6 | -------------------------------------------------------------------------------- /.changeset/kind-shirts-cross.md: -------------------------------------------------------------------------------- 1 | --- 2 | "frontend": patch 3 | --- 4 | 5 | index.css のコメントを修正 6 | -------------------------------------------------------------------------------- /.changeset/wet-seahorses-kiss.md: -------------------------------------------------------------------------------- 1 | --- 2 | "frontend": patch 3 | --- 4 | 5 | codegen の設定を追加 6 | -------------------------------------------------------------------------------- /env_files/.gitignore: -------------------------------------------------------------------------------- 1 | !codespace.dev.env 2 | !logto.dev.env 3 | !postgres.dev.env 4 | !minio.dev.env -------------------------------------------------------------------------------- /env_files/postgres.dev.env: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=db 2 | POSTGRES_USER=main 3 | POSTGRES_PASSWORD=password -------------------------------------------------------------------------------- /env_files/postgres.prod.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=db 2 | POSTGRES_USER=main 3 | POSTGRES_PASSWORD=password -------------------------------------------------------------------------------- /.changeset/clean-olives-attack.md: -------------------------------------------------------------------------------- 1 | --- 2 | "backend": patch 3 | --- 4 | 5 | resolver.ts の import 先を修正 6 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/frontend/.npmrc: -------------------------------------------------------------------------------- 1 | store-dir=node_modules/.pnpm-store 2 | # public-hoist-pattern[]=*@nextui-org/theme* -------------------------------------------------------------------------------- /packages/frontend/src/lib/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fragment-masking"; 2 | export * from "./gql"; -------------------------------------------------------------------------------- /env_files/logto.prod.env.sample: -------------------------------------------------------------------------------- 1 | DB_URL="postgresql://main:password@db-logto:5432/logto" 2 | TRUST_PROXY_HEADER=1 -------------------------------------------------------------------------------- /.changeset/many-cobras-dress.md: -------------------------------------------------------------------------------- 1 | --- 2 | "frontend": patch 3 | --- 4 | 5 | vite のポーリングを有効にし、docker からホットリロードを許可 6 | -------------------------------------------------------------------------------- /.changeset/metal-adults-care.md: -------------------------------------------------------------------------------- 1 | --- 2 | "frontend": major 3 | "backend": major 4 | --- 5 | 6 | docker compose に対応 7 | -------------------------------------------------------------------------------- /.changeset/angry-schools-smoke.md: -------------------------------------------------------------------------------- 1 | --- 2 | "frontend": major 3 | "backend": major 4 | --- 5 | 6 | CI のセットアップ並びにコードの修正 7 | -------------------------------------------------------------------------------- /packages/docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calloc134/callstack/HEAD/packages/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /packages/docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve("@docusaurus/core/lib/babel/preset")], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calloc134/callstack/HEAD/packages/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /packages/frontend/public/callstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calloc134/callstack/HEAD/packages/frontend/public/callstack.png -------------------------------------------------------------------------------- /.changeset/small-pumpkins-divide.md: -------------------------------------------------------------------------------- 1 | --- 2 | "frontend": patch 3 | "backend": patch 4 | --- 5 | 6 | graphql-codegen の設定と grapqhl スキーマを変更 7 | -------------------------------------------------------------------------------- /packages/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/backend/prisma/migrations/20230806124217_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Post" ALTER COLUMN "is_public" SET DEFAULT false; 3 | -------------------------------------------------------------------------------- /packages/backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .pnpm-store 3 | .git 4 | .gitignore 5 | .github 6 | .vscode 7 | .eslintignore 8 | .eslintrc.js 9 | node_modules -------------------------------------------------------------------------------- /packages/docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calloc134/callstack/HEAD/packages/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /packages/backend/prisma/migrations/20230806124111_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Post" ADD COLUMN "is_public" BOOLEAN NOT NULL DEFAULT true; 3 | -------------------------------------------------------------------------------- /packages/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .pnpm-store 3 | .git 4 | .gitignore 5 | .github 6 | .vscode 7 | .eslintignore 8 | .eslintrc.js 9 | node_modules -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-extras/img/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calloc134/callstack/HEAD/packages/docs/docs/tutorial-extras/img/localeDropdown.png -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-extras/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Tutorial - Extras", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "strict": true, 5 | "skipLibCheck": true, 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-extras/img/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calloc134/callstack/HEAD/packages/docs/docs/tutorial-extras/img/docsVersionDropdown.png -------------------------------------------------------------------------------- /packages/backend/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" -------------------------------------------------------------------------------- /packages/docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /packages/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300&family=Noto+Sans+JP:wght@300&display=swap"); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | -------------------------------------------------------------------------------- /env_files/codespace.dev.env: -------------------------------------------------------------------------------- 1 | # データベースURLの設定 2 | DATABASE_URL="postgresql://main:password@db:5432/db" 3 | # 内部ネットワークからのminioエンドポイント 4 | MINIO_INSIDE_ENDPOINT=minio 5 | # 外部ネットワークからのminioエンドポイント 6 | MINIO_OUTSIDE_ENDPOINT=http://localhost:9000 -------------------------------------------------------------------------------- /packages/backend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | }; -------------------------------------------------------------------------------- /packages/backend/prisma/migrations/20230711134019_20230711_dev/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'USER'; 6 | -------------------------------------------------------------------------------- /env_files/backend.prod.env.example: -------------------------------------------------------------------------------- 1 | # データベースURLの設定 2 | DATABASE_URL="postgresql://main:password@db-app:5432/db" 3 | # 内部ネットワークからのminioエンドポイント 4 | MINIO_INSIDE_ENDPOINT=minio 5 | # 外部ネットワークからのminioエンドポイント 6 | MINIO_OUTSIDE_ENDPOINT=http://localhost:9000 -------------------------------------------------------------------------------- /packages/docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-basics/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Tutorial - Basics", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "5 minutes to learn the most important Docusaurus concepts." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/backend/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "baseUrl": ".", 4 | "parser": { 5 | "syntax": "typescript" 6 | }, 7 | "target": "es2020" 8 | 9 | }, 10 | "module": { 11 | "type": "commonjs", 12 | "noInterop": true 13 | } 14 | } -------------------------------------------------------------------------------- /packages/frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.devcontainer/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": ["*"] 8 | }, 9 | "Action": ["s3:GetObject"], 10 | "Resource": ["arn:aws:s3:::development/*"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/infra/config/minio/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "AddPerm", 6 | "Effect": "Allow", 7 | "Principal": "*", 8 | "Action": ["s3:GetObject"], 9 | "Resource": ["arn:aws:s3:::development/*"] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/backend/prisma/migrations/20230925101132_dev/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `image_url` to the `User` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" ADD COLUMN "image_url" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile.minio: -------------------------------------------------------------------------------- 1 | FROM quay.io/minio/minio:latest 2 | 3 | # developmentバケットの作成 4 | RUN mkdir -p /data/development 5 | # RUN mkdir -p /data/.minio.sys/buckets/development 6 | # COPY ./policy.json /data/.minio.sys/buckets/development/policy.json 7 | 8 | ENTRYPOINT ["minio", "server", "/data", "--console-address", ":9001"] -------------------------------------------------------------------------------- /graphql.config.ts: -------------------------------------------------------------------------------- 1 | import type { IGraphQLConfig } from "graphql-config"; 2 | 3 | const config: IGraphQLConfig = { 4 | schema: "packages/graphql/schemas/*.graphql", 5 | documents: ["packages/frontend/src/**/*.tsx", "packages/frontend/src/**/**/*.tsx", "packages/frontend/src/**/**/**/*.tsx"], 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /packages/backend/prisma/migrations/20231028023451_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[handle]` on the table `User` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "User_handle_key" ON "User"("handle"); 9 | -------------------------------------------------------------------------------- /packages/frontend/src/404.tsx: -------------------------------------------------------------------------------- 1 | // 404ページ 2 | const NotFoundPage = () => ( 3 | <> 4 |
5 |

404

6 |

ページが見つかりませんでした。

7 |
8 | 9 | ); 10 | 11 | export { NotFoundPage }; 12 | -------------------------------------------------------------------------------- /packages/infra/Dockerfiles/Minio-Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/minio/minio:latest 2 | 3 | # productionバケットの作成 4 | RUN mkdir -p /data/production 5 | # RUN mkdir -p /data/.minio.sys/buckets/production 6 | # COPY ./policy.json /data/.minio.sys/buckets/production/policy.json 7 | 8 | ENTRYPOINT ["minio", "server", "/data", "--console-address", ":9001"] -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**"] 7 | }, 8 | "codegen": {}, 9 | "typecheck": {}, 10 | "lint": {}, 11 | "format": {}, 12 | "prigen": {}, 13 | "primig": {}, 14 | "priseed": {} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import tsConfigPaths from "vite-tsconfig-paths"; 3 | import react from "@vitejs/plugin-react-swc"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | server: { 8 | host: true, 9 | port: 5173, 10 | }, 11 | plugins: [react(), tsConfigPaths()], 12 | }); 13 | -------------------------------------------------------------------------------- /packages/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | callstack 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.devcontainer/.env: -------------------------------------------------------------------------------- 1 | # 各環境変数ファイルのパスを指定 2 | POSTGRES_ENV=../env_files/postgres.dev.env 3 | CODESPACE_ENV=../env_files/codespace.dev.env 4 | 5 | # コンテナから見たスキーマのパスを指定 6 | SCHEMA_PATH=../graphql/schemas/*.graphql 7 | # OPERATION_PATH=../graphql/operations/*.graphql 8 | 9 | # minioの環境変数の設定 10 | MINIO_ROOT_USER=development 11 | MINIO_ROOT_PASSWORD=development 12 | # minioのバケット名を設定 13 | MINIO_BUCKET_NAME=development -------------------------------------------------------------------------------- /packages/frontend/src/config.ts: -------------------------------------------------------------------------------- 1 | import { LogtoConfig } from "@logto/react"; 2 | import { logto_endpoint, logto_app_id, logto_api_resource } from "./env"; 3 | 4 | // Logtoクライアントの設定 5 | // 環境変数より取得 6 | const logto_config: LogtoConfig = { 7 | // Logtoのエンドポイント 8 | endpoint: logto_endpoint, 9 | // Logtoのサインインに使用するアプリケーションID 10 | appId: logto_app_id, 11 | // Logtoで認可してもらう対象のAPIリソース 12 | resources: [logto_api_resource], 13 | }; 14 | 15 | export { logto_config }; 16 | -------------------------------------------------------------------------------- /packages/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /packages/frontend/src/lib/provider/authn/useAuthn.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { AuthnContext } from "./AuthnProvider"; 3 | 4 | const useAuthn = () => { 5 | // コンテキストを取得 6 | const jwt_context = useContext(AuthnContext); 7 | 8 | // コンテキストが取得できなかった場合はエラーを投げる 9 | if (!jwt_context) { 10 | throw new Error("useTheme must be used within a ThemeProvider"); 11 | } 12 | 13 | // コンテキストを返す 14 | return jwt_context; 15 | }; 16 | 17 | export { useAuthn }; 18 | -------------------------------------------------------------------------------- /packages/docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/frontend/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 5 | addons: [ 6 | "@storybook/addon-links", 7 | "@storybook/addon-essentials", 8 | "@storybook/addon-interactions", 9 | "@storybook/addon-styling", 10 | ], 11 | framework: { 12 | name: "@storybook/react-vite", 13 | options: {}, 14 | }, 15 | docs: { 16 | autodocs: "tag", 17 | }, 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /packages/backend/.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 -------------------------------------------------------------------------------- /packages/backend/prisma/migrations/20230711073849_20230711_dev/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[sub_auth]` on the table `User` will be added. If there are existing duplicate values, this will fail. 5 | - Added the required column `sub_auth` 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 "sub_auth" TEXT NOT NULL; 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "User_sub_auth_key" ON "User"("sub_auth"); 13 | -------------------------------------------------------------------------------- /packages/backend/src/context.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { Client } from "minio"; 3 | import { UserPayload } from "@envelop/auth0"; 4 | import { IncomingMessage } from "http"; 5 | import { User } from "@prisma/client"; 6 | 7 | // コンテキストの型定義 8 | export type GraphQLContext = { 9 | // Prismaクライアントの型定義 10 | prisma: PrismaClient; 11 | // minioクライアントの型定義 12 | minio: Client; 13 | // JWTのペイロードの型定義 14 | logto?: UserPayload; 15 | // リクエストの型定義 16 | req: IncomingMessage; 17 | // 現在ログインしているユーザーのUUID 18 | currentUser: User; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/backend/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { createSchema } from "graphql-yoga"; 2 | import { loadFilesSync } from "@graphql-tools/load-files"; 3 | import { resolvers } from "./resolver"; 4 | import { typeDefs as scalarTypeDefs } from "graphql-scalars"; 5 | import { schema_path } from "./env"; 6 | 7 | // graphql-yogaのcreateSchema関数を利用してスキーマを作成 8 | export const schema = createSchema({ 9 | // 型定義 10 | typeDefs: [ 11 | // スカラー型の定義をマージ 12 | ...scalarTypeDefs, 13 | // ファイルからスキーマを読み込み 14 | loadFilesSync(schema_path), 15 | ], 16 | // リゾルバー 17 | resolvers, 18 | }); 19 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye 2 | 3 | # [Optional] Uncomment this section to install additional OS packages. 4 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 5 | # && apt-get -y install --no-install-recommends 6 | 7 | # [Optional] Uncomment if you want to install an additional version of node using nvm 8 | # ARG EXTRA_NODE_VERSION=10 9 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 10 | 11 | # [Optional] Uncomment if you want to install more global node modules 12 | # RUN su node -c "npm install -g " 13 | -------------------------------------------------------------------------------- /packages/infra/config/nginx-prod/default.conf.template: -------------------------------------------------------------------------------- 1 | # メインサーバの設定 2 | server { 3 | 4 | # 6000ポートでlistenする 5 | listen 6000; 6 | # ホスト名 7 | # 環境変数で指定した値を使用する 8 | server_name ${HOSTNAME}; 9 | 10 | access_log /var/log/nginx/access.log; 11 | error_log /var/log/nginx/error.log; 12 | 13 | # ルートディレクトリ 14 | # ここではフロントエンドのビルドファイルを配置する 15 | location / { 16 | root /usr/share/nginx/dist; 17 | index index.html index.htm; 18 | try_files $uri $uri/ /index.html; 19 | } 20 | error_page 404 /404.html; 21 | location = /40x.html { 22 | } 23 | error_page 500 502 503 504 /50x.html; 24 | location = /50x.html { 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /packages/backend/src/lib/security/armor.ts: -------------------------------------------------------------------------------- 1 | import { EnvelopArmor } from "@escape.tech/graphql-armor"; 2 | 3 | // graphql-armorのセットアップ 4 | export const Armor = new EnvelopArmor({ 5 | // 最大深度を設定 6 | maxDepth: { 7 | enabled: true, 8 | n: 15, 9 | }, 10 | // 最大トークン数を設定 11 | maxTokens: { 12 | enabled: true, 13 | n: 1000, 14 | }, 15 | // 最大ディレクティブ数を設定 16 | maxDirectives: { 17 | enabled: true, 18 | n: 50, 19 | }, 20 | // 最大エイリアス数を設定 21 | maxAliases: { 22 | enabled: true, 23 | n: 10, 24 | }, 25 | // 最大コスト数を設定 26 | costLimit: { 27 | maxCost: 5000, 28 | objectCost: 2, 29 | scalarCost: 1, 30 | depthCostFactor: 1.5, 31 | ignoreIntrospection: true, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/frontend/src/features/index/pages/IndexPage.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Image, Spacer } from "@nextui-org/react"; 2 | import { Link } from "@tanstack/react-router"; 3 | 4 | export const RootIndexPage = () => { 5 | return ( 6 | <> 7 |
8 | 9 |

callstack

10 |

callstackボイラープレートのサンプルです。

11 | 12 | 15 |
16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": "." 23 | }, 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /packages/frontend/src/callback.tsx: -------------------------------------------------------------------------------- 1 | import { useHandleSignInCallback } from "@logto/react"; 2 | import { useNavigate } from "@tanstack/react-router"; 3 | import { Spinner } from "@nextui-org/react"; 4 | 5 | const CallBackPage = () => { 6 | // ログイン後にリダイレクトする関数をフックより取得 7 | const navigate = useNavigate({ 8 | from: "/auth/callback", 9 | }); 10 | 11 | // ログイン後にリダイレクト 12 | const { isLoading } = useHandleSignInCallback(() => { 13 | navigate({ 14 | to: "/auth/posts", 15 | }); 16 | }); 17 | 18 | // ログインしている状態であれば 19 | if (isLoading) { 20 | return ( 21 |
22 | 23 |
24 | ); 25 | } 26 | }; 27 | 28 | export { CallBackPage }; 29 | -------------------------------------------------------------------------------- /packages/frontend/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | import { withThemeByClassName } from '@storybook/addon-styling'; 3 | import '../src/index.css'; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | actions: { argTypesRegex: '^on[A-Z].*' }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | }, 15 | 16 | decorators: [ 17 | // Adds theme switching support. 18 | // NOTE: requires setting "darkMode" to "class" in your tailwind config 19 | withThemeByClassName({ 20 | themes: { 21 | light: 'light', 22 | dark: 'dark', 23 | }, 24 | defaultTheme: 'light', 25 | }), 26 | ], 27 | }; 28 | 29 | export default preview; 30 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-basics/deploy-your-site.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Deploy your site 6 | 7 | Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). 8 | 9 | It builds your site as simple **static HTML, JavaScript and CSS files**. 10 | 11 | ## Build your site 12 | 13 | Build your site **for production**: 14 | 15 | ```bash 16 | npm run build 17 | ``` 18 | 19 | The static files are generated in the `build` folder. 20 | 21 | ## Deploy your site 22 | 23 | Test your production build locally: 24 | 25 | ```bash 26 | npm run serve 27 | ``` 28 | 29 | The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). 30 | 31 | You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). 32 | -------------------------------------------------------------------------------- /packages/backend/prisma/migrations/20230704065830_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "uuid" TEXT NOT NULL, 4 | "email" TEXT NOT NULL, 5 | 6 | CONSTRAINT "User_pkey" PRIMARY KEY ("uuid") 7 | ); 8 | 9 | -- CreateTable 10 | CREATE TABLE "Profile" ( 11 | "uuid" TEXT NOT NULL, 12 | "age" INTEGER NOT NULL, 13 | "name" TEXT NOT NULL, 14 | "userId" TEXT NOT NULL, 15 | 16 | CONSTRAINT "Profile_pkey" PRIMARY KEY ("uuid") 17 | ); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 21 | 22 | -- CreateIndex 23 | CREATE UNIQUE INDEX "Profile_uuid_key" ON "Profile"("uuid"); 24 | 25 | -- CreateIndex 26 | CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId"); 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("uuid") ON DELETE RESTRICT ON UPDATE CASCADE; 30 | -------------------------------------------------------------------------------- /packages/docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{ type: "autogenerated", dirName: "." }], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /packages/frontend/src/env.ts: -------------------------------------------------------------------------------- 1 | // 環境変数を取得し、開発環境かどうかを判定 2 | const is_dev = import.meta.env.MODE === "development"; 3 | 4 | // 開発環境であれば、環境変数からJWTを取得 5 | // 本番環境であれば、空文字を設定 6 | const dev_jwt_token = is_dev ? import.meta.env.VITE_JWT_TOKEN || "" : ""; 7 | 8 | // 開発環境であれば、空文字を設定 9 | // 本番環境であれば、LogtoエンドポイントのURLを設定 10 | const logto_endpoint = is_dev ? "" : import.meta.env.VITE_LOGTO_ENDPOINT || ""; 11 | 12 | // 開発環境であれば、空文字を設定 13 | // 本番環境であれば、LogtoのアプリケーションIDを設定 14 | const logto_app_id = is_dev ? "" : import.meta.env.VITE_LOGTO_APPID || ""; 15 | 16 | // 開発環境であれば、空文字を設定 17 | // 本番環境であれば、Logtoのapiリソースを設定 18 | const logto_api_resource = is_dev ? "" : import.meta.env.VITE_LOGTO_API_RESOURCE || ""; 19 | 20 | // ホスト名を取得 21 | // 開発環境であれば、localhostを設定 22 | // 本番環境であれば、環境変数から取得 23 | const hostname = is_dev ? "localhost:6173" : import.meta.env.VITE_HOSTNAME || ""; 24 | 25 | export { is_dev, dev_jwt_token, logto_endpoint, logto_app_id, logto_api_resource, hostname }; 26 | -------------------------------------------------------------------------------- /packages/docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /packages/backend/codegen.ts: -------------------------------------------------------------------------------- 1 | import type { CodegenConfig } from "@graphql-codegen/cli"; 2 | 3 | const config: CodegenConfig = { 4 | overwrite: true, 5 | schema: process.env.SCHEMA_PATH, 6 | generates: { 7 | "src/lib/generated/resolver-types.ts": { 8 | plugins: ["typescript", "typescript-resolvers"], 9 | config: { 10 | contextType: "../../context#GraphQLContext", 11 | strictScalars: true, 12 | scalars: { 13 | UUID: "string", 14 | DateTime: "Date", 15 | NonNegativeInt: "number", 16 | NonEmptyString: "string", 17 | HandleString: "string", 18 | ScreenNameString: "string", 19 | BioString: "string", 20 | TitleString: "string", 21 | BodyString: "string", 22 | File: "File", 23 | }, 24 | enumsAsTypes: true, 25 | skipTypename: true, 26 | useTypeImports: true, 27 | }, 28 | }, 29 | }, 30 | }; 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /packages/frontend/src/lib/route/ProtectedRouter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Outlet } from "@tanstack/react-router"; 3 | import { useAuthn } from "src/lib/provider/authn/useAuthn"; 4 | import { hostname } from "src/env"; 5 | import { Spinner } from "@nextui-org/react"; 6 | import toast from "react-hot-toast"; 7 | 8 | export const ProtectedRouter = () => { 9 | // Logtoフックより認証状態とログイン関数を取得 10 | const { isAuthenticated, isLoading, signIn } = useAuthn(); 11 | 12 | // 認証していない場合はsignIn関数でログインリダイレクト 13 | useEffect(() => { 14 | if (!isAuthenticated && !isLoading) { 15 | toast("認証を行います...", { 16 | icon: "🔑", 17 | }); 18 | signIn(`https://${hostname}/auth/callback`); 19 | } 20 | }, [isAuthenticated, isLoading]); 21 | 22 | // 認証されている場合は子コンポーネントを表示 23 | if (!isAuthenticated) { 24 | return ( 25 |
26 | 27 |
28 | ); 29 | } 30 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-basics/create-a-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Create a Blog Post 6 | 7 | Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... 8 | 9 | ## Create your first Post 10 | 11 | Create a file at `blog/2021-02-28-greetings.md`: 12 | 13 | ```md title="blog/2021-02-28-greetings.md" 14 | --- 15 | slug: greetings 16 | title: Greetings! 17 | authors: 18 | - name: Joel Marcey 19 | title: Co-creator of Docusaurus 1 20 | url: https://github.com/JoelMarcey 21 | image_url: https://github.com/JoelMarcey.png 22 | - name: Sébastien Lorber 23 | title: Docusaurus maintainer 24 | url: https://sebastienlorber.com 25 | image_url: https://github.com/slorber.png 26 | tags: [greetings] 27 | --- 28 | 29 | Congratulations, you have made your first post! 30 | 31 | Feel free to play around and edit this post as much you like. 32 | ``` 33 | 34 | A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "callstack", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "build": "turbo build", 6 | "typecheck": "turbo typecheck", 7 | "format": "turbo format", 8 | "lint": "turbo lint", 9 | "prigen": "turbo prigen", 10 | "primig": "turbo primig", 11 | "priseed": "turbo priseed", 12 | "prepare": "husky install && turbo prigen && turbo codegen" 13 | }, 14 | "keywords": [], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+github.com:calloc134/callstack.git" 18 | }, 19 | "author": "calloc134 ", 20 | "license": "MIT", 21 | "packageManager": "pnpm@78.6.2", 22 | "engines": { 23 | "pnpm": ">=8.6.2" 24 | }, 25 | "dependencies": { 26 | "@changesets/cli": "^2.26.2", 27 | "@commitlint/cli": "^17.6.7", 28 | "@commitlint/config-conventional": "^17.6.7", 29 | "husky": "^8.0.3", 30 | "tsc": "^2.0.4", 31 | "tsx": "^3.12.7", 32 | "turbo": "^1.10.12" 33 | }, 34 | "devDependencies": { 35 | "graphql-config": "^5.0.2", 36 | "husky": "^8.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/types/postType.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { PostResolvers } from "src/lib/generated/resolver-types"; 3 | import { GraphQLContext } from "src/context"; 4 | import { withErrorHandling } from "src/lib/error/handling"; 5 | 6 | const PostTypeResolver: PostResolvers = { 7 | // ユーザーフィールドのリゾルバー 8 | // @ts-expect-error 返却されるpostにuserフィールドが存在しないためエラーが出るが、実際には存在するので無視 9 | user: async (parent, _args, context) => { 10 | const safe = withErrorHandling(async (post_uuid: string, prisma: PrismaClient) => { 11 | // UUIDから投稿を取得 12 | const result = await prisma.post 13 | .findUniqueOrThrow({ 14 | where: { 15 | post_uuid: post_uuid, 16 | }, 17 | }) 18 | // そこから投稿者を取得 19 | .user(); 20 | return result; 21 | }); 22 | 23 | // ペアレントオブジェクトから投稿のUUIDを取得 24 | const { post_uuid } = parent; 25 | // コンテキストからPrismaクライアントを取得 26 | const { prisma } = context; 27 | 28 | return await safe(post_uuid, prisma); 29 | }, 30 | }; 31 | 32 | export { PostTypeResolver }; 33 | -------------------------------------------------------------------------------- /packages/frontend/codegen.ts: -------------------------------------------------------------------------------- 1 | import type { CodegenConfig } from "@graphql-codegen/cli"; 2 | 3 | const config: CodegenConfig = { 4 | overwrite: true, 5 | schema: process.env.SCHEMA_PATH, 6 | documents: ["./src/features/**/components/**/*.tsx", "./src/features/**/pages/**/*.tsx"], 7 | ignoreNoDocuments: true, 8 | generates: { 9 | "src/lib/generated/": { 10 | preset: "client", 11 | config: { 12 | strictScalars: true, 13 | scalars: { 14 | UUID: "string", 15 | DateTime: "Date", 16 | NonNegativeInt: "number", 17 | NonEmptyString: "string", 18 | HandleString: "string", 19 | ScreenNameString: "string", 20 | BioString: "string", 21 | TitleString: "string", 22 | BodyString: "string", 23 | File: "File", 24 | }, 25 | enumsAsTypes: true, 26 | skipTypename: true, 27 | useTypeImports: true, 28 | // schema: "zod", 29 | // scalarSchemas: { 30 | // UUID: "z.string().uuid()", 31 | // }, 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | export default config; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 calloc134 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /env_files/prod.env.example: -------------------------------------------------------------------------------- 1 | # 各パッケージのディレクトリを指定 2 | BACKDIR=packages/backend 3 | FRONTDIR=packages/frontend 4 | INFRADIR=packages/infra 5 | 6 | # 各環境変数ファイルのパスを指定 7 | BACK_ENV=env_files/backend.prod.env 8 | POSTGRES_ENV=env_files/postgres.prod.env 9 | LOGTO_ENV=env_files/logto.prod.env 10 | 11 | # minioの環境変数の設定 12 | MINIO_ROOT_USER=production 13 | MINIO_ROOT_PASSWORD=production 14 | # minioのバケット名を設定 15 | MINIO_BUCKET_NAME=production 16 | 17 | # logtoエンドポイントの指定 18 | # この設定はLogtoで行うため、HTTPスキームを指定する 19 | LOGTO_ENDPOINT="https://auth.dummy" 20 | LOGTO_ADMIN_ENDPOINT="https://authadmin.dummy" 21 | # logtoのJWTのオーディエンス 22 | # 接続を認可したいAPIリソースと同じ値を指定 23 | LOGTO_AUDIENCE="https://dummy.dummy" 24 | # logtoのアプリケーションID 25 | # logtoで指定されるランダムな識別子 26 | LOGTO_APPID="dummyx5gi659pnnri1ihw" 27 | # logtoのAPIリソース 28 | # 接続を認可したいAPIリソースを指定 29 | LOGTO_API_RESOURCE="https://dummy.dummy" 30 | # logtoのWebHook用の署名検証用のシークレット 31 | # logtoで指定されるランダムな識別子 32 | LOGTO_WEBHOOK_SECRET="secretco4i4xbvpuhwcuro" 33 | 34 | # コンテナから見たスキーマのパスを指定 35 | SCHEMA_PATH=/home/graphql/schemas/*.graphql 36 | # OPERATION_PATH=/home/graphql/operations/*.graphql 37 | 38 | # ホストネームを指定 39 | HOSTNAME="dummy" 40 | # メールを指定 41 | CERTBOT_EMAIL="dummy@dummy" -------------------------------------------------------------------------------- /packages/docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme="dark"] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-basics/congratulations.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Congratulations! 6 | 7 | You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. 8 | 9 | Docusaurus has **much more to offer**! 10 | 11 | Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. 12 | 13 | Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) 14 | 15 | ## What's next? 16 | 17 | - Read the [official documentation](https://docusaurus.io/) 18 | - Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) 19 | - Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) 20 | - Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) 21 | - Add a [search bar](https://docusaurus.io/docs/search) 22 | - Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) 23 | - Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) 24 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "docbuild": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "2.4.1", 18 | "@docusaurus/preset-classic": "2.4.1", 19 | "@mdx-js/react": "^1.6.22", 20 | "clsx": "^1.2.1", 21 | "prism-react-renderer": "^1.3.5", 22 | "react": "^17.0.2", 23 | "react-dom": "^17.0.2" 24 | }, 25 | "devDependencies": { 26 | "@docusaurus/module-type-aliases": "2.4.1" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.5%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "engines": { 41 | "node": ">=16.14" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-basics/create-a-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Create a Page 6 | 7 | Add **Markdown or React** files to `src/pages` to create a **standalone page**: 8 | 9 | - `src/pages/index.js` → `localhost:3000/` 10 | - `src/pages/foo.md` → `localhost:3000/foo` 11 | - `src/pages/foo/bar.js` → `localhost:3000/foo/bar` 12 | 13 | ## Create your first React Page 14 | 15 | Create a file at `src/pages/my-react-page.js`: 16 | 17 | ```jsx title="src/pages/my-react-page.js" 18 | import React from "react"; 19 | import Layout from "@theme/Layout"; 20 | 21 | export default function MyReactPage() { 22 | return ( 23 | 24 |

My React page

25 |

This is a React page

26 |
27 | ); 28 | } 29 | ``` 30 | 31 | A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). 32 | 33 | ## Create your first Markdown Page 34 | 35 | Create a file at `src/pages/my-markdown-page.md`: 36 | 37 | ```mdx title="src/pages/my-markdown-page.md" 38 | # My Markdown page 39 | 40 | This is a Markdown page 41 | ``` 42 | 43 | A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). 44 | -------------------------------------------------------------------------------- /packages/backend/src/lib/scalars/BodyString.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, GraphQLScalarType, Kind } from "graphql"; 2 | import { createGraphQLError } from "graphql-yoga"; 3 | 4 | const validate = (value: any, ast?: ASTNode) => { 5 | if (typeof value !== "string") { 6 | throw createGraphQLError(`Value is not a string: ${value}`, ast ? { nodes: ast } : undefined); 7 | } 8 | 9 | if (value.length > 1000) { 10 | throw createGraphQLError(`Value cannot be longer than 1000 characters: ${value}`, ast ? { nodes: ast } : undefined); 11 | } 12 | 13 | return value; 14 | }; 15 | 16 | export const GraphQLBodyString = /*#__PURE__*/ new GraphQLScalarType({ 17 | name: "BodyString", 18 | 19 | description: "A string that is used for the body of a post", 20 | 21 | serialize: validate, 22 | 23 | parseValue: validate, 24 | 25 | parseLiteral(ast) { 26 | if (ast.kind !== Kind.STRING) { 27 | throw createGraphQLError(`Can only validate strings but got a: ${ast.kind}`, { nodes: ast }); 28 | } 29 | return validate(ast.value, ast); 30 | }, 31 | extensions: { 32 | codegenScalarType: "string", 33 | jsonSchema: { 34 | title: "BodyString", 35 | type: "string", 36 | minLength: 1, 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /packages/frontend/src/features/users/pages/UsersPage.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "urql"; 2 | import { graphql } from "src/lib/generated/gql"; 3 | import { Spinner } from "@nextui-org/react"; 4 | import { UserCard } from "../components/UserCard"; 5 | 6 | // 利用されるクエリの定義 7 | const GetUsersQuery = graphql(` 8 | query GetUsersQuery { 9 | getAllUsers(limit: 10) { 10 | ...UserFragment 11 | } 12 | } 13 | `); 14 | 15 | const UsersPage = () => { 16 | // クエリを実行してユーザーの情報を取得 17 | const [result] = useQuery({ 18 | query: GetUsersQuery, 19 | }); 20 | 21 | // クエリの結果を取得 22 | const { data, fetching } = result; 23 | 24 | // ローディング中であれば 25 | if (fetching) 26 | return ( 27 |
28 | 29 |
30 | ); 31 | 32 | return ( 33 |
34 |
35 | {data?.getAllUsers.map((user, i) => ( 36 | 37 | ))} 38 |
39 |
40 | ); 41 | // graphqlのフラグメントマスキングでやむを得ずmapのkeyでインデックスを使っているので、少し心配 42 | }; 43 | 44 | export { UsersPage }; 45 | -------------------------------------------------------------------------------- /packages/frontend/src/features/posts/pages/PostsPage.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "urql"; 2 | import { graphql } from "src/lib/generated/gql"; 3 | import { PostCard } from "../components/PostCard"; 4 | import { Spinner } from "@nextui-org/react"; 5 | 6 | // 利用されるクエリの定義 7 | const GetAllPostsQuery = graphql(` 8 | query GetAllPostsQuery { 9 | getAllPosts(limit: 10) { 10 | ...PostFragment 11 | } 12 | } 13 | `); 14 | 15 | const PostsPage = () => { 16 | // graphqlに対してクエリを実行 17 | const [result] = useQuery({ 18 | query: GetAllPostsQuery, 19 | }); 20 | 21 | // クエリの結果を取得 22 | const { data, fetching } = result; 23 | 24 | // ローディング中であれば 25 | if (fetching) 26 | return ( 27 |
28 | 29 |
30 | ); 31 | 32 | return ( 33 |
34 |
35 | {data?.getAllPosts.map((post, i) => ( 36 | 37 | ))} 38 |
39 |
40 | ); 41 | // graphqlのフラグメントマスキングでやむを得ずmapのkeyでiを使っているので、少し心配 42 | }; 43 | 44 | export { PostsPage }; 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # masterブランチへのpushに対応する 5 | push: 6 | branches: ["master"] 7 | # プルリクエストでのCIに対応する 8 | pull_request: 9 | types: [opened, synchronize] 10 | 11 | # 手動での実行に対応する 12 | workflow_dispatch: 13 | 14 | 15 | jobs: 16 | check: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 15 19 | 20 | # 環境変数の読み込み 21 | env: 22 | SCHEMA_PATH: ../graphql/schemas/*.graphql 23 | # OPERATION_PATH: ../graphql/operations/*.graphql 24 | 25 | # 実行ステップ 26 | steps: 27 | - uses: actions/checkout@v2 28 | 29 | - uses: pnpm/action-setup@v2.2.4 30 | with: 31 | version: 8.6.3 32 | 33 | - name: Setup Node.js environment 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18 37 | cache: "pnpm" 38 | 39 | # 依存関係のインストール 40 | - name: install dependencies 41 | run: pnpm install --frozen-lockfile 42 | 43 | # 型チェック 44 | - name: typecheck 45 | run: pnpm turbo typecheck 46 | 47 | # ビルド 48 | - name: build 49 | run: pnpm turbo build 50 | 51 | # - name: eslint 52 | # run: pnpm lint 53 | 54 | # - name: prettier 55 | # run: pnpm format 56 | -------------------------------------------------------------------------------- /packages/backend/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 | // ロールを格納する列挙型を定義 5 | enum Role { 6 | ADMIN 7 | USER 8 | } 9 | 10 | // データベースソースの設定 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | generator client { 17 | provider = "prisma-client-js" 18 | } 19 | 20 | // ユーザのモデルを定義 21 | // UUIDがプライマリキーの役割を果たす 22 | // ユーザとプロフィールは1対1の関係 23 | model User { 24 | user_uuid String @id @default(uuid()) 25 | auth_sub String @unique 26 | handle String @unique 27 | screen_name String 28 | bio String 29 | image_url String 30 | created_at DateTime @default(now()) 31 | updated_at DateTime @updatedAt 32 | role Role @default(USER) 33 | posts Post[] 34 | } 35 | 36 | // 投稿のモデルを定義 37 | // UUIDがプライマリキーの役割を果たす 38 | model Post { 39 | post_uuid String @id @default(uuid()) // UUIDはPrismaのデフォルトの関数を使用 40 | user User @relation(fields: [userUuid], references: [user_uuid]) 41 | userUuid String 42 | title String 43 | body String 44 | created_at DateTime @default(now()) 45 | updated_at DateTime @updatedAt 46 | is_public Boolean @default(false) 47 | } 48 | -------------------------------------------------------------------------------- /packages/docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import Link from "@docusaurus/Link"; 4 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 5 | import Layout from "@theme/Layout"; 6 | import HomepageFeatures from "@site/src/components/HomepageFeatures"; 7 | 8 | import styles from "./index.module.css"; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 |

{siteConfig.title}

16 |

{siteConfig.tagline}

17 |
18 | 19 | チュートリアルを確認する 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export default function Home() { 28 | const { siteConfig } = useDocusaurusContext(); 29 | return ( 30 | 31 | 32 |
33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-basics/create-a-document.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Create a Document 6 | 7 | Documents are **groups of pages** connected through: 8 | 9 | - a **sidebar** 10 | - **previous/next navigation** 11 | - **versioning** 12 | 13 | ## Create your first Doc 14 | 15 | Create a Markdown file at `docs/hello.md`: 16 | 17 | ```md title="docs/hello.md" 18 | # Hello 19 | 20 | This is my **first Docusaurus document**! 21 | ``` 22 | 23 | A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). 24 | 25 | ## Configure the Sidebar 26 | 27 | Docusaurus automatically **creates a sidebar** from the `docs` folder. 28 | 29 | Add metadata to customize the sidebar label and position: 30 | 31 | ```md title="docs/hello.md" {1-4} 32 | --- 33 | sidebar_label: "Hi!" 34 | sidebar_position: 3 35 | --- 36 | 37 | # Hello 38 | 39 | This is my **first Docusaurus document**! 40 | ``` 41 | 42 | It is also possible to create your sidebar explicitly in `sidebars.js`: 43 | 44 | ```js title="sidebars.js" 45 | module.exports = { 46 | tutorialSidebar: [ 47 | "intro", 48 | // highlight-next-line 49 | "hello", 50 | { 51 | type: "category", 52 | label: "Tutorial", 53 | items: ["tutorial-basics/create-a-document"], 54 | }, 55 | ], 56 | }; 57 | ``` 58 | -------------------------------------------------------------------------------- /packages/frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { RouterProvider } from "@tanstack/react-router"; 4 | import { NextUIProvider } from "@nextui-org/react"; 5 | 6 | import { router } from "./route"; 7 | import { AuthnProvider } from "./lib/provider/authn/AuthnProvider"; 8 | import { UrqlProvider } from "./lib/provider/urql/UrqlProvider"; 9 | import { TanStackRouterDevtools } from "@tanstack/router-devtools"; 10 | import { Toaster } from "react-hot-toast"; 11 | import { is_dev } from "./env"; 12 | import "src/index.css"; 13 | 14 | export const Main = () => { 15 | return ( 16 | <> 17 | 18 | { 19 | // デバッグ環境のみdevtoolsを表示 20 | // @ts-expect-error routerの型のエラーを無視 21 | is_dev && 22 | } 23 | 29 | 30 | ); 31 | }; 32 | 33 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 34 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | ); 44 | -------------------------------------------------------------------------------- /packages/infra/Dockerfiles/Backend-Dockerfile: -------------------------------------------------------------------------------- 1 | # ビルド用ステージ 2 | FROM node:latest as build-stage 3 | 4 | # 作業ディレクトリを指定 5 | WORKDIR /home 6 | 7 | # パッケージの存在するパス 8 | ARG PACKAGE_PATH=packages/backend 9 | ARG SCHEMA_PATH=./graphql/schemas/*.graphql 10 | # ARG OPERATION_PATH=/home/graphql/operations/*.graphql 11 | 12 | # package.jsonのみをコピーする 13 | COPY ${PACKAGE_PATH}/package.json ./ 14 | 15 | # pnpmに切り替える 16 | RUN corepack enable pnpm 17 | RUN corepack prepare pnpm@latest --activate 18 | 19 | # 依存関係をインストールする 20 | RUN pnpm install 21 | 22 | # データの引継ぎ 23 | COPY ${PACKAGE_PATH}/ ./ 24 | COPY ${PACKAGE_PATH}/../graphql/ ./graphql/ 25 | 26 | # graphql codegenを実行する 27 | RUN pnpm codegen 28 | 29 | # prisma generateを実行する 30 | RUN pnpm prigen 31 | 32 | # ビルドする 33 | RUN pnpm build 34 | 35 | # 本番用ステージ 36 | # nodeイメージを利用 37 | FROM node:latest as production-stage 38 | 39 | # 作業ディレクトリを指定 40 | WORKDIR /home 41 | 42 | # 6173ポートを開放している 43 | EXPOSE 6173 44 | 45 | # tiniのインストール 46 | RUN apt-get update && apt-get install -y tini 47 | 48 | # データを引き継ぐ 49 | COPY --from=build-stage --chown=node:node /home ./ 50 | 51 | # pnpmに切り替える 52 | RUN corepack enable pnpm 53 | RUN corepack prepare pnpm@latest --activate 54 | 55 | USER node 56 | 57 | # コンテナ起動時に実行するコマンド 58 | # 依存パッケージのインストールとPrismaのマイグレーション、サーバーの起動を行う 59 | ENTRYPOINT ["/usr/bin/tini", "--"] 60 | CMD ["sh", "-c", "pnpm prisma migrate deploy && pnpm start"] 61 | -------------------------------------------------------------------------------- /packages/backend/src/lib/scalars/TitleString.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, GraphQLScalarType, Kind } from "graphql"; 2 | import { createGraphQLError } from "graphql-yoga"; 3 | 4 | const validate = (value: any, ast?: ASTNode) => { 5 | if (typeof value !== "string") { 6 | throw createGraphQLError(`Value is not a string: ${value}`, ast ? { nodes: ast } : undefined); 7 | } 8 | 9 | if (!value.trim().length) { 10 | throw createGraphQLError(`Value cannot be an empty string: ${value}`, ast ? { nodes: ast } : undefined); 11 | } 12 | 13 | if (value.length > 50) { 14 | throw createGraphQLError(`Value cannot be longer than 50 characters: ${value}`, ast ? { nodes: ast } : undefined); 15 | } 16 | 17 | return value; 18 | }; 19 | 20 | export const GraphQLTitleString = /*#__PURE__*/ new GraphQLScalarType({ 21 | name: "TitleString", 22 | 23 | description: "A string that is used for the body of a post", 24 | 25 | serialize: validate, 26 | 27 | parseValue: validate, 28 | 29 | parseLiteral(ast) { 30 | if (ast.kind !== Kind.STRING) { 31 | throw createGraphQLError(`Can only validate strings but got a: ${ast.kind}`, { nodes: ast }); 32 | } 33 | return validate(ast.value, ast); 34 | }, 35 | extensions: { 36 | codegenScalarType: "string", 37 | jsonSchema: { 38 | title: "TitleString", 39 | type: "string", 40 | minLength: 1, 41 | }, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /packages/backend/src/lib/error/handling.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; 2 | import { GraphQLErrorWithCode } from "src/lib/error/error"; 3 | 4 | type AsyncFunction = (...args: T) => Promise; 5 | 6 | // 例外ハンドリングを行うための関数 7 | // 高階関数を引数に取る 8 | const withErrorHandling = 9 | (func: AsyncFunction): AsyncFunction => 10 | async (...args: T): Promise => { 11 | // ここで与えられた高階関数を実行する 12 | try { 13 | return await func(...args); 14 | } catch (error) { 15 | if (error instanceof PrismaClientKnownRequestError) { 16 | switch (error.code) { 17 | // アイテムが見つからない場合 18 | case "P2025": 19 | throw new GraphQLErrorWithCode("item_not_found"); 20 | // アイテムが存在している場合 21 | case "P2002": 22 | throw new GraphQLErrorWithCode("item_already_exists"); 23 | // ここにエラーの種類を追加していく 24 | // その他のエラー 25 | default: 26 | console.error(error); 27 | throw new GraphQLErrorWithCode("unknown_error", error.message); 28 | } 29 | } else if (error instanceof Error) { 30 | throw new GraphQLErrorWithCode("unknown_error", error.message); 31 | } else { 32 | throw new GraphQLErrorWithCode("unknown_error"); 33 | } 34 | } 35 | }; 36 | 37 | export { withErrorHandling }; 38 | -------------------------------------------------------------------------------- /packages/backend/src/lib/scalars/BioString.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, GraphQLScalarType, Kind } from "graphql"; 2 | import { createGraphQLError } from "graphql-yoga"; 3 | 4 | const validate = (value: any, ast?: ASTNode) => { 5 | if (typeof value !== "string") { 6 | throw createGraphQLError(`Value is not a string: ${value}`, ast ? { nodes: ast } : undefined); 7 | } 8 | 9 | // 空文字列を許容しない 10 | if (!value.trim().length) { 11 | throw createGraphQLError(`Value cannot be an empty string: ${value}`, ast ? { nodes: ast } : undefined); 12 | } 13 | 14 | // 400文字を超える文字列を許容しない 15 | if (value.length > 400) { 16 | throw createGraphQLError(`Value cannot be longer than 400 characters: ${value}`, ast ? { nodes: ast } : undefined); 17 | } 18 | 19 | return value; 20 | }; 21 | 22 | export const GraphQLBioString = /*#__PURE__*/ new GraphQLScalarType({ 23 | name: "BioString", 24 | 25 | description: "A string that is used for the bio of a user", 26 | 27 | serialize: validate, 28 | 29 | parseValue: validate, 30 | 31 | parseLiteral(ast) { 32 | if (ast.kind !== Kind.STRING) { 33 | throw createGraphQLError(`Can only validate strings but got a: ${ast.kind}`, { nodes: ast }); 34 | } 35 | return validate(ast.value, ast); 36 | }, 37 | extensions: { 38 | codegenScalarType: "string", 39 | jsonSchema: { 40 | title: "BioString", 41 | type: "string", 42 | minLength: 1, 43 | }, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /packages/frontend/src/features/posts/pages/PostDetailPage.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "urql"; 2 | import { graphql } from "src/lib/generated/gql"; 3 | import { PostDetailCard } from "../components/PostDetailCard"; 4 | import { Spinner } from "@nextui-org/react"; 5 | import { useParams } from "@tanstack/react-router"; 6 | 7 | // クエリするフラグメントを定義 8 | const GetPostDetailQuery = graphql(` 9 | query GetPostDetailQuery($uuid: UUID!) { 10 | getPostByUUID(uuid: $uuid) { 11 | ...PostDetailFragment 12 | } 13 | } 14 | `); 15 | 16 | const PostDetailPage = () => { 17 | // URLパラメータより投稿のUUIDを取得 18 | const post_uuid = useParams({ 19 | from: "/auth/posts/$post_uuid", 20 | })?.post_uuid; 21 | 22 | // クエリを行って投稿の情報を取得 23 | const [result] = useQuery({ 24 | query: GetPostDetailQuery, 25 | variables: { 26 | uuid: post_uuid, 27 | }, 28 | }); 29 | 30 | // クエリの結果を取得 31 | const { data, fetching } = result; 32 | 33 | // ローディング中であれば 34 | if (fetching) 35 | return ( 36 |
37 | 38 |
39 | ); 40 | 41 | return ( 42 |
43 |
{data ? :
投稿が見つかりませんでした
}
44 |
45 | ); 46 | }; 47 | 48 | export { PostDetailPage }; 49 | -------------------------------------------------------------------------------- /packages/backend/src/env.ts: -------------------------------------------------------------------------------- 1 | // 環境変数を取得し、開発環境かどうかを判定 2 | const is_dev = process.env.NODE_ENV === "development"; 3 | 4 | // 環境変数が存在していれば、そのパスから読み込み 5 | // 環境変数が存在しなければ、../graphql/schema.graphqlから読み込み 6 | const schema_path = process.env.SCHEMA_PATH || "../graphql/schemas/*.graphql"; 7 | 8 | // minioの内部エンドポイントを設定 9 | const minio_inside_endpoint = process.env.MINIO_INSIDE_ENDPOINT || ""; 10 | 11 | // minioの外部エンドポイントを設定 12 | const minio_outside_endpoint = process.env.MINIO_OUTSIDE_ENDPOINT || ""; 13 | 14 | // minioのユーザー名とパスワードを取得 15 | const minio_root_user = process.env.MINIO_ROOT_USER || ""; 16 | const minio_root_password = process.env.MINIO_ROOT_PASSWORD || ""; 17 | 18 | // minioのバケット名を設定 19 | const minio_bucket_name = process.env.MINIO_BUCKET_NAME || ""; 20 | 21 | // 開発環境であれば、空文字を設定 22 | // 本番環境であれば、LogtoエンドポイントのURLを設定 23 | const logto_endpoint = is_dev ? "" : process.env.LOGTO_ENDPOINT || ""; 24 | 25 | // 開発環境であれば、空文字を設定 26 | // 本番環境であれば、Logtoのオーディエンスを設定 27 | const logto_audience = is_dev ? "" : process.env.LOGTO_AUDIENCE || ""; 28 | 29 | // 開発環境であれば、空文字を設定 30 | // 本番環境であれば、Logtoのwebhookの検証用シークレットを設定 31 | const logto_webhook_secret = is_dev ? "" : process.env.LOGTO_WEBHOOK_SECRET || ""; 32 | 33 | export { 34 | is_dev, 35 | minio_inside_endpoint, 36 | minio_outside_endpoint, 37 | minio_root_user, 38 | minio_root_password, 39 | minio_bucket_name, 40 | logto_endpoint, 41 | logto_audience, 42 | schema_path, 43 | logto_webhook_secret, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/backend/src/lib/scalars/ScreenNameString.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, GraphQLScalarType, Kind } from "graphql"; 2 | import { createGraphQLError } from "graphql-yoga"; 3 | 4 | const validate = (value: any, ast?: ASTNode) => { 5 | if (typeof value !== "string") { 6 | throw createGraphQLError(`Value is not a string: ${value}`, ast ? { nodes: ast } : undefined); 7 | } 8 | 9 | // 空文字列を許容しない 10 | if (!value.trim().length) { 11 | throw createGraphQLError(`Value cannot be an empty string: ${value}`, ast ? { nodes: ast } : undefined); 12 | } 13 | 14 | // 50文字を超える文字列を許容しない 15 | if (value.length > 50) { 16 | throw createGraphQLError(`Value cannot be longer than 50 characters: ${value}`, ast ? { nodes: ast } : undefined); 17 | } 18 | 19 | return value; 20 | }; 21 | 22 | export const GraphQLScreenNameString = /*#__PURE__*/ new GraphQLScalarType({ 23 | name: "ScreenNameString", 24 | 25 | description: "A string that is used for the screen name of a user", 26 | 27 | serialize: validate, 28 | 29 | parseValue: validate, 30 | 31 | parseLiteral(ast) { 32 | if (ast.kind !== Kind.STRING) { 33 | throw createGraphQLError(`Can only validate strings but got a: ${ast.kind}`, { nodes: ast }); 34 | } 35 | return validate(ast.value, ast); 36 | }, 37 | extensions: { 38 | codegenScalarType: "string", 39 | jsonSchema: { 40 | title: "ScreenNameString", 41 | type: "string", 42 | minLength: 1, 43 | }, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /packages/backend/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | function generateRandomString(num_chars = 8) { 6 | return Math.random().toString(num_chars).substring(2, 15) + Math.random().toString(36).substring(2, 15); 7 | } 8 | 9 | async function main() { 10 | const auth_subs = ["1g2h3j4k5l6", "9h8g7f6d5s4"]; 11 | 12 | await prisma.user.createMany({ 13 | data: [ 14 | { 15 | auth_sub: auth_subs[0], 16 | handle: generateRandomString(), 17 | screen_name: generateRandomString(), 18 | bio: generateRandomString(), 19 | image_url: "https://picsum.photos/200", 20 | }, 21 | 22 | { 23 | auth_sub: auth_subs[1], 24 | handle: generateRandomString(), 25 | screen_name: generateRandomString(), 26 | bio: generateRandomString(), 27 | image_url: "https://picsum.photos/200", 28 | }, 29 | ], 30 | }); 31 | 32 | await Promise.all( 33 | auth_subs.map(async (auth_sub) => { 34 | await prisma.post.create({ 35 | data: { 36 | title: generateRandomString(), 37 | body: generateRandomString(36), 38 | user: { 39 | connect: { 40 | auth_sub: auth_sub, 41 | }, 42 | }, 43 | }, 44 | }); 45 | }) 46 | ); 47 | } 48 | 49 | main() 50 | .catch((e) => { 51 | throw e; 52 | }) 53 | .finally(async () => { 54 | await prisma.$disconnect(); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-extras/manage-docs-versions.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Manage Docs Versions 6 | 7 | Docusaurus can manage multiple versions of your docs. 8 | 9 | ## Create a docs version 10 | 11 | Release a version 1.0 of your project: 12 | 13 | ```bash 14 | npm run docusaurus docs:version 1.0 15 | ``` 16 | 17 | The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created. 18 | 19 | Your docs now have 2 versions: 20 | 21 | - `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs 22 | - `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs** 23 | 24 | ## Add a Version Dropdown 25 | 26 | To navigate seamlessly across versions, add a version dropdown. 27 | 28 | Modify the `docusaurus.config.js` file: 29 | 30 | ```js title="docusaurus.config.js" 31 | module.exports = { 32 | themeConfig: { 33 | navbar: { 34 | items: [ 35 | // highlight-start 36 | { 37 | type: "docsVersionDropdown", 38 | }, 39 | // highlight-end 40 | ], 41 | }, 42 | }, 43 | }; 44 | ``` 45 | 46 | The docs version dropdown appears in your navbar: 47 | 48 | ![Docs Version Dropdown](./img/docsVersionDropdown.png) 49 | 50 | ## Update an existing version 51 | 52 | It is possible to edit versioned docs in their respective folder: 53 | 54 | - `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello` 55 | - `docs/hello.md` updates `http://localhost:3000/docs/next/hello` 56 | -------------------------------------------------------------------------------- /packages/infra/Dockerfiles/Frontend-Dockerfile: -------------------------------------------------------------------------------- 1 | # ビルド用ステージ 2 | FROM node:latest as build-stage 3 | 4 | # 作業ディレクトリを指定 5 | WORKDIR /home 6 | 7 | # パッケージの存在するパス 8 | ARG PACKAGE_PATH=packages/frontend 9 | ARG SCHEMA_PATH=./graphql/schemas/*.graphql 10 | # ARG OPERATION_PATH=/home/graphql/operations/*.graphql 11 | 12 | # logtoのエンドポイントを指定 13 | ARG VITE_LOGTO_ENDPOINT=https://auth.localhost 14 | # logtoのアプリケーションIDを指定 15 | ARG VITE_LOGTO_APPID=dummy 16 | # logtoのAPIリソースを指定 17 | ARG VITE_LOGTO_API_RESOURCE=https://dummy 18 | 19 | # ホスト名(バックエンドへのフェッチに必須) 20 | ARG VITE_HOSTNAME=dummy.dummy 21 | 22 | # package.jsonのみをコピーする 23 | COPY ${PACKAGE_PATH}/package.json ./ 24 | 25 | # pnpmに切り替える 26 | RUN corepack enable pnpm 27 | RUN corepack prepare pnpm@latest --activate 28 | 29 | # 依存関係をインストールする 30 | RUN pnpm install 31 | 32 | # データの引継ぎ 33 | COPY ${PACKAGE_PATH}/ ./ 34 | COPY ${PACKAGE_PATH}/../graphql/ ./graphql/ 35 | 36 | # graphql codegenを実行する 37 | RUN pnpm codegen 38 | 39 | # ビルドする 40 | # 環境変数としてホスト名、エンドポイント、アプリケーションID、APIリソースを渡す 41 | RUN VITE_HOSTNAME=${VITE_HOSTNAME} VITE_LOGTO_ENDPOINT=${VITE_LOGTO_ENDPOINT} VITE_LOGTO_APPID=${VITE_LOGTO_APPID} VITE_LOGTO_API_RESOURCE=${VITE_LOGTO_API_RESOURCE} pnpm build 42 | 43 | # 本番用ステージ 44 | # nginxイメージを利用 45 | FROM nginx:latest as production-stage 46 | 47 | # nginx構成ファイルのディレクトリ 48 | ARG INFRADIR=packages/infra 49 | 50 | # データを引き継ぐ 51 | COPY --from=build-stage /home/dist /usr/share/nginx/dist 52 | 53 | # COPY ${INFRADIR}/config/nginx-prod /etc/nginx/conf.d 54 | COPY ${INFRADIR}/config/nginx-prod /etc/nginx/templates -------------------------------------------------------------------------------- /packages/backend/src/resolver.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLContext } from "./context"; 2 | import { Resolvers } from "./lib/generated/resolver-types"; 3 | import { resolvers as scalarResolvers } from "graphql-scalars"; 4 | import { UserTypeResolver } from "./resolvers/types/userType"; 5 | import { PostTypeResolver } from "./resolvers/types/postType"; 6 | import { PanelQueryResolver } from "./resolvers/queries/panelQuery"; 7 | import { PanelMutationResolver } from "./resolvers/mutations/panelMutation"; 8 | import { GraphQLHandleString } from "./lib/scalars/HandleString"; 9 | import { GraphQLTitleString } from "./lib/scalars/TitleString"; 10 | import { GraphQLBodyString } from "./lib/scalars/BodyString"; 11 | import { GraphQLScreenNameString } from "./lib/scalars/ScreenNameString"; 12 | import { GraphQLBioString } from "./lib/scalars/BioString"; 13 | 14 | // リゾルバーの定義 15 | export const resolvers: Resolvers = { 16 | // スカラー型に対応するリゾルバーをマージ 17 | ...scalarResolvers, 18 | 19 | // ユーザのハンドル用のスカラー型のリゾルバー 20 | HandleString: GraphQLHandleString, 21 | // ユーザのスクリーンネーム用のスカラー型のリゾルバー 22 | ScreenNameString: GraphQLScreenNameString, 23 | // ユーザの自己紹介用のスカラー型のリゾルバー 24 | BioString: GraphQLBioString, 25 | // タイトル用のスカラー型のリゾルバー 26 | TitleString: GraphQLTitleString, 27 | // 本文用のスカラー型のリゾルバー 28 | BodyString: GraphQLBodyString, 29 | 30 | // クエリのリゾルバー 31 | Query: { 32 | ...PanelQueryResolver, 33 | }, 34 | 35 | // ミューテーションのリゾルバー 36 | Mutation: { 37 | ...PanelMutationResolver, 38 | }, 39 | // ユーザ型のリゾルバー 40 | User: { 41 | ...UserTypeResolver, 42 | }, 43 | // 投稿型のリゾルバー 44 | Post: { 45 | ...PostTypeResolver, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /packages/backend/src/lib/scalars/HandleString.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, GraphQLScalarType, Kind } from "graphql"; 2 | import { createGraphQLError } from "graphql-yoga"; 3 | 4 | const validate = (value: any, ast?: ASTNode) => { 5 | if (typeof value !== "string") { 6 | throw createGraphQLError(`Value is not a string: ${value}`, ast ? { nodes: ast } : undefined); 7 | } 8 | 9 | // 空文字列を許容しない 10 | if (!value.trim().length) { 11 | throw createGraphQLError(`Value cannot be an empty string: ${value}`, ast ? { nodes: ast } : undefined); 12 | } 13 | 14 | // ASCIIで表現可能な小文字、数字、簡単な記号のみを許容 15 | if (!/^[a-z0-9_]+$/.test(value)) { 16 | throw createGraphQLError(`Value must consist only of lowercase ASCII letters, numbers, or underscores: ${value}`, ast ? { nodes: ast } : undefined); 17 | } 18 | 19 | // 20文字を超える文字列を許容しない 20 | if (value.length > 20) { 21 | throw createGraphQLError(`Value cannot be longer than 20 characters: ${value}`, ast ? { nodes: ast } : undefined); 22 | } 23 | 24 | return value; 25 | }; 26 | 27 | export const GraphQLHandleString = /*#__PURE__*/ new GraphQLScalarType({ 28 | name: "HandleString", 29 | 30 | description: "A string that is used for the handle of a user", 31 | 32 | serialize: validate, 33 | 34 | parseValue: validate, 35 | 36 | parseLiteral(ast) { 37 | if (ast.kind !== Kind.STRING) { 38 | throw createGraphQLError(`Can only validate strings but got a: ${ast.kind}`, { nodes: ast }); 39 | } 40 | return validate(ast.value, ast); 41 | }, 42 | extensions: { 43 | codegenScalarType: "string", 44 | jsonSchema: { 45 | title: "HandleString", 46 | type: "string", 47 | minLength: 1, 48 | pattern: "^[a-z0-9_]+$", // JSON Schemaでの正規表現パターン 49 | }, 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /packages/frontend/src/features/users/components/UserDetailProfileImageInput.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "@nextui-org/react"; 2 | import { graphql } from "src/lib/generated/gql"; 3 | import { useMutation } from "urql"; 4 | import { toast } from "react-hot-toast"; 5 | 6 | const UpdateMyProfileImageMutation = graphql(` 7 | mutation UpdateMyProfileImageMutation($file: File!) { 8 | uploadProfileImage(file: $file) { 9 | image_url 10 | } 11 | } 12 | `); 13 | 14 | const UserDetailProfileImageInput = ({ is_myself, image_url }: { is_myself: boolean; image_url: string }) => { 15 | // 画像のミューテーション用のフックを実行 16 | const [, update_my_profile_image] = useMutation(UpdateMyProfileImageMutation); 17 | 18 | // ファイルが選択されたときのイベントハンドラ 19 | const handle_select_file = async (e: React.ChangeEvent) => { 20 | // ファイルが選択されていない場合は処理を終了 21 | if (!e.target.files || e.target.files.length === 0) { 22 | return; 23 | } 24 | 25 | // ファイルを取得 26 | const file = e.target.files[0]; 27 | 28 | // ミューテーションを実行 29 | const result = await update_my_profile_image({ 30 | file, 31 | }); 32 | 33 | if (result.error) { 34 | toast.error("エラーが発生しました"); 35 | return; 36 | } 37 | 38 | toast.success("プロフィール画像を更新しました"); 39 | return; 40 | }; 41 | 42 | return ( 43 |
44 | 45 | {is_myself && ( 46 | 47 | )} 48 |
49 | ); 50 | }; 51 | 52 | export { UserDetailProfileImageInput }; 53 | -------------------------------------------------------------------------------- /packages/frontend/src/features/users/components/PostCardForUser.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@tanstack/react-router"; 2 | import { Card, CardBody, CardFooter, Button } from "@nextui-org/react"; 3 | import { FragmentType, useFragment } from "src/lib/generated"; 4 | import { graphql } from "src/lib/generated/gql"; 5 | 6 | // クエリするフラグメントを定義 7 | const PostPopupFragment = graphql(` 8 | fragment PostPopupFragment on Post { 9 | post_uuid 10 | title 11 | body 12 | } 13 | `); 14 | 15 | // フラグメントの定義 16 | // ユーザ画面でポップアップとして表示される投稿カード 17 | const PostCardForUser = ({ post: post_frag }: { post: FragmentType }) => { 18 | // フラグメントの型を指定して対応するデータを取得 19 | const post = useFragment(PostPopupFragment, post_frag); 20 | 21 | return ( 22 | 23 | 24 |
25 |
26 |

{post.title}

27 |
28 |
29 |

{post.body}

30 |
31 |
32 |
33 | 34 |
35 | 45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export { PostCardForUser }; 52 | -------------------------------------------------------------------------------- /packages/backend/src/lib/security/authn.ts: -------------------------------------------------------------------------------- 1 | import { Auth0PluginOptions } from "@envelop/auth0"; 2 | import { AuthMockPluginOptions } from "../plugins/useAuthMock"; 3 | import { logto_audience, logto_endpoint } from "src/env"; 4 | import { TokenExpiredError, JsonWebTokenError, NotBeforeError } from "jsonwebtoken"; 5 | import { GraphQLErrorWithCode } from "src/lib/error/error"; 6 | 7 | // エラー処理を行う関数 8 | const onError = (error: Error) => { 9 | if (error instanceof TokenExpiredError) { 10 | // JWTが期限切れの場合 11 | throw new GraphQLErrorWithCode("jwt_expired"); 12 | } else if (error instanceof NotBeforeError) { 13 | // JWTが有効期限前の場合 14 | throw new GraphQLErrorWithCode("jwt_not_before"); 15 | } else if (error instanceof JsonWebTokenError) { 16 | // JWTが不正な場合 17 | throw new GraphQLErrorWithCode("jwt_web_token_error"); 18 | } else { 19 | // その他のエラー 20 | throw new GraphQLErrorWithCode("unknown_error", error.message); 21 | } 22 | }; 23 | 24 | const AuthMockOption: AuthMockPluginOptions = { 25 | // 認証されていないリクエストも許可 26 | preventUnauthenticatedAccess: false, 27 | // ペイロードを格納するフィールド名を指定 28 | extendContextField: "logto", 29 | // エラー処理 30 | onError: onError, 31 | }; 32 | 33 | const AuthnOption: Auth0PluginOptions = { 34 | // ドメイン部分は上書きするためダミー 35 | domain: "", 36 | // audienceは環境変数から取得 37 | audience: logto_audience, 38 | // オプションを上書き 39 | // logtoのjwksUriと指定 40 | jwksClientOptions: { 41 | jwksUri: `${logto_endpoint}/oidc/jwks`, 42 | }, 43 | // JWTの検証オプションを上書き 44 | jwtVerifyOptions: { 45 | algorithms: ["ES384"], 46 | issuer: `${logto_endpoint}/oidc`, 47 | }, 48 | // 認証されていないリクエストも許可 49 | preventUnauthenticatedAccess: false, 50 | // ペイロードを格納するフィールド名を指定 51 | extendContextField: "logto", 52 | // エラー処理 53 | onError: onError, 54 | }; 55 | 56 | export { AuthMockOption, AuthnOption }; 57 | -------------------------------------------------------------------------------- /packages/docs/src/components/HomepageFeatures/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import styles from "./styles.module.css"; 4 | 5 | const FeatureList = [ 6 | { 7 | title: "Easy to Use", 8 | Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default, 9 | description: <>Docusaurus was designed from the ground up to be easily installed and used to get your website up and running quickly., 10 | }, 11 | { 12 | title: "Focus on What Matters", 13 | Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default, 14 | description: ( 15 | <> 16 | Docusaurus lets you focus on your docs, and we'll do the chores. Go ahead and move your docs into the docs directory. 17 | 18 | ), 19 | }, 20 | { 21 | title: "Powered by React", 22 | Svg: require("@site/static/img/undraw_docusaurus_react.svg").default, 23 | description: <>Extend or customize your website layout by reusing React. Docusaurus can be extended while reusing the same header and footer., 24 | }, 25 | ]; 26 | 27 | function Feature({ Svg, title, description }) { 28 | return ( 29 |
30 |
31 | 32 |
33 |
34 |

{title}

35 |

{description}

36 |
37 |
38 | ); 39 | } 40 | 41 | export default function HomepageFeatures() { 42 | return ( 43 |
44 |
45 |
46 | {FeatureList.map((props, idx) => ( 47 | 48 | ))} 49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /packages/frontend/src/features/posts/components/UserCardForPost.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@tanstack/react-router"; 2 | import { Card, CardBody, CardFooter, Button } from "@nextui-org/react"; 3 | import { FragmentType, useFragment } from "src/lib/generated"; 4 | import { graphql } from "src/lib/generated/gql"; 5 | 6 | // クエリするフラグメントを定義 7 | const UserPopupFragment = graphql(` 8 | fragment UserPopupFragment on User { 9 | user_uuid 10 | handle 11 | screen_name 12 | bio 13 | } 14 | `); 15 | 16 | const UserCardForPost = ({ user: user_frag }: { user: FragmentType }) => { 17 | // フラグメントの型を指定して対応するデータを取得 18 | const user = useFragment(UserPopupFragment, user_frag); 19 | 20 | return ( 21 | 22 | 23 |
24 |
25 |

{user.screen_name}

26 |

@{user.handle}

27 |
28 |
29 |

{user.bio}

30 |
31 |
32 |
33 | 34 |
35 | 45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export { UserCardForPost }; 52 | -------------------------------------------------------------------------------- /packages/backend/src/resolvers/types/userType.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { UserResolvers } from "src/lib/generated/resolver-types"; 3 | import { GraphQLContext } from "src/context"; 4 | import { withErrorHandling } from "src/lib/error/handling"; 5 | 6 | const UserTypeResolver: UserResolvers = { 7 | // 投稿フィールドのリゾルバー 8 | // @ts-expect-error 返却されるuserにpostsフィールドが存在しないためエラーが出るが、実際には存在するので無視 9 | posts: async (parent, args, context) => { 10 | const safe = withErrorHandling( 11 | async (currentUser_uuid: string, user_uuid: string, prisma: PrismaClient, { offset, limit }: { offset: number; limit: number }) => { 12 | // UUIDからユーザーを取得 13 | const result = await prisma.user 14 | .findUniqueOrThrow({ 15 | where: { 16 | user_uuid: user_uuid, 17 | }, 18 | }) 19 | // そこから投稿を取得 20 | .posts({ 21 | where: { 22 | // 投稿者が自分でない かつ 非公開のものは除外する 23 | // つまり、投稿が自分 または 公開のもののみ取得する 24 | OR: [ 25 | { 26 | userUuid: currentUser_uuid, 27 | }, 28 | { 29 | is_public: true, 30 | }, 31 | ], 32 | }, 33 | skip: offset, 34 | take: limit, 35 | // 投稿を新しい順に並び替える 36 | orderBy: { 37 | created_at: "desc", 38 | }, 39 | }); 40 | return result; 41 | } 42 | ); 43 | 44 | // ペアレントオブジェクトから現在ログインしているユーザーのデータを取得 45 | const { user_uuid } = parent; 46 | // 引数からページネーションのoffsetとlimitを取得 47 | const { offset, limit } = args; 48 | // コンテキストからPrismaクライアントと現在ログインしているユーザーのデータを取得 49 | const { prisma, currentUser } = context; 50 | 51 | return await safe(currentUser.user_uuid, user_uuid, prisma, { limit, offset }); 52 | }, 53 | }; 54 | 55 | export { UserTypeResolver }; 56 | -------------------------------------------------------------------------------- /packages/frontend/src/features/users/pages/UserDetailPage.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "urql"; 2 | import { useParams } from "@tanstack/react-router"; 3 | import { Spinner } from "@nextui-org/react"; 4 | import { graphql } from "src/lib/generated/gql"; 5 | import { UserDetailCard } from "../components/UserDetailCard"; 6 | 7 | // 自身のユーザを取得するためのクエリの定義 8 | const GetMeQuery = graphql(` 9 | query GetMeQuery { 10 | getMyUser { 11 | user_uuid 12 | } 13 | } 14 | `); 15 | 16 | // 利用されるクエリの定義 17 | const UserDetailQuery = graphql(` 18 | query UserDetailQuery($uuid: UUID!) { 19 | getUserByUUID(uuid: $uuid) { 20 | ...UserDetailFragment 21 | } 22 | } 23 | `); 24 | 25 | const UserDetailPage = () => { 26 | // URLパラメータよりユーザーのUUIDを取得 27 | const user_uuid = useParams({ 28 | from: "/auth/users/$user_uuid", 29 | })?.user_uuid; 30 | 31 | // 自身のユーザの情報を取得 32 | const [myUserResult] = useQuery({ 33 | query: GetMeQuery, 34 | requestPolicy: "cache-first", 35 | }); 36 | 37 | // クエリを行ってユーザーの情報を取得 38 | const [UserResult] = useQuery({ 39 | query: UserDetailQuery, 40 | variables: { 41 | uuid: user_uuid, 42 | }, 43 | }); 44 | 45 | // 自身のユーザーの情報を取得 46 | const { data: myUserData } = myUserResult; 47 | 48 | // 自身のユーザーのUUIDを取得 49 | const my_user_uuid = myUserData?.getMyUser?.user_uuid ?? ""; 50 | 51 | // クエリの結果を取得 52 | const { data, fetching } = UserResult; 53 | 54 | // ローディング中であれば 55 | if (fetching) 56 | return ( 57 |
58 | 59 |
60 | ); 61 | 62 | return ( 63 |
64 |
65 | {data ? :
ユーザが見つかりませんでした
} 66 |
67 |
68 | ); 69 | }; 70 | 71 | export { UserDetailPage }; 72 | -------------------------------------------------------------------------------- /packages/frontend/vite.config.ts.timestamp-1694775590419-7203cb0b93586.mjs: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from "file:///workspaces/callstack/node_modules/.pnpm/vite@4.4.8_@types+node@20.4.8_less@4.2.0/node_modules/vite/dist/node/index.js"; 3 | import tsConfigPaths from "file:///workspaces/callstack/node_modules/.pnpm/vite-tsconfig-paths@4.2.0_typescript@5.1.6_vite@4.4.8/node_modules/vite-tsconfig-paths/dist/index.mjs"; 4 | import react from "file:///workspaces/callstack/node_modules/.pnpm/@vitejs+plugin-react-swc@3.3.2_vite@4.4.8/node_modules/@vitejs/plugin-react-swc/index.mjs"; 5 | var vite_config_default = defineConfig({ 6 | server: { 7 | host: true, 8 | port: 5173 9 | }, 10 | plugins: [react(), tsConfigPaths()] 11 | }); 12 | export { 13 | vite_config_default as default 14 | }; 15 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvd29ya3NwYWNlcy9jYWxsc3RhY2svcGFja2FnZXMvZnJvbnRlbmRcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi93b3Jrc3BhY2VzL2NhbGxzdGFjay9wYWNrYWdlcy9mcm9udGVuZC92aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vd29ya3NwYWNlcy9jYWxsc3RhY2svcGFja2FnZXMvZnJvbnRlbmQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xuaW1wb3J0IHRzQ29uZmlnUGF0aHMgZnJvbSBcInZpdGUtdHNjb25maWctcGF0aHNcIjtcbmltcG9ydCByZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djXCI7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBzZXJ2ZXI6IHtcbiAgICBob3N0OiB0cnVlLFxuICAgIHBvcnQ6IDUxNzMsXG4gIH0sXG4gIHBsdWdpbnM6IFtyZWFjdCgpLCB0c0NvbmZpZ1BhdGhzKCldLFxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQXVTLFNBQVMsb0JBQW9CO0FBQ3BVLE9BQU8sbUJBQW1CO0FBQzFCLE9BQU8sV0FBVztBQUdsQixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixRQUFRO0FBQUEsSUFDTixNQUFNO0FBQUEsSUFDTixNQUFNO0FBQUEsRUFDUjtBQUFBLEVBQ0EsU0FBUyxDQUFDLE1BQU0sR0FBRyxjQUFjLENBQUM7QUFDcEMsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K 16 | -------------------------------------------------------------------------------- /packages/docs/docs/tutorial-extras/translate-your-site.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Translate your site 6 | 7 | Let's translate `docs/intro.md` to French. 8 | 9 | ## Configure i18n 10 | 11 | Modify `docusaurus.config.js` to add support for the `fr` locale: 12 | 13 | ```js title="docusaurus.config.js" 14 | module.exports = { 15 | i18n: { 16 | defaultLocale: "en", 17 | locales: ["en", "fr"], 18 | }, 19 | }; 20 | ``` 21 | 22 | ## Translate a doc 23 | 24 | Copy the `docs/intro.md` file to the `i18n/fr` folder: 25 | 26 | ```bash 27 | mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ 28 | 29 | cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md 30 | ``` 31 | 32 | Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French. 33 | 34 | ## Start your localized site 35 | 36 | Start your site on the French locale: 37 | 38 | ```bash 39 | npm run start -- --locale fr 40 | ``` 41 | 42 | Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. 43 | 44 | :::caution 45 | 46 | In development, you can only use one locale at a same time. 47 | 48 | ::: 49 | 50 | ## Add a Locale Dropdown 51 | 52 | To navigate seamlessly across languages, add a locale dropdown. 53 | 54 | Modify the `docusaurus.config.js` file: 55 | 56 | ```js title="docusaurus.config.js" 57 | module.exports = { 58 | themeConfig: { 59 | navbar: { 60 | items: [ 61 | // highlight-start 62 | { 63 | type: "localeDropdown", 64 | }, 65 | // highlight-end 66 | ], 67 | }, 68 | }, 69 | }; 70 | ``` 71 | 72 | The locale dropdown now appears in your navbar: 73 | 74 | ![Locale Dropdown](./img/localeDropdown.png) 75 | 76 | ## Build your localized site 77 | 78 | Build your site for a specific locale: 79 | 80 | ```bash 81 | npm run build -- --locale fr 82 | ``` 83 | 84 | Or build your site to include all the locales at once: 85 | 86 | ```bash 87 | npm run build 88 | ``` 89 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.1", 4 | "type": "commonjs", 5 | "scripts": { 6 | "dev": "NODE_ENV=development tsx watch ./src/index.ts", 7 | "build": "swc src -d dist/src && tsc-alias", 8 | "start": "NODE_ENV=production node ./dist/src/index.js", 9 | "typecheck": "tsc --noEmit", 10 | "lint": "eslint src --ext .ts", 11 | "format": "prettier --write \"src/**/*.ts\"", 12 | "codegen": "graphql-codegen --config codegen.ts", 13 | "prigen": "prisma generate", 14 | "primig": "prisma migrate deploy", 15 | "priseed": "prisma db seed" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@envelop/auth0": "^5.0.0", 22 | "@envelop/core": "^4.0.0", 23 | "@envelop/disable-introspection": "^5.0.0", 24 | "@envelop/generic-auth": "^6.0.0", 25 | "@envelop/graphql-jit": "^6.0.1", 26 | "@escape.tech/graphql-armor": "^2.2.0", 27 | "@graphql-tools/load-files": "^7.0.0", 28 | "@graphql-tools/merge": "^9.0.0", 29 | "@prisma/client": "^5.1.1", 30 | "@swc/helpers": "^0.5.1", 31 | "@types/jsonwebtoken": "^9.0.2", 32 | "@types/uuid": "^9.0.3", 33 | "graphql": "^16.7.1", 34 | "graphql-middleware": "^6.1.35", 35 | "graphql-scalars": "^1.22.2", 36 | "graphql-shield": "^7.6.5", 37 | "graphql-yoga": "^4.0.3", 38 | "jimp": "^0.22.10", 39 | "jsonwebtoken": "^9.0.1", 40 | "jwks-rsa": "^3.0.1", 41 | "minio": "^7.1.3", 42 | "prisma": "^5.1.1", 43 | "ts-patch": "^3.0.2", 44 | "tsc-alias": "^1.8.7", 45 | "tsx": "^3.12.7", 46 | "uuid": "^9.0.0" 47 | }, 48 | "devDependencies": { 49 | "@graphql-codegen/cli": "5.0.0", 50 | "@graphql-codegen/typescript": "4.0.1", 51 | "@graphql-codegen/typescript-resolvers": "4.0.1", 52 | "@swc/cli": "^0.1.62", 53 | "@swc/core": "^1.3.74", 54 | "@typescript-eslint/eslint-plugin": "^5.62.0", 55 | "@typescript-eslint/parser": "^5.62.0", 56 | "eslint": "^8.46.0", 57 | "typescript": "^5.1.6" 58 | }, 59 | "prisma": { 60 | "seed": "tsx prisma/seed.ts" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # callstack とは 6 | 7 | callstack とは、**pnpm workspace** + **turborepo**で構成されたモノレポのボイラープレートです。 8 | 9 | ⚠️ このプロジェクトはまだ **開発中** です。 10 | 11 | # 特徴 12 | 13 | - [pnpm](https://pnpm.io/) と[turborepo](https://turbo.build/) を使ったモノレポのボイラープレート 14 | - [husky](https://github.com/typicode/husky) 等のツールによるコード品質の維持 15 | - devcontainer による開発環境の管理 16 | - [react](https://reactjs.org/) によるフロントエンドの構築 17 | - [graphql-yoga](https://the-guild.dev/graphql/yoga-server) によるバックエンドの構築 18 | - [prisma](https://www.prisma.io/) によるデータベース管理 19 | - docker compose による本番環境のコンテナ管理 20 | - 組み込まれた認証とユーザ管理基盤 21 | - graphql-yoga の認可モジュールによる認可管理 22 | 23 | ## Getting Started 24 | 25 | Get started by **creating a new site**. 26 | 27 | Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**. 28 | 29 | ### What you'll need 30 | 31 | - [Node.js](https://nodejs.org/en/download/) version 16.14 or above: 32 | - When installing Node.js, you are recommended to check all checkboxes related to dependencies. 33 | 34 | ## Generate a new site 35 | 36 | Generate a new Docusaurus site using the **classic template**. 37 | 38 | The classic template will automatically be added to your project after you run the command: 39 | 40 | ```bash 41 | npm init docusaurus@latest my-website classic 42 | ``` 43 | 44 | You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. 45 | 46 | The command also installs all necessary dependencies you need to run Docusaurus. 47 | 48 | ## Start your site 49 | 50 | Run the development server: 51 | 52 | ```bash 53 | cd my-website 54 | npm run start 55 | ``` 56 | 57 | The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there. 58 | 59 | The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/. 60 | 61 | Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes. 62 | -------------------------------------------------------------------------------- /packages/backend/src/lib/error/error.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from "graphql"; 2 | 3 | // エラーコード 4 | const error_code = { 5 | // JWTのエラー 6 | // JWTの期限切れエラー 7 | jwt_expired: "JWT_EXPIRED", 8 | // JWTの署名エラー 9 | jwt_invalid_signature: "JWT_INVALID_SIGNATURE", 10 | // JWTの有効期限が開始前エラー 11 | jwt_not_before: "JWT_NOT_BEFORE", 12 | // JWTトークンのエラー 13 | jwt_web_token_error: "JWT_WEB_TOKEN_ERROR", 14 | 15 | // 認可において対応するユーザが存在しないエラー(ログインしていない またはログインが無効) 16 | authz_not_logged_in: "AUTHZ_NOT_LOGGED_IN", 17 | // 認可ディレクティブで指定した引数に対応するロールが存在しないエラー 18 | authz_role_not_found: "AUTHZ_ROLE_NOT_FOUND", 19 | // ログインユーザでの認可に失敗したエラー 20 | authz_failed: "AUTHZ_FAILED", 21 | 22 | // 存在しないアイテムのエラー 23 | item_not_found: "ITEM_NOT_FOUND", 24 | // 存在するアイテムのエラー 25 | item_already_exists: "ITEM_ALREADY_EXISTS", 26 | 27 | // アイテムのオーナが自分ではないエラー 28 | item_not_owned: "ITEM_NOT_OWNED", 29 | 30 | // その他のエラー 31 | unknown_error: "UNKNOWN_ERROR", 32 | }; 33 | 34 | // エラーコードの型 35 | type ErrorCode = keyof typeof error_code; 36 | 37 | // エラーコードのメッセージ 38 | const error_message: { [key in ErrorCode]: string } = { 39 | jwt_expired: "⏰ JWT is expired", 40 | jwt_invalid_signature: "❌ JWT signature is invalid", 41 | jwt_not_before: "⏳ JWT is not before", 42 | jwt_web_token_error: "🚫 JWT is invalid", 43 | authz_not_logged_in: "👤 Not logged in", 44 | authz_role_not_found: "🔍 Role not found", 45 | authz_failed: "🔐 Authorization failed", 46 | item_not_found: "🔎 Item not found", 47 | item_already_exists: "🔄 Item already exists", 48 | item_not_owned: "🚷 Item is not owned", 49 | unknown_error: "❓ Unknown error", 50 | }; 51 | 52 | // カスタムのエラークラス 53 | class GraphQLErrorWithCode extends GraphQLError { 54 | constructor(code: ErrorCode, message?: string) { 55 | // 拡張フィールドの定義 56 | const extensions = { 57 | code: code, 58 | }; 59 | 60 | // エラーの内容を表示 61 | console.error(`[ERROR] ${code}: ${message ? message : error_message[code]}`); 62 | 63 | // 親クラスのコンストラクタを呼び出す 64 | super(message ? message : error_message[code], { 65 | extensions: extensions, 66 | }); 67 | } 68 | } 69 | 70 | export { GraphQLErrorWithCode, error_code }; 71 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Docusaurus site to GitHub Pages 2 | name: Deploy Docusaurus with GitHub Pages dependencies preinstalled 3 | 4 | on: 5 | # masterブランチへのpushに対応する 6 | push: 7 | branches: ["master"] 8 | # プルリクエストでのCIに対応する 9 | pull_request: 10 | types: [opened, synchronize] 11 | 12 | # 手動での実行に対応する 13 | workflow_dispatch: 14 | 15 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 16 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 17 | concurrency: 18 | group: "pages" 19 | cancel-in-progress: false 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 15 25 | 26 | # 環境変数の読み込み 27 | env: 28 | SCHEMA_PATH: ../graphql/schemas/*.graphql 29 | # OPERATION_PATH: ../graphql/operations/*.graphql 30 | 31 | # 実行ステップ 32 | steps: 33 | - uses: actions/checkout@v2 34 | 35 | - uses: pnpm/action-setup@v2.2.4 36 | with: 37 | version: 8.6.3 38 | 39 | - name: Setup Node.js environment 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: 18 43 | cache: "pnpm" 44 | 45 | # 依存関係のインストール 46 | - name: install dependencies 47 | run: pnpm install --frozen-lockfile 48 | 49 | - name: Build 50 | run: | 51 | pnpm run docbuild 52 | working-directory: packages/docs 53 | 54 | - name: Upload artifact 55 | uses: actions/upload-pages-artifact@v2 56 | with: 57 | path: packages/docs/build 58 | 59 | # Deployment job 60 | deploy: 61 | # masterブランチのときのみ実行する 62 | if : github.ref == 'refs/heads/master' 63 | # 必要な権限を付与する 64 | permissions: 65 | pages: write # to deploy to Pages 66 | id-token: write 67 | environment: 68 | name: github-pages 69 | url: ${{ steps.deployment.outputs.page_url }} 70 | runs-on: ubuntu-latest 71 | needs: build 72 | steps: 73 | - name: Deploy to GitHub Pages 74 | id: deployment 75 | uses: actions/deploy-pages@v2 76 | -------------------------------------------------------------------------------- /packages/backend/src/lib/security/authz.ts: -------------------------------------------------------------------------------- 1 | import { User, Role } from "@prisma/client"; 2 | import { GraphQLContext } from "src/context"; 3 | import { ResolveUserFn, ValidateUserFn } from "@envelop/generic-auth"; 4 | import { Kind } from "graphql"; 5 | import { GenericAuthPluginOptions } from "@envelop/generic-auth"; 6 | import { GraphQLErrorWithCode } from "src/lib/error/error"; 7 | 8 | const resolveUserFn: ResolveUserFn = async (context) => { 9 | // コンテキストからsubを取得 10 | const sub = context.logto?.sub; 11 | 12 | // もしsubが存在しない場合は 13 | if (!sub) { 14 | // nullを返す 15 | return null; 16 | } 17 | 18 | try { 19 | // 対応するユーザーを取得 20 | const user = await context.prisma.user.findUniqueOrThrow({ 21 | where: { 22 | auth_sub: sub, 23 | }, 24 | }); 25 | 26 | return user; 27 | } catch (error) { 28 | // エラーが発生した場合はnullを返す 29 | return null; 30 | } 31 | }; 32 | 33 | const validateUserFn: ValidateUserFn = ({ user, fieldAuthDirectiveNode }) => { 34 | // ユーザが存在しない場合 35 | if (!user) { 36 | // 失敗とする 37 | throw new GraphQLErrorWithCode("authz_not_logged_in"); 38 | } 39 | 40 | // ディレクティブに引数が指定されていなければ 41 | if (!fieldAuthDirectiveNode?.arguments) { 42 | // 成功とする 43 | return; 44 | } 45 | 46 | // 引数を取得 47 | const args = fieldAuthDirectiveNode.arguments; 48 | 49 | // argsの指定されている引数がRoleであるものを取得 50 | const role = args.find((arg) => arg.name.value === "role")?.value; 51 | 52 | // ロールが指定されていない場合は 53 | if (!role) { 54 | // エラーを返す 55 | throw new GraphQLErrorWithCode("authz_role_not_found"); 56 | } 57 | 58 | // ロールが指定されている場合は 59 | if (role.kind === Kind.ENUM) { 60 | // ロールの値を取得 61 | const roleValue = role.value; 62 | 63 | // ユーザーのロールが指定されたロールと一致する場合は 64 | if (user.role === (roleValue as Role)) { 65 | // 成功とする 66 | return; 67 | } else { 68 | // 失敗とする 69 | throw new GraphQLErrorWithCode("authz_failed"); 70 | } 71 | } 72 | }; 73 | 74 | // オプションを定義 75 | export const authzOption: GenericAuthPluginOptions = { 76 | resolveUserFn, 77 | validateUser: validateUserFn, 78 | mode: "protect-granular", 79 | }; 80 | -------------------------------------------------------------------------------- /packages/frontend/src/features/users/components/UserDetailBioInput.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { ModalContent, ModalBody, ModalHeader, ModalFooter, Button, Textarea } from "@nextui-org/react"; 3 | import toast from "react-hot-toast"; 4 | import { useMutation } from "urql"; 5 | import { graphql } from "src/lib/generated/gql"; 6 | 7 | // 自分のプロフィールの自己紹介文を更新するためのミューテーションを定義 8 | const UpdateMyBioMutation = graphql(` 9 | mutation UpdateMyBioMutation($input: UpdateUserInput!) { 10 | updateMyUser(input: $input) { 11 | bio 12 | } 13 | } 14 | `); 15 | 16 | const UserDetailBioInput = ({ bio, onClose }: { bio: string; onClose: () => void }) => { 17 | // フォームの入力値を取得するための参照を取得するフックを実行 18 | const input_ref = useRef(null); 19 | 20 | // 自己紹介文のミューテーション用のフックを実行 21 | const [, update_my_profile] = useMutation(UpdateMyBioMutation); 22 | 23 | // フォームが送信されたときの処理 24 | const handle_submit = async (e: React.FormEvent) => { 25 | e.preventDefault(); 26 | 27 | // 参照が取得できなかった場合はエラーを表示 28 | if (input_ref === null || input_ref.current === null) { 29 | toast.error("エラーが発生しました。"); 30 | return; 31 | } 32 | 33 | // 50文字以上の場合はエラーを表示 34 | if (input_ref.current?.value?.length > 400) { 35 | toast.error("自己紹介文は400文字以内で入力してください。"); 36 | return; 37 | } 38 | 39 | // ミューテーションを実行 40 | const result = await update_my_profile({ 41 | input: { 42 | bio: input_ref.current.value, 43 | }, 44 | }); 45 | 46 | if (result.error) { 47 | toast.error("エラーが発生しました。"); 48 | return; 49 | } 50 | 51 | toast.success("自己紹介文を更新しました。"); 52 | onClose(); 53 | return; 54 | }; 55 | 56 | return ( 57 | 58 |
59 | 自己紹介文を編集 60 | 61 |