feat: Add Client pages

This commit is contained in:
Tuluobo 2024-09-08 20:21:02 +08:00
parent c311aeb18e
commit b92fe7d572
17 changed files with 984 additions and 23 deletions

View File

@ -18,12 +18,17 @@
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^2.4.2", "@auth/prisma-adapter": "^2.4.2",
"@prisma/client": "^5.19.0", "@prisma/client": "^5.19.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toast": "^1.2.1",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"framer-motion": "^11.5.4",
"lucide-react": "^0.437.0", "lucide-react": "^0.437.0",
"next": "14.2.7", "next": "14.2.7",
"next-auth": "5.0.0-beta.20", "next-auth": "5.0.0-beta.20",

165
pnpm-lock.yaml generated
View File

@ -14,12 +14,24 @@ importers:
'@prisma/client': '@prisma/client':
specifier: ^5.19.0 specifier: ^5.19.0
version: 5.19.0(prisma@5.19.0) version: 5.19.0(prisma@5.19.0)
'@radix-ui/react-checkbox':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dropdown-menu': '@radix-ui/react-dropdown-menu':
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-label':
specifier: ^2.1.0
version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': '@radix-ui/react-slot':
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0(@types/react@18.3.5)(react@18.3.1) version: 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-switch':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-toast': '@radix-ui/react-toast':
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -32,6 +44,9 @@ importers:
crypto-js: crypto-js:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0 version: 4.2.0
framer-motion:
specifier: ^11.5.4
version: 11.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
lucide-react: lucide-react:
specifier: ^0.437.0 specifier: ^0.437.0
version: 0.437.0(react@18.3.1) version: 0.437.0(react@18.3.1)
@ -403,6 +418,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-checkbox@1.1.1':
resolution: {integrity: sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.0': '@radix-ui/react-collection@1.1.0':
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
peerDependencies: peerDependencies:
@ -434,6 +462,19 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-dialog@1.1.1':
resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.0': '@radix-ui/react-direction@1.1.0':
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
peerDependencies: peerDependencies:
@ -500,6 +541,19 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-label@2.1.0':
resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-menu@2.1.1': '@radix-ui/react-menu@2.1.1':
resolution: {integrity: sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==} resolution: {integrity: sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==}
peerDependencies: peerDependencies:
@ -587,6 +641,19 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-switch@1.1.0':
resolution: {integrity: sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-toast@1.2.1': '@radix-ui/react-toast@1.2.1':
resolution: {integrity: sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==} resolution: {integrity: sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==}
peerDependencies: peerDependencies:
@ -636,6 +703,15 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-use-previous@1.1.0':
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-rect@1.1.0': '@radix-ui/react-use-rect@1.1.0':
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
peerDependencies: peerDependencies:
@ -1262,6 +1338,20 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'} engines: {node: '>=14'}
framer-motion@11.5.4:
resolution: {integrity: sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fs.realpath@1.0.0: fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@ -2759,6 +2849,22 @@ snapshots:
'@types/react': 18.3.5 '@types/react': 18.3.5
'@types/react-dom': 18.3.0 '@types/react-dom': 18.3.0
'@radix-ui/react-checkbox@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.0(@types/react@18.3.5)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.5
'@types/react-dom': 18.3.0
'@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1)
@ -2783,6 +2889,28 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.5 '@types/react': 18.3.5
'@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@18.3.1)
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.5.7(@types/react@18.3.5)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.5
'@types/react-dom': 18.3.0
'@radix-ui/react-direction@1.1.0(@types/react@18.3.5)(react@18.3.1)': '@radix-ui/react-direction@1.1.0(@types/react@18.3.5)(react@18.3.1)':
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@ -2841,6 +2969,15 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.5 '@types/react': 18.3.5
'@radix-ui/react-label@2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.5
'@types/react-dom': 18.3.0
'@radix-ui/react-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.0 '@radix-ui/primitive': 1.1.0
@ -2938,6 +3075,21 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.5 '@types/react': 18.3.5
'@radix-ui/react-switch@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.0(@types/react@18.3.5)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.5
'@types/react-dom': 18.3.0
'@radix-ui/react-toast@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-toast@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.0 '@radix-ui/primitive': 1.1.0
@ -2984,6 +3136,12 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.5 '@types/react': 18.3.5
'@radix-ui/react-use-previous@1.1.0(@types/react@18.3.5)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.5
'@radix-ui/react-use-rect@1.1.0(@types/react@18.3.5)(react@18.3.1)': '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.5)(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/rect': 1.1.0 '@radix-ui/rect': 1.1.0
@ -3800,6 +3958,13 @@ snapshots:
cross-spawn: 7.0.3 cross-spawn: 7.0.3
signal-exit: 4.1.0 signal-exit: 4.1.0
framer-motion@11.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
tslib: 2.7.0
optionalDependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
fs.realpath@1.0.0: {} fs.realpath@1.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:

View File

@ -1,22 +0,0 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"avatarUrl" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'USER',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");

View File

@ -0,0 +1,83 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"avatarUrl" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'USER',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "clients" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"redirectUri" TEXT NOT NULL,
"home" TEXT NOT NULL,
"logo" TEXT NOT NULL,
"description" TEXT,
"clientId" TEXT NOT NULL,
"clientSecret" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "clients_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "codes" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"userId" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
CONSTRAINT "codes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "access_tokens" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"userId" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
CONSTRAINT "access_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "clients_clientId_key" ON "clients"("clientId");
-- CreateIndex
CREATE UNIQUE INDEX "codes_code_key" ON "codes"("code");
-- CreateIndex
CREATE UNIQUE INDEX "access_tokens_token_key" ON "access_tokens"("token");
-- AddForeignKey
ALTER TABLE "clients" ADD CONSTRAINT "clients_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "codes" ADD CONSTRAINT "codes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "codes" ADD CONSTRAINT "codes_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "access_tokens" ADD CONSTRAINT "access_tokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "access_tokens" ADD CONSTRAINT "access_tokens_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -20,8 +20,60 @@ model User {
avatarUrl String? avatarUrl String?
role UserRole @default(USER) role UserRole @default(USER)
clients Client[]
codes Code[]
accessTokens AccessToken[]
createdAt DateTime @default(now()) @map(name: "created_at") createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at") updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "users") @@map("users")
}
model Client {
id String @id @default(cuid())
name String
redirectUri String
home String
logo String
description String?
clientId String @unique
clientSecret String
userId String
user User @relation(fields: [userId], references: [id])
authCodes Code[]
accessTokens AccessToken[]
@@map("clients")
}
model Code {
id String @id @default(cuid())
code String @unique
expiresAt DateTime
userId String
user User @relation(fields: [userId], references: [id])
clientId String
client Client @relation(fields: [clientId], references: [id])
@@map("codes")
}
model AccessToken {
id String @id @default(cuid())
token String @unique
expiresAt DateTime
userId String
user User @relation(fields: [userId], references: [id])
clientId String
client Client @relation(fields: [clientId], references: [id])
@@map("access_tokens")
} }

51
src/actions/add-client.ts Normal file
View File

@ -0,0 +1,51 @@
"use server";
import { prisma } from "@/lib/prisma";
import { getCurrentUser } from "@/lib/session";
import { generateClientKeyId, generateSecretKey } from "@/lib/utils";
export async function AddClientAction(formData: FormData) {
const name = formData.get("name") as string;
const home = formData.get("home") as string;
const logo = formData.get("logo") as string;
const redirectUri = formData.get("redirectUri") as string;
const description = formData.get("description") as string;
const user = await getCurrentUser();
// Generate a unique client ID and secret
let clientId = generateClientKeyId();
while (await findClientByClientId(clientId)) {
clientId = generateClientKeyId();
}
const clientSecret = generateSecretKey();
try {
const newClient = await prisma.client.create({
data: {
name,
home,
logo,
redirectUri,
description,
clientId,
clientSecret,
userId: user?.id,
},
});
console.log("New client created:", newClient);
return { success: true, client: newClient };
} catch (error) {
console.error("Error creating client:", error);
return { success: false, error: "Failed to create client" };
}
}
async function findClientByClientId(clientId: string) {
return await prisma.client.findUnique({
where: {
clientId,
},
});
}

View File

@ -0,0 +1,82 @@
import { prisma } from "@/lib/prisma";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { AddClientButton } from "@/components/clients/add-client";
// 创建 Prisma 客户端实例
async function fetchClients() {
return await prisma.client.findMany({
select: {
id: true,
name: true,
clientId: true,
redirectUri: true,
user: {
select: {
name: true,
email: true,
},
},
},
});
}
export default async function ClientsPage() {
const clients = await fetchClients();
return (
<div className="container mx-auto py-10">
<div className="mb-5 flex items-center justify-between">
<h1 className="text-3xl font-bold">Clients</h1>
<div className="mb-4">
{/* <Input
type="text"
placeholder="Search clients..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="max-w-sm"
/> */}
</div>
<AddClientButton />
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Client ID</TableHead>
<TableHead>Redirect URI</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{clients.map((client) => (
<TableRow key={client.id}>
<TableCell>{client.name}</TableCell>
<TableCell>{client.clientId}</TableCell>
<TableCell>{client.redirectUri}</TableCell>
<TableCell>
{client.user.name} ({client.user.email})
</TableCell>
<TableCell>
<Button variant="outline" size="sm" className="mr-2">
Edit
</Button>
<Button variant="destructive" size="sm">
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

View File

@ -13,6 +13,14 @@ export default function DashboardPage() {
Home Home
</a> </a>
</li> </li>
<li className="mb-2">
<a
href="/dashboard/clients"
className="block rounded px-4 py-2 hover:bg-gray-700"
>
Client
</a>
</li>
</ul> </ul>
</nav> </nav>

View File

@ -0,0 +1,81 @@
"use client";
import { useState } from "react";
import { AddClientAction } from "@/actions/add-client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function AddClientButton() {
const [isAddClientOpen, setIsAddClientOpen] = useState(false);
return (
<Dialog open={isAddClientOpen} onOpenChange={setIsAddClientOpen}>
<DialogTrigger asChild>
<Button>Add Client</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Client</DialogTitle>
</DialogHeader>
<form action={AddClientAction}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" name="name" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="home" className="text-right">
Home Page
</Label>
<Input id="home" name="home" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="logo" className="text-right">
Logo
</Label>
<Input id="logo" name="logo" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Input
id="description"
name="description"
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="redirectUri" className="text-right">
Redirect URI
</Label>
<Input
id="redirectUri"
name="redirectUri"
className="col-span-3"
/>
</div>
</div>
<div className="flex justify-end">
<Button type="submit">Add Client</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,86 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@ -0,0 +1,122 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@ -0,0 +1,25 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@ -0,0 +1,26 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@ -0,0 +1,29 @@
"use client";
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

117
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,117 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@ -1,6 +1,27 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import WordArray from "crypto-js/lib-typedarrays";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export function generateClientKeyId(length: number = 16): string {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
let result = "";
const randomValues = WordArray.random(length);
for (let i = 0; i < length; i++) {
const byte = (randomValues.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
result += characters[byte % charactersLength];
}
return result;
}
export function generateSecretKey(length: number = 32): string {
const buffer = WordArray.random(length);
return buffer.toString();
}