diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..30790fe --- /dev/null +++ b/.env.local @@ -0,0 +1,6 @@ +AUTH_KEYCLOAK_ID=verity_client +AUTH_KEYCLOAK_SECRET=veritysecret +AUTH_KEYCLOAK_ISSUER=http://localhost:8080/realms/verity +AUTH_SECRET=authsecret + +DATABASE_URL=postgresql://verity:verity@localhost:5432/verity diff --git a/README.md b/README.md index 9406186..7b8a470 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,58 @@ Verity is a service that allows you to create new versions of various applications and connect them to each other. Manage your applications easily and efficiently. + +## Local Development + +### Prerequisites + +- [Docker](https://www.docker.com/) +- [Docker Compose](https://docs.docker.com/compose/) +- [Node.js LTS](https://nodejs.org/en/) + +### Getting Started + +1. Clone the repository: + +```bash +git clone git@github.com:platacard/verity.git +``` + +or + +```bash +git clone https://github.com/platacard/verity.git +``` + +2. Change to the project directory: + +```bash +cd verity +``` + +3. Start the development environment: + +```bash +docker-compose up -d +``` + +4. Install the dependencies: + +```bash +npm install +``` + +5. Start the development server: + +```bash +npm start +``` + +6. Open your browser and navigate to [http://localhost:3000](http://localhost:3000). + +7. Login with the following credentials: + +- **Username:** verity +- **Password:** verity + +8. You're all set! 🚀 diff --git a/apps/web/app/api/auth/[...nextauth]/route.ts b/apps/web/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..6dd17dd --- /dev/null +++ b/apps/web/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from '@verity/auth/server'; + +export const { GET, POST } = handlers; diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 59d3022..f118968 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,11 +1,18 @@ import Link from 'next/link'; +import { redirect } from 'next/navigation'; import { MountainIcon } from 'lucide-react'; +import { auth } from '@verity/auth/server'; import { Button } from '@verity/ui/button'; import { Input } from '@verity/ui/input'; -export default function Component() { +export default async function Component() { + const session = await auth(); + if (!session) { + redirect('/api/auth/signin'); + } + return (
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts new file mode 100644 index 0000000..f1b7c2c --- /dev/null +++ b/apps/web/middleware.ts @@ -0,0 +1,10 @@ +import NextAuth from 'next-auth'; +import Keycloak from 'next-auth/providers/keycloak'; + +export const middleware = NextAuth({ + providers: [Keycloak], +}).auth; + +export const config = { + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], +}; diff --git a/apps/web/project.json b/apps/web/project.json index 9e8f5ac..3992130 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -5,5 +5,9 @@ "projectType": "application", "tags": [], "// targets": "to see all targets run: nx show project web --web", - "targets": {} + "targets": { + "dev": { + "dependsOn": ["verity:prisma:migrate:dev"] + } + } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..88edf30 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,63 @@ +version: '3.8' + +services: + keycloakdb: + image: postgres:latest + container_name: postgres + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: keycloak + volumes: + - keycloakdb_data:/var/lib/postgresql/data + networks: + - keycloak-network + + keycloak: + image: quay.io/keycloak/keycloak:latest + container_name: keycloak + command: + - start-dev + - -Dkeycloak.migration.action=import + - -Dkeycloak.migration.provider=singleFile + - -Dkeycloak.migration.file=/opt/keycloak/data/import/realm-export.json + - -Dkeycloak.migration.strategy=IGNORE_EXISTING + environment: + DB_VENDOR: POSTGRES + DB_ADDR: keycloakdb + DB_DATABASE: keycloak + DB_USER: keycloak + DB_PASSWORD: keycloak + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KEYCLOAK_IMPORT: /opt/keycloak/data/import/realm-export.json + volumes: + - ./realm-export.json:/opt/keycloak/data/import/realm-export.json + ports: + - 8080:8080 + depends_on: + - keycloakdb + networks: + - keycloak-network + + verity_db: + image: postgres:latest + container_name: verity_db + environment: + POSTGRES_DB: verity + POSTGRES_USER: verity + POSTGRES_PASSWORD: verity + volumes: + - verity_db_data:/var/lib/postgresql/data + networks: + - keycloak-network + ports: + - 5432:5432 + +networks: + keycloak-network: + driver: bridge + +volumes: + keycloakdb_data: + verity_db_data: diff --git a/libs/auth/.eslintrc.json b/libs/auth/.eslintrc.json new file mode 100644 index 0000000..a39ac5d --- /dev/null +++ b/libs/auth/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/auth/README.md b/libs/auth/README.md new file mode 100644 index 0000000..910a0e9 --- /dev/null +++ b/libs/auth/README.md @@ -0,0 +1,7 @@ +# auth + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test auth` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/auth/project.json b/libs/auth/project.json new file mode 100644 index 0000000..06f038a --- /dev/null +++ b/libs/auth/project.json @@ -0,0 +1,9 @@ +{ + "name": "auth", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/auth/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project auth --web", + "targets": {} +} diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts new file mode 100644 index 0000000..e13d2ba --- /dev/null +++ b/libs/auth/src/index.ts @@ -0,0 +1 @@ +export { signIn, signOut } from 'next-auth/react'; diff --git a/libs/auth/src/lib/auth.ts b/libs/auth/src/lib/auth.ts new file mode 100644 index 0000000..fd8ab72 --- /dev/null +++ b/libs/auth/src/lib/auth.ts @@ -0,0 +1,13 @@ +import NextAuth from 'next-auth'; +import Keycloak from 'next-auth/providers/keycloak'; + +import { PrismaAdapter } from '@auth/prisma-adapter'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export const { handlers, signIn, signOut, auth } = NextAuth({ + adapter: PrismaAdapter(prisma), + providers: [Keycloak], + session: { strategy: 'jwt' }, +}); diff --git a/libs/auth/src/server.ts b/libs/auth/src/server.ts new file mode 100644 index 0000000..de7e0fb --- /dev/null +++ b/libs/auth/src/server.ts @@ -0,0 +1,3 @@ +import 'server-only'; + +export * from './lib/auth'; diff --git a/libs/auth/tsconfig.json b/libs/auth/tsconfig.json new file mode 100644 index 0000000..95cfeb2 --- /dev/null +++ b/libs/auth/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/libs/auth/tsconfig.lib.json b/libs/auth/tsconfig.lib.json new file mode 100644 index 0000000..08e579b --- /dev/null +++ b/libs/auth/tsconfig.lib.json @@ -0,0 +1,25 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "next", + "@nx/next/typings/image.d.ts" + ] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/package-lock.json b/package-lock.json index ec24d10..2685acf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,10 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@auth/prisma-adapter": "^2.1.0", "@hookform/resolvers": "^3.4.2", + "@prisma/adapter-pg": "^5.14.0", + "@prisma/client": "^5.14.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.3", @@ -45,12 +48,15 @@ "input-otp": "^1.2.4", "lucide-react": "^0.381.0", "next": "14.2.3", + "next-auth": "^5.0.0-beta.18", "next-themes": "^0.3.0", + "pg": "^8.11.5", "react": "18.3.1", "react-day-picker": "^8.10.1", "react-dom": "18.3.1", "react-hook-form": "^7.51.5", "react-resizable-panels": "^2.0.19", + "server-only": "^0.0.1", "sonner": "^1.4.41", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", @@ -82,6 +88,7 @@ "@testing-library/react": "^15.0.7", "@types/jest": "^29.4.0", "@types/node": "18.16.9", + "@types/pg": "^8.11.6", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "^7.3.0", @@ -105,6 +112,7 @@ "prettier": "^3.2.5", "prettier-plugin-packagejson": "^2.5.0", "prettier-plugin-tailwindcss": "^0.6.0", + "prisma": "^5.14.0", "tailwindcss": "3.4.3", "ts-jest": "^29.1.0", "ts-node": "10.9.1", @@ -141,6 +149,47 @@ "node": ">=6.0.0" } }, + "node_modules/@auth/core": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.31.0.tgz", + "integrity": "sha512-UKk3psvA1cRbk4/c9CkpWB8mdWrkKvzw0DmEYRsWolUQytQ2cRqx+hYuV6ZCsngw/xbj9hpmkZmAZEyq2g4fMg==", + "dependencies": { + "@panva/hkdf": "^1.1.1", + "@types/cookie": "0.6.0", + "cookie": "0.6.0", + "jose": "^5.1.3", + "oauth4webapi": "^2.4.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.1.0.tgz", + "integrity": "sha512-x1gYsi8xCFdxpEM6pxhh7OTV+VHB3PgID2L18y8F1kXu+PbcEWt4VZSQ8zk6dI/4YRStcuwQdHe7neCpczr0mg==", + "dependencies": { + "@auth/core": "0.31.0" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", @@ -4816,6 +4865,14 @@ "yargs-parser": "21.1.1" } }, + "node_modules/@panva/hkdf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz", + "integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@phenomnomnominal/tsquery": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", @@ -4864,6 +4921,87 @@ "node": ">=16" } }, + "node_modules/@prisma/adapter-pg": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-5.14.0.tgz", + "integrity": "sha512-AcV1DKY4ps3zvBIKolCzmBD+resdG4oH14I0iG3SntMjjnCbC/COefIInSV038c8g20RoHJO7fPyf1WPKicRgQ==", + "dependencies": { + "@prisma/driver-adapter-utils": "5.14.0", + "postgres-array": "3.0.2" + }, + "peerDependencies": { + "pg": "^8.11.3" + } + }, + "node_modules/@prisma/client": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.14.0.tgz", + "integrity": "sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.14.0.tgz", + "integrity": "sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==" + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-5.14.0.tgz", + "integrity": "sha512-EyAfdKjk0M7CaA7BfiwDTPLs7udOwt+RUnyWIPSg7uBEFp2GS/l9Ig7CCgMZFcBFc62v0c6Z/R3WFuQ+bNxfnA==", + "dependencies": { + "@prisma/debug": "5.14.0" + } + }, + "node_modules/@prisma/engines": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.14.0.tgz", + "integrity": "sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.14.0", + "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/fetch-engine": "5.14.0", + "@prisma/get-platform": "5.14.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz", + "integrity": "sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz", + "integrity": "sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.14.0", + "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/get-platform": "5.14.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.14.0.tgz", + "integrity": "sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.14.0" + } + }, "node_modules/@radix-ui/number": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", @@ -7080,6 +7218,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -7265,6 +7408,65 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, + "node_modules/@types/pg": { + "version": "8.11.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", + "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -9917,7 +10119,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -16012,6 +16213,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.3.0.tgz", + "integrity": "sha512-IChe9AtAE79ru084ow8jzkN2lNrG3Ntfiv65Cvj9uOCE2m5LNsdHG+9EbxWxAoWRF9TgDOqLN5jm08++owDVRg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17430,6 +17639,32 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.18", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.18.tgz", + "integrity": "sha512-x55L8wZb8PcPGCYA3e/l9tdpd7YL3FDuhas4W8pxq3PjrWJ9OoDxNN0otK9axJamJBbBgjfzTJjVQB6hXoe0ZQ==", + "dependencies": { + "@auth/core": "0.31.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14", + "nodemailer": "^6.6.5", + "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", @@ -17696,6 +17931,14 @@ } } }, + "node_modules/oauth4webapi": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.10.4.tgz", + "integrity": "sha512-DSoj8QoChzOCQlJkRmYxAJCIpnXFW32R0Uq7avyghIeB6iJq0XAblOD7pcq3mx4WEBDwMuKr0Y1qveCBleG2Xw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -18187,6 +18430,104 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/pg": { + "version": "8.11.5", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz", + "integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -18984,6 +19325,72 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, + "node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/preact-render-to-string/node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -19126,6 +19533,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.14.0.tgz", + "integrity": "sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.14.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/proc-log": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", @@ -20236,6 +20659,11 @@ "node": ">= 0.8.0" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -20637,7 +21065,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, "engines": { "node": ">= 10.x" } @@ -22889,6 +23316,14 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index b61b1c2..08d175f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,10 @@ "start": "nx dev web" }, "dependencies": { + "@auth/prisma-adapter": "^2.1.0", "@hookform/resolvers": "^3.4.2", + "@prisma/adapter-pg": "^5.14.0", + "@prisma/client": "^5.14.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.3", @@ -44,12 +47,15 @@ "input-otp": "^1.2.4", "lucide-react": "^0.381.0", "next": "14.2.3", + "next-auth": "^5.0.0-beta.18", "next-themes": "^0.3.0", + "pg": "^8.11.5", "react": "18.3.1", "react-day-picker": "^8.10.1", "react-dom": "18.3.1", "react-hook-form": "^7.51.5", "react-resizable-panels": "^2.0.19", + "server-only": "^0.0.1", "sonner": "^1.4.41", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", @@ -81,6 +87,7 @@ "@testing-library/react": "^15.0.7", "@types/jest": "^29.4.0", "@types/node": "18.16.9", + "@types/pg": "^8.11.6", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "^7.3.0", @@ -104,6 +111,7 @@ "prettier": "^3.2.5", "prettier-plugin-packagejson": "^2.5.0", "prettier-plugin-tailwindcss": "^0.6.0", + "prisma": "^5.14.0", "tailwindcss": "3.4.3", "ts-jest": "^29.1.0", "ts-node": "10.9.1", diff --git a/prisma/migrations/20240531202421_auth/migration.sql b/prisma/migrations/20240531202421_auth/migration.sql new file mode 100644 index 0000000..62283b4 --- /dev/null +++ b/prisma/migrations/20240531202421_auth/migration.sql @@ -0,0 +1,81 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Account" ( + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId") +); + +-- CreateTable +CREATE TABLE "Session" ( + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token") +); + +-- CreateTable +CREATE TABLE "Authenticator" ( + "credentialID" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "credentialPublicKey" TEXT NOT NULL, + "counter" INTEGER NOT NULL, + "credentialDeviceType" TEXT NOT NULL, + "credentialBackedUp" BOOLEAN NOT NULL, + "transports" TEXT, + + CONSTRAINT "Authenticator_pkey" PRIMARY KEY ("userId","credentialID") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Authenticator" ADD CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..329b3b4 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,78 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id String @id @default(cuid()) + name String? + email String @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + // Optional for WebAuthn support + Authenticator Authenticator[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Account { + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([provider, providerAccountId]) +} + +model Session { + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model VerificationToken { + identifier String + token String + expires DateTime + + @@id([identifier, token]) +} + +// Optional for WebAuthn support +model Authenticator { + credentialID String @unique + userId String + providerAccountId String + credentialPublicKey String + counter Int + credentialDeviceType String + credentialBackedUp Boolean + transports String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([userId, credentialID]) +} diff --git a/project.json b/project.json index 0623223..48ea0be 100644 --- a/project.json +++ b/project.json @@ -2,5 +2,12 @@ "name": "verity", "$schema": "./node_modules/nx/schemas/project-schema.json", "tags": [], - "targets": {} + "targets": { + "prisma:migrate:dev": { + "command": "npx prisma migrate dev" + }, + "prisma:generate": { + "command": "npx prisma generate" + } + } } diff --git a/realm-export.json b/realm-export.json new file mode 100644 index 0000000..47a6380 --- /dev/null +++ b/realm-export.json @@ -0,0 +1,31 @@ +{ + "id": "verity", + "realm": "verity", + "enabled": true, + "clients": [ + { + "clientId": "verity_client", + "secret": "veritysecret", + "enabled": true, + "protocol": "openid-connect", + "redirectUris": ["http://localhost:3000/*"] + } + ], + "users": [ + { + "username": "verity", + "enabled": true, + "emailVerified": true, + "firstName": "Rootus", + "lastName": "Adminus", + "email": "verity@mail.com", + "credentials": [ + { + "type": "password", + "value": "verity" + } + ], + "realmRoles": ["ADMIN"] + } + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 75007dc..669fee5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,8 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@verity/auth": ["libs/auth/src/index.ts"], + "@verity/auth/server": ["libs/auth/src/server.ts"], "@verity/ui": ["libs/ui/src/index.ts"], "@verity/ui/*": ["libs/ui/src/lib/*"], "@verity/ui/server": ["libs/ui/src/server.ts"],