Compare commits
330 Commits
616b1ad389
...
v1.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d4e600be5 | |||
| 610a0fc657 | |||
| be75bb7bc1 | |||
| 3ef9285278 | |||
| 3e156d3f5d | |||
| f82fc0fb77 | |||
| d40392745a | |||
| edc605fb62 | |||
| a38463837f | |||
| 38fa0a0a07 | |||
| d4679f3733 | |||
| 5ae62d5d22 | |||
| 063181da5a | |||
| 0eed6cfdbf | |||
| e6087f43d0 | |||
| da98961b8b | |||
| 6d3efa57ca | |||
| d1d1870100 | |||
| 0758b9f75a | |||
| 6e73220962 | |||
| 8f2df85208 | |||
| 1bd717f84f | |||
| bb9fa3bcaa | |||
| bf3b2f7a94 | |||
| 325c2e3a87 | |||
| c262778373 | |||
| c782145e7e | |||
| cb2258aa64 | |||
| da16bc1dbe | |||
| 43e814fbe7 | |||
| 4c9505d476 | |||
| b79c84a004 | |||
| 70b90c3ed7 | |||
| 83b15ddf37 | |||
| 0f4d6be36f | |||
| a1a16afb76 | |||
| 70c89b783a | |||
| bd862e54fa | |||
| af0e9c6522 | |||
| acaf14c403 | |||
| f62e2ad2a6 | |||
| ec090e3e20 | |||
| 4e306adc9f | |||
| 38fa850e38 | |||
| 041e27c87d | |||
| 6d09087289 | |||
| 7e05789fe5 | |||
| e418476b20 | |||
| 90a67b681e | |||
| e777afc433 | |||
| 749cb2e13b | |||
| 7b7f940a60 | |||
| 0461c0ce70 | |||
| 613f9e1a8c | |||
| 2c75d6de4a | |||
| 65db82ce60 | |||
| 19dc49b10d | |||
| 3211e25bd6 | |||
| 359ab3b072 | |||
| b16e454058 | |||
| 2698d5f133 | |||
| e0b80ea422 | |||
| 448bed89bd | |||
| accd5b8754 | |||
| 73e409ce84 | |||
| 3821ef6657 | |||
| c872b55083 | |||
| c2914d8e29 | |||
| 0ebda96d62 | |||
| 1baff0712a | |||
| 4d9245aabb | |||
| 263ad2a0ae | |||
| 4bace58823 | |||
| d5d799b425 | |||
| b0a8ac9d66 | |||
| ef7dae1bae | |||
| e25c4f0455 | |||
| f1f32f03ec | |||
| cbd4ca5686 | |||
| 8992963da8 | |||
| 6f172c6cb6 | |||
| 02a73a5e33 | |||
| 44a1da5be2 | |||
| b4ade3bdd1 | |||
| 5d9dfe7382 | |||
| 393377b5da | |||
| 42e9bd0f0b | |||
| 7730ae7981 | |||
| 3f0e281d42 | |||
| 327f49bff0 | |||
| e1606b707e | |||
| 39f3e6b93c | |||
| 4f5f771e2b | |||
| e782b926a4 | |||
| dc0a8a1071 | |||
| 7ab982e4fd | |||
| 7a612f8480 | |||
| c6cc3a7098 | |||
| 088e168460 | |||
| b6d9a518e8 | |||
| 96e8d0e090 | |||
| 5e7ed3d6f7 | |||
| 134335021d | |||
| b764d3c932 | |||
| 367ec1d9a0 | |||
| af3b4b0cc3 | |||
| fcec99f1c5 | |||
| 7d065d1264 | |||
| 51ed3a7747 | |||
| 92a3510547 | |||
| ee4feb57c5 | |||
| 5159fa3606 | |||
| 53665f8847 | |||
| 30224774e6 | |||
| e3ca7ac027 | |||
| bb054f7f5a | |||
| 2a1e0d45dd | |||
| 8e0d7dc873 | |||
| 259fae4c63 | |||
| 9133b45744 | |||
| eb4301ba98 | |||
| 7f441f5126 | |||
| 054d505117 | |||
| 26142f9a71 | |||
| 11d503ab49 | |||
| 748ea70c0d | |||
| f1876e19bf | |||
| 17bcb8787a | |||
| c17108e094 | |||
| 5c29938730 | |||
| f16cbe5443 | |||
| f49a1503f0 | |||
| 4f782e4cea | |||
| 2dd088fdf3 | |||
| e5c0c354e5 | |||
| 21cee7c3f1 | |||
| 369fe28c5c | |||
| 17c065ae79 | |||
| 7bbcf2fac7 | |||
| d281a6c804 | |||
| 2f86362f4b | |||
| 448a7b48ba | |||
| a2972de417 | |||
| 98745895eb | |||
| 87d6c738c7 | |||
| 2f9b922485 | |||
| d2287bc363 | |||
| 8e33f1b61b | |||
| c2e5ab51df | |||
| b6f1495981 | |||
| 4df7de91d0 | |||
| c5463e9ffe | |||
| 96a76568cc | |||
| 9206b7fcc0 | |||
| 464931cc98 | |||
| 9e09a7bc72 | |||
| 3b2f4f1f40 | |||
| a661827842 | |||
| 9570fb4524 | |||
| 8039a3571d | |||
| c6471cc169 | |||
| 8645380fbf | |||
| 37988b4582 | |||
| f5f80385ad | |||
| 97352e26b2 | |||
| 6a44e902fd | |||
| 1246613fb1 | |||
| d2744689b2 | |||
| b75c4fb551 | |||
| 166201371c | |||
| d03ce79653 | |||
| 9f60ea9228 | |||
| fda2eb01ec | |||
| dda4f8da05 | |||
| 4d21045303 | |||
| 5316d922ab | |||
| 5dd0d7c2f3 | |||
| d4c40db011 | |||
| 7985b141c0 | |||
| 1d434f03dd | |||
| 062719adce | |||
| e954f2fe76 | |||
| eef23909f4 | |||
| c2868b5128 | |||
| 36246d3263 | |||
| ebf48b6062 | |||
| 641f08a042 | |||
| e67a7f2f01 | |||
| c548add86e | |||
| aa33643982 | |||
| 77edc576ea | |||
| 6a1ef7b409 | |||
| 1be4fea2f5 | |||
| c9fab9fa8f | |||
| 016ba13466 | |||
| d46b5a36b1 | |||
| 5975b71dcc | |||
| 78fe191845 | |||
| 2c53203473 | |||
| bcecdcf9ef | |||
| 5c956ba949 | |||
| c161a6d298 | |||
| 842d834c6d | |||
| 1416043529 | |||
| 3a10e6abb7 | |||
| 8fbbcabc57 | |||
| 1a57c5ca49 | |||
| 870b04bb28 | |||
| 777d36617f | |||
| 440732bfb9 | |||
| 2e3aed0038 | |||
| 72f6893660 | |||
| b0728746cd | |||
| 6a46d6dce7 | |||
| 25ec96492f | |||
| 28a113c132 | |||
| ebf6530c1f | |||
| bb83a87748 | |||
| 52cbe2a7ed | |||
| 1d7e2dc5b4 | |||
| 58f5a715f2 | |||
| 5c0951fc64 | |||
| 9af60b0dbe | |||
| 3d729996bb | |||
| 65927214ed | |||
| 35f11ccb83 | |||
| 30f84486fa | |||
| 19ba954c6b | |||
| 2589b24673 | |||
| e3eef8e23f | |||
| 2bc3e5221d | |||
| 96ff60e3ec | |||
| 923f499709 | |||
| 87977298e9 | |||
| 539050629d | |||
| ed64e2b846 | |||
| 8061f3d292 | |||
| 702869e1f8 | |||
| 46396153e3 | |||
| b707b27020 | |||
| 5c7bd5bb74 | |||
| 0ebcb79259 | |||
| f990d81ca2 | |||
| 07b95549fd | |||
| 53e63f4d6a | |||
| 65a2ad75dd | |||
| 607f0a7a4d | |||
| 8399c9e8a7 | |||
| 07ee1f906f | |||
| 4c3731da70 | |||
| e856af135c | |||
| ed5be9ec39 | |||
| 19ef964c08 | |||
| f2bb14bb17 | |||
| 840b427a7e | |||
| 9adcc812a3 | |||
| 57e8d9ac21 | |||
| 35fcf700e2 | |||
| 09df4d6475 | |||
| c477af2fad | |||
| 3d56faa361 | |||
| fc28b9cf04 | |||
| d86a9bfe94 | |||
| d754ad1eba | |||
| 57997bf184 | |||
| f6720db786 | |||
| 83335d01ef | |||
| 670f4f9ea5 | |||
| ae15665b2f | |||
| e8bea369b8 | |||
| f9a7c0c5c4 | |||
| 8d301bdbd1 | |||
| 986fde4724 | |||
| d01efd710e | |||
| 36d33d9f11 | |||
| 7b5dacf723 | |||
| d41e43fcb8 | |||
| f183e7566e | |||
| 2eab11561d | |||
| 5b7c5d0f1d | |||
| 9815f0efdf | |||
| bfa5042f07 | |||
| 52a1cd2817 | |||
| b232d6648c | |||
| 672b4b771e | |||
| dbdf9e415e | |||
| 8ac701f214 | |||
| fdac3b1433 | |||
| 7e30cef008 | |||
| 1709552ed4 | |||
| 0d33e18a88 | |||
| d3106a6576 | |||
| ac86ab23ad | |||
| acbf404780 | |||
| d533bd528d | |||
| 55b631cd9c | |||
| 241834feb8 | |||
| 96e8afca57 | |||
| 8755df31c9 | |||
| f48395dda1 | |||
| c397f7b1e5 | |||
| 8ecf54d8af | |||
| 97b02556ba | |||
| 49415eb4db | |||
| a008ff96ed | |||
| 1cd705665e | |||
| 9b0cdf18f5 | |||
| 7d6e056e6b | |||
| 30e8e89422 | |||
| a154b3fa39 | |||
| c6ab1ea75d | |||
| c2956adb4b | |||
| 8ea3a7b25c | |||
| 29f2c09a69 | |||
| a69cd3af61 | |||
| 458b38b2d9 | |||
| 3b82fbbd1a | |||
| a5d1842895 | |||
| 3f01207c29 | |||
| 1da28cd78d | |||
| 2a9cc506f0 | |||
| 223fbf39b2 | |||
| 8a6ee6d98c | |||
| 39b93997c2 | |||
| 3a6e364c70 | |||
| 87166564fc | |||
| 2078b0a934 | |||
| f7ee30f5dd | |||
| 4d45b22592 | |||
| bc99fba385 |
@@ -1,80 +0,0 @@
|
|||||||
# .gitea/workflows/deploy.yml
|
|
||||||
name: Deploy to K3s
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: localhost:5000/tiny-ci-runner:latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
IMAGE_TAG: ${{ github.sha }}
|
|
||||||
KUBECONFIG: /tmp/.kube/config
|
|
||||||
NODE_ENV: production
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Write kubeconfig
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/.kube
|
|
||||||
cat << 'EOF' > /tmp/.kube/config
|
|
||||||
${{ secrets.KUBECONFIG_DATA }}
|
|
||||||
EOF
|
|
||||||
chmod 600 /tmp/.kube/config
|
|
||||||
|
|
||||||
- name: Verify Kubernetes access
|
|
||||||
run: |
|
|
||||||
kubectl cluster-info
|
|
||||||
kubectl get nodes
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
run: |
|
|
||||||
git clone --depth=1 --branch master \
|
|
||||||
https://git.tonesc.cn/tone/tonePage.git \
|
|
||||||
/workspace/tone/tonePage
|
|
||||||
cd /workspace/tone/tonePage
|
|
||||||
git log -1 --oneline
|
|
||||||
|
|
||||||
- name: Build and push backend image
|
|
||||||
run: |
|
|
||||||
cd /workspace/tone/tonePage/apps/backend
|
|
||||||
docker build -t localhost:5000/backend:${IMAGE_TAG} .
|
|
||||||
docker push localhost:5000/backend:${IMAGE_TAG}
|
|
||||||
|
|
||||||
- name: Build and push frontend image
|
|
||||||
run: |
|
|
||||||
cd /workspace/tone/tonePage/apps/frontend
|
|
||||||
docker build \
|
|
||||||
--build-arg API_BASE="http://backend-service:3001" \
|
|
||||||
-t localhost:5000/frontend:${IMAGE_TAG} .
|
|
||||||
docker push localhost:5000/frontend:${IMAGE_TAG}
|
|
||||||
|
|
||||||
- name: Deploy to K3s
|
|
||||||
run: |
|
|
||||||
cd /workspace/tone/tonePage/apps/deploy
|
|
||||||
|
|
||||||
# 基础资源
|
|
||||||
kubectl apply -f postgres-deployment.yaml
|
|
||||||
kubectl apply -f backend-deployment.yaml
|
|
||||||
kubectl apply -f frontend-deployment.yaml
|
|
||||||
|
|
||||||
# 更新镜像(触发滚动更新)
|
|
||||||
kubectl set image deployment/backend \
|
|
||||||
backend=localhost:5000/backend:${IMAGE_TAG}
|
|
||||||
|
|
||||||
kubectl set image deployment/frontend \
|
|
||||||
frontend=localhost:5000/frontend:${IMAGE_TAG}
|
|
||||||
|
|
||||||
# 等待滚动完成
|
|
||||||
kubectl rollout status deployment/backend --timeout=120s
|
|
||||||
kubectl rollout status deployment/frontend --timeout=120s
|
|
||||||
|
|
||||||
- name: Post-deploy sanity check
|
|
||||||
run: |
|
|
||||||
kubectl get pods
|
|
||||||
kubectl get svc
|
|
||||||
21
LICENSE
21
LICENSE
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 tonecn
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.next
|
|
||||||
.git
|
|
||||||
.env.local
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
FROM node:22-alpine AS builder
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN pnpm run build
|
|
||||||
|
|
||||||
RUN CI=true pnpm prune --prod
|
|
||||||
|
|
||||||
FROM node:22-alpine AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
COPY --from=builder /app/package.json ./package.json
|
|
||||||
|
|
||||||
EXPOSE 3001
|
|
||||||
CMD ["node", "dist/main.js"]
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
|
||||||
import { Resource } from "src/resource/entity/resource.entity";
|
|
||||||
import { Repository } from "typeorm";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AdminResourceService {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Resource)
|
|
||||||
private readonly resourceRepository: Repository<Resource>,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
|
|
||||||
async findAll() {
|
|
||||||
return this.resourceRepository.find({
|
|
||||||
order: {
|
|
||||||
updatedAt: 'DESC',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(id: string): Promise<Resource> {
|
|
||||||
return this.resourceRepository.findOne({ where: { id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(data: Partial<Resource>): Promise<Resource> {
|
|
||||||
const resource = this.resourceRepository.create(data);
|
|
||||||
return this.resourceRepository.save(resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(data: Partial<Resource>): Promise<Resource> {
|
|
||||||
// const updateRes = await this.resourceRepository.update(id, data);
|
|
||||||
// updateRes.affected
|
|
||||||
// return this.resourceRepository.findOne({ where: { id } });
|
|
||||||
return this.resourceRepository.save(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
|
||||||
await this.resourceRepository.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import {
|
|
||||||
BadRequestException,
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
Req,
|
|
||||||
Res,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { LoginByPasswordDto } from './dto/login.dto';
|
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { UserSessionService } from 'src/auth/service/user-session.service';
|
|
||||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
|
||||||
import { Request, Response } from 'express';
|
|
||||||
import { UserService } from 'src/user/user.service';
|
|
||||||
import { AuthGuard } from './guards/auth.guard';
|
|
||||||
import { SmsLoginDto } from './dto/sms-login.dto';
|
|
||||||
import { SmsService } from 'src/sms/sms.service';
|
|
||||||
import { UserSession } from 'src/auth/entity/user-session.entity';
|
|
||||||
import { PasskeyService } from './service/passkey.service';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { PasskeyLoginDto } from './dto/passkey-login.dto';
|
|
||||||
import { AuthUser, CurrentUser } from './decorator/current-user.decorator';
|
|
||||||
import { PasskeyRegisterDto } from './dto/passkey-register.dto';
|
|
||||||
|
|
||||||
@Controller('auth')
|
|
||||||
export class AuthController {
|
|
||||||
constructor(
|
|
||||||
private readonly authService: AuthService,
|
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly userSessionService: UserSessionService,
|
|
||||||
private readonly smsService: SmsService,
|
|
||||||
private readonly passkeyService: PasskeyService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
private setUserSession(res: Response, session: UserSession) {
|
|
||||||
res.cookie('session', session.sessionId, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
// 永不过期,不用设置maxAge
|
|
||||||
path: '/',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('login/password')
|
|
||||||
@UseGuards(ThrottlerGuard)
|
|
||||||
@Throttle({
|
|
||||||
'min': { limit: 5, ttl: 60 * 1000 },
|
|
||||||
'hour': { limit: 20, ttl: 60 * 60 * 1000 },
|
|
||||||
'day': { limit: 50, ttl: 24 * 60 * 60 * 1000 }
|
|
||||||
})
|
|
||||||
async loginByPassword(
|
|
||||||
@Body() loginDto: LoginByPasswordDto,
|
|
||||||
@Res({ passthrough: true }) res: Response,
|
|
||||||
) {
|
|
||||||
const { identifier, password } = loginDto;
|
|
||||||
const session = await this.authService.loginWithPassword(identifier, password);
|
|
||||||
this.setUserSession(res, session);
|
|
||||||
return {
|
|
||||||
user: await this.userService.findById(session.userId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('login/sms')
|
|
||||||
@UseGuards(ThrottlerGuard)
|
|
||||||
@Throttle({
|
|
||||||
'day': { limit: 50, ttl: 24 * 60 * 60 * 1000 }
|
|
||||||
})
|
|
||||||
async loginBySms(
|
|
||||||
@Body() dto: SmsLoginDto,
|
|
||||||
@Res({ passthrough: true }) res: Response,
|
|
||||||
) {
|
|
||||||
const { phone, code } = dto;
|
|
||||||
await this.smsService.checkSms(phone, 'login', code);
|
|
||||||
// 验证通过,(注册并)登陆
|
|
||||||
const session = await this.authService.loginWithPhone(phone);
|
|
||||||
this.setUserSession(res, session);
|
|
||||||
return {
|
|
||||||
user: await this.userService.findById(session.userId),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Post('passkey/login/options')
|
|
||||||
@UseGuards(ThrottlerGuard)
|
|
||||||
@Throttle({
|
|
||||||
'day': { limit: 20, ttl: 24 * 60 * 60 * 1000 }
|
|
||||||
})
|
|
||||||
async loginByPasskeyOptions(
|
|
||||||
@Res({ passthrough: true }) res: Response,
|
|
||||||
) {
|
|
||||||
const tempSessionId = uuidv4();
|
|
||||||
const options = await this.passkeyService.getAuthenticationOptions(tempSessionId);
|
|
||||||
|
|
||||||
res.cookie('passkey_temp_session', tempSessionId, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
path: '/api/auth/passkey/login',
|
|
||||||
maxAge: 1 * 60 * 1000,
|
|
||||||
});
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('passkey/login')
|
|
||||||
@UseGuards(ThrottlerGuard)
|
|
||||||
@Throttle({
|
|
||||||
'day': { limit: 20, ttl: 24 * 60 * 60 * 1000 }
|
|
||||||
})
|
|
||||||
async loginByPasskey(
|
|
||||||
@Req() req: Request,
|
|
||||||
@Body() body: PasskeyLoginDto,
|
|
||||||
@Res({ passthrough: true }) res: Response,
|
|
||||||
) {
|
|
||||||
const tempSessionId = req.cookies?.passkey_temp_session;
|
|
||||||
if (!tempSessionId) {
|
|
||||||
throw new BadRequestException('登录失败,请重试');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await this.passkeyService.login(tempSessionId, body.credentialResponse);
|
|
||||||
|
|
||||||
const session = await this.userSessionService.createSession(user.userId);
|
|
||||||
|
|
||||||
this.setUserSession(res, session);
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: await this.userService.findById(user.userId),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
res.clearCookie('passkey_temp_session', {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
path: '/api/auth/passkey/login',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Post('passkey/register/options')
|
|
||||||
async getPasskeyRegisterOptions(
|
|
||||||
@CurrentUser() user: AuthUser,
|
|
||||||
) {
|
|
||||||
const { userId } = user;
|
|
||||||
return this.passkeyService.getRegistrationOptions(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Post('passkey/register')
|
|
||||||
async registerPasskey(
|
|
||||||
@CurrentUser() user: AuthUser,
|
|
||||||
@Body() dto: PasskeyRegisterDto,
|
|
||||||
) {
|
|
||||||
const { userId } = user;
|
|
||||||
const { credentialResponse, name } = dto;
|
|
||||||
|
|
||||||
const passkey = await this.passkeyService.register(userId, credentialResponse, name.trim());
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: passkey.id,
|
|
||||||
name: passkey.name,
|
|
||||||
createdAt: passkey.createdAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Post('logout')
|
|
||||||
async logout(@CurrentUser() user: AuthUser, @Res({ passthrough: true }) res: Response) {
|
|
||||||
const { sessionId } = user;
|
|
||||||
await this.userSessionService.invalidateSession(sessionId, '用户主动登出');
|
|
||||||
res.clearCookie('session', {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
path: '/',
|
|
||||||
})
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { forwardRef, Module } from '@nestjs/common';
|
|
||||||
import { AuthController } from './auth.controller';
|
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { UserModule } from 'src/user/user.module';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { UserSession } from 'src/auth/entity/user-session.entity';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { VerificationModule } from 'src/verification/verification.module';
|
|
||||||
import { AuthGuard } from './guards/auth.guard';
|
|
||||||
import { OptionalAuthGuard } from './guards/optional-auth.guard';
|
|
||||||
import { SmsModule } from 'src/sms/sms.module';
|
|
||||||
import { PasskeyCredential } from './entity/passkey-credential.entity';
|
|
||||||
import { UserSessionService } from './service/user-session.service';
|
|
||||||
import { PasskeyService } from './service/passkey.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule,
|
|
||||||
forwardRef(() => UserModule),
|
|
||||||
TypeOrmModule.forFeature([UserSession, PasskeyCredential]),
|
|
||||||
VerificationModule,
|
|
||||||
SmsModule,
|
|
||||||
],
|
|
||||||
controllers: [AuthController],
|
|
||||||
providers: [AuthService, UserSessionService, PasskeyService, AuthGuard, OptionalAuthGuard],
|
|
||||||
exports: [AuthService, UserSessionService, PasskeyService, AuthGuard, OptionalAuthGuard],
|
|
||||||
})
|
|
||||||
export class AuthModule { }
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { createHash } from 'crypto';
|
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
|
||||||
import { UserService } from 'src/user/user.service';
|
|
||||||
import { UserSessionService } from 'src/auth/service/user-session.service';
|
|
||||||
import { BusinessException } from 'src/common/exceptions/business.exception';
|
|
||||||
import { ErrorCode } from 'src/common/constants/error-codes';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthService {
|
|
||||||
constructor(
|
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly userSessionService: UserSessionService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async loginWithPassword(identifier: string, password: string) {
|
|
||||||
// 依次使用邮箱、手机号、账号登陆(防止有大聪明给账号改成别人的邮箱或手机号)
|
|
||||||
const user = await this.userService.findOne(
|
|
||||||
[{ email: identifier }, { phone: identifier }, { username: identifier }],
|
|
||||||
{
|
|
||||||
withDeleted: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (user && user.deletedAt !== null) {
|
|
||||||
throw new BusinessException({
|
|
||||||
message: '该账号注销中',
|
|
||||||
code: ErrorCode.USER_ACCOUNT_DEACTIVATED,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user === null || !user.password_hash || !user.salt) {
|
|
||||||
throw new BusinessException({
|
|
||||||
message: '账户或密码错误',
|
|
||||||
code: ErrorCode.AUTH_INVALID_CREDENTIALS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断密码是否正确
|
|
||||||
const hashedPassword = this.hashPassword(password, user.salt);
|
|
||||||
if (hashedPassword !== user.password_hash) {
|
|
||||||
throw new BusinessException({
|
|
||||||
message: '账户或密码错误',
|
|
||||||
code: ErrorCode.AUTH_INVALID_CREDENTIALS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { userId } = user;
|
|
||||||
|
|
||||||
return this.userSessionService.createSession(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loginWithPhone(phone: string) {
|
|
||||||
// 判断用户是否存在,若不存在则进行注册
|
|
||||||
let user = await this.userService.findOne({ phone }, { withDeleted: true });
|
|
||||||
if (user && user.deletedAt !== null) {
|
|
||||||
throw new BadRequestException('该账号注销中,请使用其他手机号');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
// 执行注册操作
|
|
||||||
user = await this.userService.register({ phone });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user || !user.userId) {
|
|
||||||
// 注册失败或用户信息错误
|
|
||||||
throw new BadRequestException('请求失败,请稍后再试');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.userSessionService.createSession(user.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private hashPassword(password: string, salt: string): string {
|
|
||||||
return createHash('sha256').update(`${password}${salt}`).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
||||||
import { Request } from 'express';
|
|
||||||
|
|
||||||
export interface AuthUser {
|
|
||||||
sessionId: string;
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CurrentUser = createParamDecorator(
|
|
||||||
(data: unknown, ctx: ExecutionContext): AuthUser => {
|
|
||||||
const request = ctx.switchToHttp().getRequest<Request>();
|
|
||||||
return request.user;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { IsEnum, IsString, Length, ValidateIf } from 'class-validator';
|
|
||||||
|
|
||||||
// export class LoginDto {
|
|
||||||
// @IsEnum(['password', 'phone', 'email'], { message: '请求类型错误' })
|
|
||||||
// type: 'password' | 'phone' | 'email';
|
|
||||||
|
|
||||||
// @ValidateIf((o) => o.type === 'password')
|
|
||||||
|
|
||||||
// account?: string;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// @ValidateIf((o) => o.type === 'phone')
|
|
||||||
// @IsString({ message: '手机号必须输入' })
|
|
||||||
// @Length(11, 11, { message: '手机号异常' }) // 中国大陆,11位数字
|
|
||||||
// phone?: string;
|
|
||||||
|
|
||||||
// @ValidateIf((o) => o.type === 'email')
|
|
||||||
// @IsString({ message: '邮箱必须输入' })
|
|
||||||
// @Length(6, 254, { message: '邮箱异常' }) // RFC 5321
|
|
||||||
// email?: string;
|
|
||||||
|
|
||||||
// @ValidateIf((o) => o.type === 'phone' || o.type === 'email')
|
|
||||||
// @IsString({ message: '验证码必须输入' })
|
|
||||||
// @Length(6, 6, { message: '验证码异常' }) // 6位数字
|
|
||||||
// code?: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export class LoginByPasswordDto {
|
|
||||||
@IsString({ message: '账户必须输入' })
|
|
||||||
@Length(1, 254, { message: '账户异常' }) // 用户名、邮箱、手机号
|
|
||||||
identifier: string;
|
|
||||||
|
|
||||||
@IsString({ message: '密码必须输入' })
|
|
||||||
@Length(6, 32, { message: '密码异常' }) // 6-32位
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { IsObject } from "class-validator";
|
|
||||||
|
|
||||||
export class PasskeyLoginDto {
|
|
||||||
@IsObject()
|
|
||||||
credentialResponse: any;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { IsObject, IsString } from "class-validator";
|
|
||||||
|
|
||||||
export class PasskeyRegisterDto {
|
|
||||||
@IsObject()
|
|
||||||
credentialResponse: any;
|
|
||||||
|
|
||||||
@IsString({ message: '通行证名称只能是字符串' })
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { IsPhoneNumber, Matches } from "class-validator";
|
|
||||||
|
|
||||||
export class SmsLoginDto {
|
|
||||||
@IsPhoneNumber('CN', {
|
|
||||||
message: '请输入有效的中国大陆手机号',
|
|
||||||
})
|
|
||||||
phone: string;
|
|
||||||
|
|
||||||
@Matches(/^\d{6}$/, {
|
|
||||||
message: '验证码必须是6位数字',
|
|
||||||
})
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { User } from "src/user/entities/user.entity";
|
|
||||||
import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
@Index(['user'])
|
|
||||||
export class PasskeyCredential {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
// 关联用户
|
|
||||||
@ManyToOne(() => User, user => user.passkeys, { onDelete: 'CASCADE' })
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
// WebAuthn 必需字段
|
|
||||||
@Column({ length: 255 })
|
|
||||||
name: string; // 用户自定义名称,如 "iPhone", "工作笔记本"
|
|
||||||
|
|
||||||
@Column({ unique: true })
|
|
||||||
credentialId: string; // Base64URL 编码的 credentialId(唯一标识)
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
publicKey: string; // Base64URL 编码的公钥(SPKI 格式)
|
|
||||||
|
|
||||||
@Column({ type: 'int' })
|
|
||||||
signCount: number; // 防重放攻击,每次签名递增
|
|
||||||
|
|
||||||
// 是否已验证(注册时验证,登录时更新)
|
|
||||||
@Column({ default: false })
|
|
||||||
verified: boolean;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// auth.guard.ts
|
|
||||||
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { Request } from 'express';
|
|
||||||
import { UserSessionService } from 'src/auth/service/user-session.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthGuard implements CanActivate {
|
|
||||||
constructor(
|
|
||||||
private userSessionService: UserSessionService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
const request = context.switchToHttp().getRequest<Request>();
|
|
||||||
|
|
||||||
// 从 Cookie 读取 session
|
|
||||||
const sessionId = request.cookies?.['session'];
|
|
||||||
if (!sessionId) {
|
|
||||||
throw new UnauthorizedException('登陆凭证无效,请重新登陆');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 session
|
|
||||||
const session = await this.userSessionService.getSession(sessionId);
|
|
||||||
if (!session) {
|
|
||||||
throw new UnauthorizedException('登陆凭证无效,请重新登陆');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { userId } = session;
|
|
||||||
request.user = {
|
|
||||||
sessionId,
|
|
||||||
userId,
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { ExecutionContext, Injectable } from "@nestjs/common";
|
|
||||||
import { AuthGuard } from "./auth.guard";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class OptionalAuthGuard extends AuthGuard {
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
return await super.canActivate(context);
|
|
||||||
} catch (error) {
|
|
||||||
// 验证失败时,req.user = null,但允许继续
|
|
||||||
const request = context.switchToHttp().getRequest();
|
|
||||||
request.user = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
|
||||||
import { PasskeyCredential } from "../entity/passkey-credential.entity";
|
|
||||||
import { Repository } from "typeorm";
|
|
||||||
import { User } from "src/user/entities/user.entity";
|
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
import { generateAuthenticationOptions, GenerateAuthenticationOptionsOpts, generateRegistrationOptions, GenerateRegistrationOptionsOpts, VerifiedAuthenticationResponse, VerifiedRegistrationResponse, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
|
|
||||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
||||||
|
|
||||||
interface ChallengeEntry {
|
|
||||||
value: string;
|
|
||||||
expiresAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MemoryChallengeStore {
|
|
||||||
private store = new Map<string, ChallengeEntry>();
|
|
||||||
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
constructor(private ttlMs: number = 5 * 60 * 100) {
|
|
||||||
this.startCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key: string, value: string): void {
|
|
||||||
this.store.set(key, {
|
|
||||||
value,
|
|
||||||
expiresAt: Date.now() + this.ttlMs,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key: string): string | null {
|
|
||||||
const entry = this.store.get(key);
|
|
||||||
if (!entry) return null;
|
|
||||||
if (Date.now() > entry.expiresAt) {
|
|
||||||
this.store.delete(key);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return entry.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(key: string): void {
|
|
||||||
this.store.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private startCleanup(): void {
|
|
||||||
this.cleanupInterval = setInterval(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
for (const [key, entry] of this.store.entries()) {
|
|
||||||
if (now > entry.expiresAt) {
|
|
||||||
this.store.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 60_000); // 每分钟清理一次
|
|
||||||
}
|
|
||||||
|
|
||||||
stopCleanup(): void {
|
|
||||||
if (this.cleanupInterval) {
|
|
||||||
clearInterval(this.cleanupInterval);
|
|
||||||
this.cleanupInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const registrationChallenges = new MemoryChallengeStore(5 * 60 * 1000); // 5 分钟过期
|
|
||||||
const authenticationChallenges = new MemoryChallengeStore(5 * 60 * 1000);
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PasskeyService implements OnModuleDestroy {
|
|
||||||
|
|
||||||
private readonly rpID: string;
|
|
||||||
private readonly origin: string;
|
|
||||||
private readonly rpName: string;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(PasskeyCredential)
|
|
||||||
private readonly passkeyRepo: Repository<PasskeyCredential>,
|
|
||||||
@InjectRepository(User)
|
|
||||||
private readonly userRepository: Repository<User>,
|
|
||||||
) {
|
|
||||||
this.rpID = process.env.WEBAUTHN_RP_ID;
|
|
||||||
this.origin = process.env.WEBAUTHN_ORIGIN;
|
|
||||||
this.rpName = process.env.WEBAUTHN_RP_NAME;
|
|
||||||
|
|
||||||
if (!this.rpID || !this.origin || !this.rpName) {
|
|
||||||
throw new Error('Missing required env: WEBAUTHN_RP_ID or WEBAUTHN_ORIGIN');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onModuleDestroy() {
|
|
||||||
registrationChallenges.stopCleanup();
|
|
||||||
authenticationChallenges.stopCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateChallenge(length: number = 32): string {
|
|
||||||
return randomBytes(length).toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRegistrationOptions(userId: string) {
|
|
||||||
const user = await this.userRepository.findOneBy({ userId });
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException('用户不存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
const challenge = this.generateChallenge();
|
|
||||||
|
|
||||||
const opts: GenerateRegistrationOptionsOpts = {
|
|
||||||
rpName: this.rpName,
|
|
||||||
rpID: this.rpID,
|
|
||||||
userID: Buffer.from(userId),
|
|
||||||
userName: user.username || 'user',
|
|
||||||
userDisplayName: user.nickname || 'User',
|
|
||||||
challenge,
|
|
||||||
authenticatorSelection: {
|
|
||||||
residentKey: 'required', // 必须是可发现凭证(Passkey)
|
|
||||||
userVerification: 'preferred',
|
|
||||||
},
|
|
||||||
supportedAlgorithmIDs: [-7], // ES256
|
|
||||||
timeout: 60000,
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = await generateRegistrationOptions(opts);
|
|
||||||
registrationChallenges.set(userId, options.challenge)
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
async register(userId: string, credentialResponse: any, name: string): Promise<PasskeyCredential> {
|
|
||||||
const expectedChallenge = registrationChallenges.get(userId);
|
|
||||||
if (!expectedChallenge) {
|
|
||||||
throw new BadRequestException('注册失败,请重试');
|
|
||||||
}
|
|
||||||
|
|
||||||
let verification: VerifiedRegistrationResponse;
|
|
||||||
try {
|
|
||||||
verification = await verifyRegistrationResponse({
|
|
||||||
response: credentialResponse,
|
|
||||||
expectedChallenge,
|
|
||||||
expectedOrigin: this.origin,
|
|
||||||
expectedRPID: this.rpID,
|
|
||||||
requireUserVerification: false,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
throw new BadRequestException('注册失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verification.verified) {
|
|
||||||
throw new BadRequestException('注册失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { credential } = verification.registrationInfo;
|
|
||||||
if (!credential) {
|
|
||||||
throw new InternalServerErrorException('服务器内部错误');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存凭证到数据库
|
|
||||||
const passkey = this.passkeyRepo.create({
|
|
||||||
user: { userId } as User,
|
|
||||||
name: name || '新的通行证',
|
|
||||||
credentialId: credential.id,
|
|
||||||
publicKey: isoBase64URL.fromBuffer(credential.publicKey),
|
|
||||||
signCount: credential.counter,
|
|
||||||
verified: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.passkeyRepo.save(passkey);
|
|
||||||
registrationChallenges.delete(userId);
|
|
||||||
|
|
||||||
return passkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAuthenticationOptions(sessionId: string) {
|
|
||||||
const challenge = this.generateChallenge();
|
|
||||||
const opts: GenerateAuthenticationOptionsOpts = {
|
|
||||||
rpID: this.rpID,
|
|
||||||
challenge,
|
|
||||||
timeout: 60000,
|
|
||||||
userVerification: 'preferred',
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = await generateAuthenticationOptions(opts);
|
|
||||||
authenticationChallenges.set(sessionId, options.challenge);
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(sessionId: string, credentialResponse: any): Promise<User> {
|
|
||||||
const expectedChallenge = authenticationChallenges.get(sessionId);
|
|
||||||
if (!expectedChallenge) {
|
|
||||||
throw new BadRequestException('认证失败,请重试');
|
|
||||||
}
|
|
||||||
|
|
||||||
const credentialId = credentialResponse.id;
|
|
||||||
const passkey = await this.passkeyRepo.findOne({
|
|
||||||
where: { credentialId, verified: true },
|
|
||||||
relations: ['user'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!passkey) {
|
|
||||||
throw new NotFoundException('未找到可用的通行证');
|
|
||||||
}
|
|
||||||
|
|
||||||
let verification: VerifiedAuthenticationResponse;
|
|
||||||
try {
|
|
||||||
verification = await verifyAuthenticationResponse({
|
|
||||||
response: credentialResponse,
|
|
||||||
expectedChallenge,
|
|
||||||
expectedOrigin: this.origin,
|
|
||||||
expectedRPID: this.rpID,
|
|
||||||
credential: {
|
|
||||||
id: passkey.credentialId,
|
|
||||||
publicKey: isoBase64URL.toBuffer(passkey.publicKey),
|
|
||||||
counter: passkey.signCount,
|
|
||||||
},
|
|
||||||
requireUserVerification: false,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
throw new BadRequestException('认证失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verification.verified) {
|
|
||||||
throw new BadRequestException('认证失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSignCount = verification.authenticationInfo.newCounter;
|
|
||||||
if (newSignCount !== passkey.signCount) {
|
|
||||||
passkey.signCount = newSignCount;
|
|
||||||
await this.passkeyRepo.save(passkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationChallenges.delete(sessionId);
|
|
||||||
return passkey.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async listUserPasskeys(userId: string): Promise<PasskeyCredential[]> {
|
|
||||||
return this.passkeyRepo.find({
|
|
||||||
where: { user: { userId }, verified: true },
|
|
||||||
select: ['id', 'name', 'createdAt'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async removePasskey(userId: string, passkeyId: string): Promise<void> {
|
|
||||||
const result = await this.passkeyRepo.delete({
|
|
||||||
id: passkeyId,
|
|
||||||
user: { userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.affected === 0) {
|
|
||||||
throw new NotFoundException('未找到对应的通行证');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { UserSession } from '../entity/user-session.entity';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UserSessionService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(UserSession)
|
|
||||||
private readonly userSessionRepository: Repository<UserSession>,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async createSession(userId: string): Promise<UserSession> {
|
|
||||||
const session = this.userSessionRepository.create({
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
return this.userSessionRepository.save(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSession(sessionId: string) {
|
|
||||||
const session = await this.userSessionRepository.findOne({
|
|
||||||
where: {
|
|
||||||
sessionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
async invalidateSession(sessionId: string, reason?: string): Promise<void> {
|
|
||||||
await this.userSessionRepository.update(
|
|
||||||
{ sessionId, deletedAt: null },
|
|
||||||
{
|
|
||||||
deletedAt: new Date(),
|
|
||||||
disabledReason: reason || null,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { CaptchaController } from './captcha.controller';
|
|
||||||
|
|
||||||
describe('CaptchaController', () => {
|
|
||||||
let controller: CaptchaController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [CaptchaController],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<CaptchaController>(CaptchaController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(controller).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
import { GetCaptchaDto } from './dto/get-captcha.dto';
|
|
||||||
|
|
||||||
@Controller('captcha')
|
|
||||||
export class CaptchaController {
|
|
||||||
@Get()
|
|
||||||
async getCaptcha(dto: GetCaptchaDto) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { CaptchaService } from './captcha.service';
|
|
||||||
import { CaptchaController } from './captcha.controller';
|
|
||||||
import { CaptchaRateLimitService } from './service/rate-limit';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [CaptchaService, CaptchaRateLimitService],
|
|
||||||
controllers: [CaptchaController],
|
|
||||||
imports: [],
|
|
||||||
})
|
|
||||||
export class CaptchaModule { }
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { CaptchaService } from './captcha.service';
|
|
||||||
|
|
||||||
describe('CaptchaService', () => {
|
|
||||||
let service: CaptchaService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [CaptchaService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<CaptchaService>(CaptchaService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ErrorCode } from 'src/common/constants/error-codes';
|
|
||||||
import { BusinessException } from 'src/common/exceptions/business.exception';
|
|
||||||
|
|
||||||
export enum CaptchaContext {
|
|
||||||
SEND_SMS = 'send_sms',
|
|
||||||
PASSKEY = 'passkey',
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CaptchaService {
|
|
||||||
public async generate(context: CaptchaContext, ip: string, userId?: string) {
|
|
||||||
await this.checkRateLimit(ip, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async verify(token: string, ip: string, userId?: string) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkRateLimit(ip: string, context: CaptchaContext) {
|
|
||||||
/** @todo */
|
|
||||||
throw new BusinessException({
|
|
||||||
code: ErrorCode.CAPTCHA_RARE_LIMIT,
|
|
||||||
message: '服务器处理不过来了,过会儿再试试吧',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { IsEnum, IsOptional, IsUUID } from "class-validator";
|
|
||||||
|
|
||||||
export enum CaptchaContext {
|
|
||||||
SEND_SMS = 'send_sms',
|
|
||||||
PASSKEY = 'passkey',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GetCaptchaDto {
|
|
||||||
|
|
||||||
@IsEnum(CaptchaContext, { message: '无效的context' })
|
|
||||||
context: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID('4', { message: 'userId不合法' })
|
|
||||||
userId?: string;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export class CaptchaRateLimitService {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { RolesGuard } from './guard/roles.guard';
|
|
||||||
import { UserModule } from 'src/user/user.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [RolesGuard],
|
|
||||||
imports: [UserModule],
|
|
||||||
exports: [RolesGuard],
|
|
||||||
})
|
|
||||||
export class CommonModule { }
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
/**
|
|
||||||
* 全局业务错误码规范:
|
|
||||||
* - 每个模块分配一个 1000 起始的段(如 USER: -1000~1999, AUTH: -2000~2999)
|
|
||||||
* - 代码结构:{ 模块名大写 }_{ 错误语义 }
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const ErrorCode = {
|
|
||||||
// 通用错误(0 ~ 999)
|
|
||||||
COMMON_INTERNAL_ERROR: -1,
|
|
||||||
COMMON_INVALID_PARAM: -2,
|
|
||||||
COMMON_NOT_FOUND: -3,
|
|
||||||
|
|
||||||
// 用户模块(1000 ~ 1999)
|
|
||||||
USER_NOT_FOUND: -1001,
|
|
||||||
USER_ALREADY_EXISTS: -1002,
|
|
||||||
USER_ACCOUNT_DISABLED: -1003,
|
|
||||||
USER_FIND_OPTIONS_EMPTY: -1004,
|
|
||||||
USER_ACCOUNT_DEACTIVATED: -1005,
|
|
||||||
|
|
||||||
// 认证模块
|
|
||||||
AUTH_INVALID_CREDENTIALS: -2001,
|
|
||||||
AUTH_PASSKEY_NOT_REGISTERED: -2002,
|
|
||||||
AUTH_SESSION_EXPIRED: -2003,
|
|
||||||
|
|
||||||
// 博客模块
|
|
||||||
BLOG_NOT_FOUND: -3001,
|
|
||||||
BLOG_PERMISSION_DENIED: -3002,
|
|
||||||
|
|
||||||
// 验证模块
|
|
||||||
CAPTCHA_RARE_LIMIT: -4001,
|
|
||||||
|
|
||||||
// 通知模块
|
|
||||||
NOTIFICATION_SEND_FAILED: -5001,
|
|
||||||
|
|
||||||
// Sms模块
|
|
||||||
SMS_CODE_INCORRECT: -6001,
|
|
||||||
SMS_CODE_EXPIRED: -6002,
|
|
||||||
|
|
||||||
// 资源模块
|
|
||||||
RESOURCE_UPLOAD_FAILED: -7001,
|
|
||||||
RESOURCE_NOT_FOUND: -7002,
|
|
||||||
|
|
||||||
// 管理员模块
|
|
||||||
ADMIN_FORBIDDEN: -8001,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type ErrorCodeType = typeof ErrorCode[keyof typeof ErrorCode];
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { HttpStatus } from '@nestjs/common';
|
|
||||||
|
|
||||||
export class BusinessException {
|
|
||||||
|
|
||||||
public statusCode: HttpStatus;
|
|
||||||
public message: string;
|
|
||||||
public code: number;
|
|
||||||
public data: any;
|
|
||||||
|
|
||||||
constructor(args: {
|
|
||||||
statusCode?: HttpStatus,
|
|
||||||
message?: string,
|
|
||||||
code?: number,
|
|
||||||
data?: any,
|
|
||||||
}) {
|
|
||||||
const { statusCode, message, code, data } = args;
|
|
||||||
this.statusCode = statusCode || HttpStatus.BAD_REQUEST;
|
|
||||||
this.message = message || '请求错误';
|
|
||||||
this.code = code || -1;
|
|
||||||
this.data = data || null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { ArgumentsHost, ExceptionFilter, HttpException, HttpStatus, Logger } from "@nestjs/common";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { BusinessException } from "../exceptions/business.exception";
|
|
||||||
|
|
||||||
export class GlobalExceptionsFilter implements ExceptionFilter {
|
|
||||||
catch(exception: any, host: ArgumentsHost) {
|
|
||||||
const ctx = host.switchToHttp();
|
|
||||||
const response = ctx.getResponse<Response>();
|
|
||||||
const request = ctx.getRequest<Request>();
|
|
||||||
|
|
||||||
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
||||||
let errorResponse = {
|
|
||||||
success: false,
|
|
||||||
message: '服务器内部错误',
|
|
||||||
code: -1,
|
|
||||||
data: null as any,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (exception instanceof BusinessException) {
|
|
||||||
statusCode = exception.statusCode;
|
|
||||||
const { message, code, data } = exception;
|
|
||||||
errorResponse = {
|
|
||||||
...errorResponse,
|
|
||||||
message, code, data,
|
|
||||||
}
|
|
||||||
} else if (exception instanceof HttpException) {
|
|
||||||
// 当HttpException传入类型为string时,响应data为null,message为传入的string
|
|
||||||
// 其他请况(object/number),响应为传入数据,message为HttpException的错误码
|
|
||||||
statusCode = exception.getStatus();
|
|
||||||
const exceptionResponse = exception.getResponse() as Record<string, any>;
|
|
||||||
if (exceptionResponse.message) {
|
|
||||||
errorResponse.message = exceptionResponse.message;
|
|
||||||
} else {
|
|
||||||
errorResponse.message = '请求失败';
|
|
||||||
errorResponse.data = exceptionResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode === HttpStatus.UNAUTHORIZED && request.cookies?.['session']) {
|
|
||||||
response.clearCookie('session', {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode === HttpStatus.TOO_MANY_REQUESTS) {
|
|
||||||
errorResponse.message = '请求过于频繁,请稍后再试';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.warn(exception, request.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.status(statusCode).json(errorResponse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import {
|
|
||||||
CanActivate,
|
|
||||||
ExecutionContext,
|
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
|
||||||
InternalServerErrorException,
|
|
||||||
Logger,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { Request } from 'express';
|
|
||||||
import { AuthUser } from 'src/auth/decorator/current-user.decorator';
|
|
||||||
import { Role } from 'src/auth/role.enum';
|
|
||||||
import { UserService } from 'src/user/user.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RolesGuard implements CanActivate {
|
|
||||||
|
|
||||||
private logger = new Logger(RolesGuard.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private reflector: Reflector,
|
|
||||||
private readonly userService: UserService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
const requiredRoles = this.reflector.getAllAndOverride<Role[] | undefined>(
|
|
||||||
'roles',
|
|
||||||
[context.getHandler(), context.getClass()],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!requiredRoles) return true;
|
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest<Request>();
|
|
||||||
const authUser = request.user as AuthUser;
|
|
||||||
|
|
||||||
if (!authUser) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Path: ${request.path} has RolesGuard enabled, but it seems AuthGuard was forgotten.`
|
|
||||||
)
|
|
||||||
throw new InternalServerErrorException('服务器内部错误');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { userId } = authUser;
|
|
||||||
const user = await this.userService.findOne({ userId })
|
|
||||||
if (!user) {
|
|
||||||
this.logger.warn(
|
|
||||||
`UserId: ${user.userId} has a valid login credential, but the user information does not exist.`
|
|
||||||
)
|
|
||||||
throw new UnauthorizedException('用户不存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!requiredRoles.some((role) => user.roles.includes(role))) {
|
|
||||||
throw new ForbiddenException('权限不足');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AuthUser } from "src/auth/decorator/current-user.decorator";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace Express {
|
|
||||||
interface Request {
|
|
||||||
user?: AuthUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import 'reflect-metadata';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
|
||||||
type: 'postgres',
|
|
||||||
host: process.env.DATABASE_HOST,
|
|
||||||
port: Number(process.env.DATABASE_PORT ?? 5432),
|
|
||||||
username: process.env.DATABASE_USERNAME,
|
|
||||||
password: process.env.DATABASE_PASSWORD,
|
|
||||||
database: process.env.DATABASE_NAME,
|
|
||||||
|
|
||||||
synchronize: false,
|
|
||||||
logging: false,
|
|
||||||
|
|
||||||
entities: ['dist/**/*.entity.js'],
|
|
||||||
migrations: ['dist/migrations/*.js'],
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
||||||
import { OssService } from './oss.service';
|
|
||||||
import { AuthGuard } from 'src/auth/guards/auth.guard';
|
|
||||||
import { AuthUser, CurrentUser } from 'src/auth/decorator/current-user.decorator';
|
|
||||||
|
|
||||||
@Controller('oss')
|
|
||||||
export class OssController {
|
|
||||||
constructor(private readonly ossService: OssService) { }
|
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Get('sts')
|
|
||||||
async getStsToken(@CurrentUser() user: AuthUser) {
|
|
||||||
const { userId } = user;
|
|
||||||
return {
|
|
||||||
...(await this.ossService.getStsToken(`${userId}`)),
|
|
||||||
userId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { PublicResource, Resource } from './entity/resource.entity';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ResourceService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Resource)
|
|
||||||
private readonly resourceRepository: Repository<Resource>,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async findAll(): Promise<PublicResource[]> {
|
|
||||||
return this.resourceRepository.find({
|
|
||||||
select: ['id', 'title', 'description', 'imageUrl', 'link', 'tags'],
|
|
||||||
order: {
|
|
||||||
updatedAt: 'DESC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { IsPhoneNumber } from "class-validator";
|
|
||||||
|
|
||||||
export class SendLoginSmsDto {
|
|
||||||
@IsPhoneNumber('CN', { message: '请输入有效的中国大陆手机号' })
|
|
||||||
phone: string;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from "typeorm";
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
@Index('IDX_SMS_PHONE_TYPE', ['phone', 'type'])
|
|
||||||
@Index('IDX_SMS_EXPIRED', ['expiredAt'])
|
|
||||||
export class SmsRecord {
|
|
||||||
@PrimaryGeneratedColumn('identity')
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
phone: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
type: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
code: string;
|
|
||||||
|
|
||||||
@Column({ type: 'smallint', default: 0 })
|
|
||||||
tryCount: number;
|
|
||||||
|
|
||||||
@CreateDateColumn({ precision: 3 })
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', precision: 3 })
|
|
||||||
expiredAt: Date;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', precision: 3, nullable: true })
|
|
||||||
usedAt: Date;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { SmsController } from './sms.controller';
|
|
||||||
|
|
||||||
describe('SmsController', () => {
|
|
||||||
let controller: SmsController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [SmsController],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<SmsController>(SmsController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(controller).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
|
||||||
import { SendLoginSmsDto } from './dto/send-login-sms.dto';
|
|
||||||
import { SmsService } from './sms.service';
|
|
||||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
|
||||||
|
|
||||||
@Controller('sms')
|
|
||||||
export class SmsController {
|
|
||||||
|
|
||||||
constructor(private readonly smsService: SmsService) { }
|
|
||||||
|
|
||||||
@Post('send/login')
|
|
||||||
@UseGuards(ThrottlerGuard)
|
|
||||||
@Throttle({
|
|
||||||
'min': { limit: 3, ttl: 60 * 1000 },
|
|
||||||
'hour': { limit: 10, ttl: 60 * 60 * 1000 },
|
|
||||||
'day': { limit: 20, ttl: 24 * 60 * 60 * 1000 }
|
|
||||||
})
|
|
||||||
async sendLoginSms(@Body() dto: SendLoginSmsDto) {
|
|
||||||
await this.smsService.sendSms(dto.phone, 'login');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { SmsService } from './sms.service';
|
|
||||||
import { SmsController } from './sms.controller';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { SmsRecord } from './entity/sms-record.entity';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TypeOrmModule.forFeature([SmsRecord])],
|
|
||||||
providers: [SmsService],
|
|
||||||
controllers: [SmsController],
|
|
||||||
exports: [SmsService],
|
|
||||||
})
|
|
||||||
export class SmsModule { }
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { SmsService } from './sms.service';
|
|
||||||
|
|
||||||
describe('SmsService', () => {
|
|
||||||
let service: SmsService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [SmsService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<SmsService>(SmsService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
|
|
||||||
import Dypnsapi, * as $Dypnsapi from '@alicloud/dypnsapi20170525';
|
|
||||||
import * as $OpenApi from '@alicloud/openapi-client';
|
|
||||||
import { randomInt } from 'crypto';
|
|
||||||
import { MoreThan, Repository } from 'typeorm';
|
|
||||||
import { SmsRecord } from './entity/sms-record.entity';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { BusinessException } from 'src/common/exceptions/business.exception';
|
|
||||||
import { ErrorCode } from 'src/common/constants/error-codes';
|
|
||||||
|
|
||||||
const LoginSmsExpiredMin = 5;
|
|
||||||
const LoginSmsMaxTryCount = 5;
|
|
||||||
|
|
||||||
const devMode = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SmsService {
|
|
||||||
|
|
||||||
private logger = new Logger(SmsService.name);
|
|
||||||
|
|
||||||
private client: Dypnsapi;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(SmsRecord)
|
|
||||||
private readonly smsRecordRepository: Repository<SmsRecord>
|
|
||||||
) {
|
|
||||||
const config = new $OpenApi.Config({})
|
|
||||||
config.accessKeyId = process.env.ALIYUN_ACCESS_KEY_ID;
|
|
||||||
config.accessKeySecret = process.env.ALIYUN_ACCESS_KEY_SECRET;
|
|
||||||
|
|
||||||
this.client = new Dypnsapi(config as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateSmsCode(): string {
|
|
||||||
// 生成 0 到 999999 的随机整数,补零到 6 位
|
|
||||||
const code = randomInt(0, 1_000_000);
|
|
||||||
return code.toString().padStart(6, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkSendSmsLimit(phone: string, type: string): Promise<void> {
|
|
||||||
const now = new Date();
|
|
||||||
const oneMinuteAgo = new Date(now.getTime() - 60 * 1000);
|
|
||||||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
// 1. 检查 1 分钟内是否已发送
|
|
||||||
const recentRecord = await this.smsRecordRepository.findOne({
|
|
||||||
where: {
|
|
||||||
phone,
|
|
||||||
type,
|
|
||||||
createdAt: MoreThan(twentyFourHoursAgo),
|
|
||||||
},
|
|
||||||
order: { createdAt: 'DESC' },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (recentRecord && recentRecord.createdAt > oneMinuteAgo) {
|
|
||||||
throw new BusinessException({ message: '操作太快了,稍后再重试吧' }); // 距离上一条不足 1 分钟
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 检查 24 小时内是否超过 5 条
|
|
||||||
const count = await this.smsRecordRepository.count({
|
|
||||||
where: {
|
|
||||||
phone,
|
|
||||||
type,
|
|
||||||
createdAt: MoreThan(twentyFourHoursAgo),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count >= 5) {
|
|
||||||
throw new BusinessException({ message: '操作太快了,稍后再重试吧' }); // 24 小时超过 5 条
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendSms(phone: string, type: 'login') {
|
|
||||||
if (type === 'login') {
|
|
||||||
// 检查限流
|
|
||||||
await this.checkSendSmsLimit(phone, type);
|
|
||||||
|
|
||||||
// 生成
|
|
||||||
const code = this.generateSmsCode();
|
|
||||||
const smsRecord = this.smsRecordRepository.create({
|
|
||||||
phone,
|
|
||||||
type,
|
|
||||||
code,
|
|
||||||
expiredAt: new Date(Date.now() + LoginSmsExpiredMin * 60 * 1000),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 发送
|
|
||||||
const request = new $Dypnsapi.SendSmsVerifyCodeRequest({});
|
|
||||||
request.phoneNumber = phone;
|
|
||||||
request.signName = '速通互联验证码';
|
|
||||||
request.templateCode = '100001';
|
|
||||||
request.templateParam = JSON.stringify({
|
|
||||||
code,
|
|
||||||
min: `${LoginSmsExpiredMin}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
let success: boolean = false;
|
|
||||||
|
|
||||||
if (devMode) {
|
|
||||||
success = true;
|
|
||||||
this.logger.debug(`${phone}:${code}`)
|
|
||||||
} else {
|
|
||||||
await this.client.sendSmsVerifyCode(request).then(a => {
|
|
||||||
success = a.body?.success || false;
|
|
||||||
}, err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
this.smsRecordRepository.save(smsRecord).catch(e => {
|
|
||||||
this.logger.warn(e, 'sendSms:saveRecord');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
} else {
|
|
||||||
throw new InternalServerErrorException('未知的Sms类型');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkSms(phone: string, type: 'login', code: string) {
|
|
||||||
if (type === 'login') {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const record = await this.smsRecordRepository.findOne({
|
|
||||||
where: {
|
|
||||||
phone,
|
|
||||||
type,
|
|
||||||
expiredAt: MoreThan(now),
|
|
||||||
},
|
|
||||||
order: { createdAt: 'DESC' },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!record) {
|
|
||||||
throw new BusinessException({
|
|
||||||
code: ErrorCode.SMS_CODE_EXPIRED,
|
|
||||||
message: '验证码已失效,请重新获取',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查被用过没
|
|
||||||
if (record.usedAt !== null) {
|
|
||||||
throw new BusinessException({
|
|
||||||
code: ErrorCode.SMS_CODE_EXPIRED,
|
|
||||||
message: '验证码已失效,请重新获取',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查尝试次数
|
|
||||||
if (record.tryCount >= LoginSmsMaxTryCount) {
|
|
||||||
throw new BusinessException({
|
|
||||||
code: ErrorCode.SMS_CODE_EXPIRED,
|
|
||||||
message: '验证码已失效,请重新获取',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否匹配
|
|
||||||
if (record.code !== code) {
|
|
||||||
// 增加尝试次数
|
|
||||||
record.tryCount = (record.tryCount || 0) + 1;
|
|
||||||
await this.smsRecordRepository.save(record);
|
|
||||||
|
|
||||||
if (record.tryCount >= LoginSmsMaxTryCount) {
|
|
||||||
throw new BusinessException({
|
|
||||||
code: ErrorCode.SMS_CODE_EXPIRED,
|
|
||||||
message: '验证码已失效,请重新获取',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BusinessException({
|
|
||||||
code: ErrorCode.SMS_CODE_INCORRECT,
|
|
||||||
message: '验证码不对的喔~',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
record.usedAt = new Date();
|
|
||||||
await this.smsRecordRepository.save(record);
|
|
||||||
} else {
|
|
||||||
throw new InternalServerErrorException('未知的Sms类型');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Body, Controller, Get, Put, Request, UseGuards } from '@nestjs/common';
|
|
||||||
import { UserService } from './user.service';
|
|
||||||
import { UpdateUserPasswordDto } from './dto/update-user-password.dto';
|
|
||||||
import { AuthGuard } from 'src/auth/guards/auth.guard';
|
|
||||||
import { AuthUser, CurrentUser } from 'src/auth/decorator/current-user.decorator';
|
|
||||||
|
|
||||||
@Controller('user')
|
|
||||||
export class UserController {
|
|
||||||
constructor(
|
|
||||||
private readonly userService: UserService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Get('me')
|
|
||||||
async getMe(@CurrentUser() user: AuthUser) {
|
|
||||||
return this.userService.findById(user.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Put('password')
|
|
||||||
async update(@CurrentUser() user: AuthUser, @Body() dto: UpdateUserPasswordDto): Promise<null> {
|
|
||||||
await this.userService.setPassword(user.userId, dto.password.trim());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import {
|
|
||||||
BadRequestException,
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { SendVerificationCodeDto } from './dto/send-verification-code.dto';
|
|
||||||
import { VerificationService } from './verification.service';
|
|
||||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
|
||||||
|
|
||||||
@Controller('verification')
|
|
||||||
export class VerificationController {
|
|
||||||
constructor(private readonly verificationService: VerificationService) { }
|
|
||||||
|
|
||||||
// @Post('send')
|
|
||||||
// @UseGuards(ThrottlerGuard)
|
|
||||||
// @Throttle({ default: { limit: 20, ttl: 60000 } })
|
|
||||||
// async sendVerificationCode(@Body() dto: SendVerificationCodeDto) {
|
|
||||||
// switch (dto.type) {
|
|
||||||
// case 'login':
|
|
||||||
// switch (dto.targetType) {
|
|
||||||
// case 'phone':
|
|
||||||
// return this.verificationService.sendPhoneCode(dto.phone, dto.type);
|
|
||||||
// case 'email':
|
|
||||||
// return this.verificationService.sendEmailCode(dto.email, dto.type);
|
|
||||||
// default:
|
|
||||||
// throw new BadRequestException('不支持的目标类型');
|
|
||||||
// }
|
|
||||||
// default:
|
|
||||||
// throw new BadRequestException('不支持的验证码类型');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: backend
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: backend
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: backend
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: backend
|
|
||||||
image: localhost:5000/backend:latest
|
|
||||||
ports:
|
|
||||||
- containerPort: 3001
|
|
||||||
env:
|
|
||||||
- name: DATABASE_HOST
|
|
||||||
value: "postgres-service"
|
|
||||||
- name: DATABASE_PORT
|
|
||||||
value: "5432"
|
|
||||||
- name: DATABASE_NAME
|
|
||||||
value: "tone_page"
|
|
||||||
- name: DATABASE_USERNAME
|
|
||||||
value: "tone_page"
|
|
||||||
- name: DATABASE_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend-secret
|
|
||||||
key: DATABASE_PASSWORD
|
|
||||||
- name: ALIYUN_ACCESS_KEY_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend-secret
|
|
||||||
key: ALIYUN_ACCESS_KEY_ID
|
|
||||||
- name: ALIYUN_ACCESS_KEY_SECRET
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend-secret
|
|
||||||
key: ALIYUN_ACCESS_KEY_SECRET
|
|
||||||
- name: ALIYUN_OSS_STS_ROLE_ARN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend-secret
|
|
||||||
key: ALIYUN_OSS_STS_ROLE_ARN
|
|
||||||
- name: WEBAUTHN_RP_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend-secret
|
|
||||||
key: WEBAUTHN_RP_ID
|
|
||||||
- name: WEBAUTHN_ORIGIN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend-secret
|
|
||||||
key: WEBAUTHN_ORIGIN
|
|
||||||
- name: WEBAUTHN_RP_NAME
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: backend-secret
|
|
||||||
key: WEBAUTHN_RP_NAME
|
|
||||||
- name: NODE_ENV
|
|
||||||
value: "production"
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: backend-service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: backend
|
|
||||||
ports:
|
|
||||||
- protocol: TCP
|
|
||||||
port: 3001
|
|
||||||
targetPort: 3001
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
apiVersion: batch/v1
|
|
||||||
kind: Job
|
|
||||||
metadata:
|
|
||||||
name: backend-migration
|
|
||||||
spec:
|
|
||||||
backoffLimit: 0 # 失败不自动重试(防止重复执行)
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
containers:
|
|
||||||
- name: migration
|
|
||||||
image: 192.168.0.200:5000/backend:IMAGE_TAG
|
|
||||||
imagePullPolicy: Always
|
|
||||||
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
echo "Running database migrations..."
|
|
||||||
node ./node_modules/typeorm/cli.js migration:run \
|
|
||||||
-d dist/data-source.js
|
|
||||||
|
|
||||||
envFrom:
|
|
||||||
# 和 backend Deployment 用同一套
|
|
||||||
- secretRef:
|
|
||||||
name: backend-secret
|
|
||||||
- secretRef:
|
|
||||||
name: postgres-secret
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: backend-secret
|
|
||||||
type: Opaque
|
|
||||||
# stringData:
|
|
||||||
# DATABASE_PASSWORD:
|
|
||||||
# ALIYUN_ACCESS_KEY_ID:
|
|
||||||
# ALIYUN_ACCESS_KEY_SECRET:
|
|
||||||
# ALIYUN_OSS_STS_ROLE_ARN:
|
|
||||||
# WEBAUTHN_RP_ID:
|
|
||||||
# WEBAUTHN_ORIGIN:
|
|
||||||
# WEBAUTHN_RP_NAME:
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: frontend
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: frontend
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: frontend
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: frontend
|
|
||||||
image: localhost:5000/frontend:latest
|
|
||||||
ports:
|
|
||||||
- containerPort: 3000
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: frontend-service
|
|
||||||
spec:
|
|
||||||
type: NodePort
|
|
||||||
ports:
|
|
||||||
- port: 3000
|
|
||||||
targetPort: 3000
|
|
||||||
nodePort: 30000
|
|
||||||
selector:
|
|
||||||
app: frontend
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: postgres
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: postgres
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: postgres
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: postgres
|
|
||||||
image: postgres:17-alpine
|
|
||||||
ports:
|
|
||||||
- containerPort: 5432
|
|
||||||
envFrom:
|
|
||||||
- secretRef:
|
|
||||||
name: postgres-secret
|
|
||||||
volumeMounts:
|
|
||||||
- name: postgres-storage
|
|
||||||
mountPath: /var/lib/postgresql/data
|
|
||||||
volumes:
|
|
||||||
- name: postgres-storage
|
|
||||||
hostPath:
|
|
||||||
path: /var/lib/postgres-data
|
|
||||||
type: DirectoryOrCreate
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: postgres-service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: postgres
|
|
||||||
ports:
|
|
||||||
- protocol: TCP
|
|
||||||
port: 5432
|
|
||||||
targetPort: 5432
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: postgres-nodeport
|
|
||||||
spec:
|
|
||||||
type: NodePort
|
|
||||||
ports:
|
|
||||||
- port: 5432
|
|
||||||
targetPort: 5432
|
|
||||||
nodePort: 30001
|
|
||||||
selector:
|
|
||||||
app: postgres
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: postgres-secret
|
|
||||||
type: Opaque
|
|
||||||
# stringData:
|
|
||||||
# POSTGRES_USER:
|
|
||||||
# POSTGRES_PASSWORD:
|
|
||||||
# POSTGRES_DB:
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# 安装依赖
|
|
||||||
FROM node:22-alpine AS deps
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# 编译
|
|
||||||
FROM node:22-alpine AS builder
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY . .
|
|
||||||
ARG API_BASE
|
|
||||||
ENV API_BASE=$API_BASE
|
|
||||||
RUN pnpm run build
|
|
||||||
|
|
||||||
# 运行
|
|
||||||
FROM node:22-alpine AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ARG API_BASE
|
|
||||||
ENV API_BASE=$API_BASE
|
|
||||||
|
|
||||||
COPY --from=builder /app/.next/standalone ./
|
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
|
||||||
COPY --from=builder /app/public ./public
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
CMD ["node", "server.js"]
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import {
|
|
||||||
Alert,
|
|
||||||
AlertDescription,
|
|
||||||
AlertTitle,
|
|
||||||
} from "@/components/ui/alert";
|
|
||||||
import { AlertCircle } from "lucide-react";
|
|
||||||
import { base62 } from "@/lib/utils";
|
|
||||||
import { BlogAPI } from "@/lib/api/server";
|
|
||||||
import { handleAPIError } from "@/lib/api/common";
|
|
||||||
|
|
||||||
const formatNumber = (num: number): string => {
|
|
||||||
if (num >= 1_000_000) {
|
|
||||||
return (num / 1_000_000).toFixed(1) + 'M';
|
|
||||||
}
|
|
||||||
if (num >= 1_000) {
|
|
||||||
return (num / 1_000).toFixed(1) + 'K';
|
|
||||||
}
|
|
||||||
return num.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBlogDetailUrl = (id: string): string => {
|
|
||||||
const cleanId = id.replace(/-/g, '');
|
|
||||||
const encoded = base62.encode(Buffer.from(cleanId, 'hex'));
|
|
||||||
return `/blog/${encoded}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: '日志 - 特恩的日志',
|
|
||||||
description: '我随便发点,你也随便看看~',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function Blog() {
|
|
||||||
let errorMsg = '';
|
|
||||||
const blogs = await BlogAPI.list().catch(e => {
|
|
||||||
handleAPIError(e, ({ message }) => { errorMsg = message });
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="max-w-120 w-auto mx-auto my-10 flex flex-col gap-8">
|
|
||||||
{
|
|
||||||
errorMsg && (
|
|
||||||
<Alert variant="destructive" className="w-full">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertTitle>出错啦</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{errorMsg}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
blogs && blogs.map((blog) => (
|
|
||||||
<article className="w-full px-5 cursor-default" key={blog.id}>
|
|
||||||
<h2 className="text-2xl font-medium">
|
|
||||||
<a
|
|
||||||
className="hover:underline focus:outline-none focus:ring-2 focus:ring-zinc-400 rounded"
|
|
||||||
href={getBlogDetailUrl(blog.id)}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{blog.title}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm font-medium text-zinc-600">{blog.description}</p>
|
|
||||||
<footer className="mt-3 text-sm text-zinc-500 flex items-center gap-2">
|
|
||||||
<time dateTime={blog.createdAt}>
|
|
||||||
{new Date(blog.createdAt).toLocaleString('zh-CN')}
|
|
||||||
</time>
|
|
||||||
<span>·</span>
|
|
||||||
<span>{formatNumber(blog.viewCount)} 次访问</span>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<section className="w-full flex-1 flex flex-col items-center justify-center">
|
|
||||||
<figure className="flex flex-col items-center">
|
|
||||||
<Image
|
|
||||||
src="/avatar.png"
|
|
||||||
alt="TONE 的个人头像"
|
|
||||||
width={180}
|
|
||||||
height={180}
|
|
||||||
className="rounded-full duration-400 size-35 md:size-45 select-none"
|
|
||||||
priority
|
|
||||||
quality={100}
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
<h1 className='text-4xl md:text-5xl font-bold mt-5 md:mt-8 gradient-title duration-400 select-none'>特恩(TONE)</h1>
|
|
||||||
<p className='text-lg sm:text-xl md:text-2xl mt-3 font-medium text-zinc-400 duration-400 select-none'>一名在各个领域反复横跳的程序员</p>
|
|
||||||
<nav className='flex sm:flex-row flex-col gap-2 sm:gap-10 mt-5 md:mt-8 duration-400' aria-label="社交媒体链接">
|
|
||||||
<a href='https://space.bilibili.com/474156211'
|
|
||||||
target='_black'
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className='bg-[#488fe9] hover:bg-[#3972ba] text-center text-white w-45 sm:w-32 px-6 py-2 text-lg rounded-full cursor-pointer'
|
|
||||||
>
|
|
||||||
哔哩哔哩
|
|
||||||
</a>
|
|
||||||
<a href='https://github.com/tonecn'
|
|
||||||
target='_black'
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className='bg-[#da843f] hover:bg-[#c87d3e] text-center text-white w-45 sm:w-32 px-6 py-2 text-lg rounded-full cursor-pointer'
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import React from "react";
|
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
interface ResourceCardImage {
|
|
||||||
imageUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ResourceCardImage({ imageUrl }: ResourceCardImage) {
|
|
||||||
const [imageError, setImageError] = React.useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!imageError && <Image
|
|
||||||
src={imageUrl}
|
|
||||||
alt="资源图片"
|
|
||||||
width={90}
|
|
||||||
height={90}
|
|
||||||
className="rounded-md shadow"
|
|
||||||
priority
|
|
||||||
quality={80}
|
|
||||||
onError={() => setImageError(true)}
|
|
||||||
/>}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useUserStore } from '@/store/useUserStore';
|
|
||||||
import { User } from '@/lib/types/user';
|
|
||||||
|
|
||||||
export function ClientProvider({
|
|
||||||
initialUser,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
initialUser: User | null;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const setUser = useUserStore((state) => state.setUser);
|
|
||||||
const setInitialized = useUserStore((state) => state.setInitialized);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialUser) {
|
|
||||||
setUser(initialUser);
|
|
||||||
}
|
|
||||||
setInitialized();
|
|
||||||
}, [initialUser, setUser, setInitialized]);
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
CloudUpload,
|
|
||||||
Inbox,
|
|
||||||
LucideIcon,
|
|
||||||
Mail,
|
|
||||||
Server,
|
|
||||||
SquareTerminal,
|
|
||||||
Undo2,
|
|
||||||
UserPen,
|
|
||||||
UsersRound,
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import { NavMain } from "@/app/console/(with-menu)/components/nav-main"
|
|
||||||
import { NavUser } from "@/app/console/(with-menu)/components/nav-user"
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarRail,
|
|
||||||
} from "@/components/ui/sidebar"
|
|
||||||
import Link from "next/link"
|
|
||||||
import { User } from "@/lib/types/user"
|
|
||||||
import { Role } from "@/lib/types/role"
|
|
||||||
|
|
||||||
export function AppSidebar({ user, ...props }: React.ComponentProps<typeof Sidebar> & { user: User | null }) {
|
|
||||||
const data = {
|
|
||||||
navMain: null as null | {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
icon?: LucideIcon
|
|
||||||
isActive?: boolean
|
|
||||||
isHidden?: boolean
|
|
||||||
items?: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
isHidden?: boolean
|
|
||||||
}[]
|
|
||||||
}[],
|
|
||||||
}
|
|
||||||
|
|
||||||
data.navMain = [
|
|
||||||
{
|
|
||||||
title: "网站管理",
|
|
||||||
url: "/console/web",
|
|
||||||
icon: SquareTerminal,
|
|
||||||
isHidden: !user?.roles.includes(Role.Admin),
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "资源",
|
|
||||||
url: "/console/web/resource",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "博客",
|
|
||||||
url: "/console/web/blog",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "配置",
|
|
||||||
url: "/console/web/config",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "用户管理",
|
|
||||||
url: "/console/user/list",
|
|
||||||
icon: UsersRound,
|
|
||||||
isHidden: !user?.roles.includes(Role.Admin),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "邮件系统",
|
|
||||||
url: "/console/mail",
|
|
||||||
icon: Mail,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "收件箱",
|
|
||||||
url: "/console/mail/inbox",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "已发送",
|
|
||||||
url: "/console/mail/sent",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "发送邮件",
|
|
||||||
url: "/console/mail/send",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "邮件管理",
|
|
||||||
url: "/console/mail/manage",
|
|
||||||
isHidden: !user?.roles.includes(Role.Admin),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "文件存储",
|
|
||||||
url: "/console/storage",
|
|
||||||
icon: CloudUpload,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "虚拟云空间",
|
|
||||||
url: "/console/vspace",
|
|
||||||
icon: Inbox,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "虚拟主机",
|
|
||||||
url: "/console/vserver",
|
|
||||||
icon: Server,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "账户信息",
|
|
||||||
url: "/console/profile",
|
|
||||||
icon: UserPen
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "前往首页",
|
|
||||||
url: "/",
|
|
||||||
icon: Undo2,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sidebar collapsible="icon" {...props}>
|
|
||||||
<SidebarHeader>
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<Link href="/console">
|
|
||||||
<SidebarMenuButton size="lg" asChild>
|
|
||||||
<div className="cursor-pointer">
|
|
||||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
|
||||||
<SquareTerminal className="size-5" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-0.5 leading-none">
|
|
||||||
<span className="font-semibold">特恩的日志 - 控制台</span>
|
|
||||||
<span className="">v1.0.0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarHeader>
|
|
||||||
<SidebarContent>
|
|
||||||
<NavMain items={data.navMain} />
|
|
||||||
</SidebarContent>
|
|
||||||
<SidebarFooter>
|
|
||||||
<NavUser user={user} />
|
|
||||||
</SidebarFooter>
|
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from "@/components/ui/field";
|
|
||||||
import { useUserStore } from "@/store/useUserStore";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table"
|
|
||||||
import { ReactElement, useState } from "react";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { AuthAPI } from "@/lib/api/client";
|
|
||||||
import { GeneralErrorHandler, handleAPIError } from "@/lib/api/common";
|
|
||||||
import { startRegistration } from '@simplewebauthn/browser';
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const user = userStore.user;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<form onSubmit={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}>
|
|
||||||
<FieldGroup className="gap-5">
|
|
||||||
<FieldSet>
|
|
||||||
<FieldLegend>账户基础信息</FieldLegend>
|
|
||||||
<FieldDescription>这是当前账户的基础信息</FieldDescription>
|
|
||||||
<FieldGroup className="gap-5">
|
|
||||||
{
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: 'username',
|
|
||||||
localName: '用户名',
|
|
||||||
required: true,
|
|
||||||
defaultValue: user?.username,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'nickname',
|
|
||||||
localName: '昵称',
|
|
||||||
required: true,
|
|
||||||
defaultValue: user?.nickname,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'email',
|
|
||||||
localName: '电子邮箱',
|
|
||||||
required: false,
|
|
||||||
defaultValue: user?.email,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'phone',
|
|
||||||
localName: '手机号',
|
|
||||||
required: false,
|
|
||||||
defaultValue: user?.phone,
|
|
||||||
description: '当前仅支持中国大陆(+86)手机号'
|
|
||||||
},
|
|
||||||
].map(({ name, localName, required, defaultValue, description }) => (
|
|
||||||
<Field key={name} className="gap-2">
|
|
||||||
<FieldLabel htmlFor={`console-profile-${name}`} className="text-zinc-800">
|
|
||||||
{localName}
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={`console-profile-${name}`}
|
|
||||||
name={name}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
placeholder={localName}
|
|
||||||
required={required}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
description && (
|
|
||||||
<FieldDescription>
|
|
||||||
{description}
|
|
||||||
</FieldDescription>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<Button type="submit" disabled>保存</Button>
|
|
||||||
<Button variant="outline" type="button" disabled>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
<FieldSeparator className="mt-2 mb-2" />
|
|
||||||
<FieldGroup className="gap-5">
|
|
||||||
<FieldSet>
|
|
||||||
<FieldLegend>通行证列表</FieldLegend>
|
|
||||||
<FieldDescription>
|
|
||||||
通行证(PassKey),一种先进的无密码身份验证技术。
|
|
||||||
</FieldDescription>
|
|
||||||
<PasskeyList />
|
|
||||||
<div>
|
|
||||||
<AddPasskeyDialog>
|
|
||||||
<Button>添加通行证</Button>
|
|
||||||
</AddPasskeyDialog>
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
</FieldGroup>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface AddPasskeyDialogProps {
|
|
||||||
children: ReactElement;
|
|
||||||
}
|
|
||||||
function AddPasskeyDialog({ children }: AddPasskeyDialogProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (name: string) => {
|
|
||||||
try {
|
|
||||||
name = name.trim();
|
|
||||||
if (name.length === 0) {
|
|
||||||
throw new Error('通行证名称不能为空')
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = await AuthAPI.getPasskeyRegisterOptions();
|
|
||||||
|
|
||||||
const credential = await startRegistration({ optionsJSON: options }).catch(() => null);
|
|
||||||
|
|
||||||
if (credential === null) {
|
|
||||||
throw new Error('认证超时');
|
|
||||||
}
|
|
||||||
|
|
||||||
const registerRes = await AuthAPI.passkeyRegister(name, credential);
|
|
||||||
if (registerRes.id) {
|
|
||||||
toast.success('添加成功');
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
handleAPIError(error, GeneralErrorHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
{children}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-100">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>添加通行证</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
如果能添加成功,则说明您的设备支持;如果添加失败了,说明您的设备不支持😊
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const formData = new FormData(e.currentTarget);
|
|
||||||
handleSubmit(formData.get('name')?.toString() || '');
|
|
||||||
}}>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<Label htmlFor="console-add-passkey-name">通行证名称</Label>
|
|
||||||
<Input id="console-add-passkey-name" name="name" required />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter className="mt-6">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">取消</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button type="submit">下一步</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog >
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PasskeyList() {
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
{/* <TableCaption>A list of your recent invoices.</TableCaption> */}
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>名称</TableHead>
|
|
||||||
<TableHead>状态</TableHead>
|
|
||||||
<TableHead>创建时间</TableHead>
|
|
||||||
<TableHead className="text-right">操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className="font-medium">INV001</TableCell>
|
|
||||||
<TableCell>Paid</TableCell>
|
|
||||||
<TableCell>Credit Card</TableCell>
|
|
||||||
<TableCell className="text-right">$250.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div>config</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// import { Button } from "@/components/ui/button";
|
|
||||||
// import { Input } from "@/components/ui/input";
|
|
||||||
// import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
|
|
||||||
// import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
|
||||||
// import { useState, useCallback } from "react";
|
|
||||||
// import { toast } from "sonner";
|
|
||||||
// import LoginHeader from "./LoginHeader";
|
|
||||||
// import { SendCodeFormData } from "./types";
|
|
||||||
// import { Label } from "@/components/ui/label";
|
|
||||||
|
|
||||||
// export default function EmailLoginMode({ onSendCode }: { onSendCode: (data: SendCodeFormData) => Promise<boolean> }) {
|
|
||||||
// const [email, setEmail] = useState("");
|
|
||||||
// const handleSendCode = useCallback(() => {
|
|
||||||
// if (!email.trim().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
|
||||||
// toast.error('请输入正确的邮箱地址');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// onSendCode({
|
|
||||||
// type: 'email',
|
|
||||||
// email,
|
|
||||||
// })
|
|
||||||
// }, [email, onSendCode]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// <LoginHeader />
|
|
||||||
// <div className="grid gap-3">
|
|
||||||
// <Label htmlFor="email">电子邮箱</Label>
|
|
||||||
// <Input
|
|
||||||
// id="email-login-mode-email"
|
|
||||||
// name="email"
|
|
||||||
// type="text"
|
|
||||||
// placeholder="电子邮箱"
|
|
||||||
// value={email}
|
|
||||||
// onChange={(e) => setEmail(e.target.value)}
|
|
||||||
// required />
|
|
||||||
// </div>
|
|
||||||
// <div className="grid gap-3">
|
|
||||||
// <div className="flex items-center h-4">
|
|
||||||
// <Label htmlFor="code">验证码</Label>
|
|
||||||
// </div>
|
|
||||||
// <div className="flex gap-5">
|
|
||||||
// <InputOTP
|
|
||||||
// id="email-login-mode-code"
|
|
||||||
// name="code"
|
|
||||||
// maxLength={6}
|
|
||||||
// pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
|
||||||
// required
|
|
||||||
// >
|
|
||||||
// <InputOTPGroup>
|
|
||||||
// <InputOTPSlot index={0} />
|
|
||||||
// <InputOTPSlot index={1} />
|
|
||||||
// <InputOTPSlot index={2} />
|
|
||||||
// <InputOTPSlot index={3} />
|
|
||||||
// <InputOTPSlot index={4} />
|
|
||||||
// <InputOTPSlot index={5} />
|
|
||||||
// </InputOTPGroup>
|
|
||||||
// </InputOTP>
|
|
||||||
// <Button type="button" variant="secondary" onClick={handleSendCode}>获取验证码</Button>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// <Button type="submit" className="w-full">
|
|
||||||
// 注册并登录
|
|
||||||
// </Button>
|
|
||||||
// </>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
interface LoginHeaderProps {
|
|
||||||
h1?: string;
|
|
||||||
h2?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoginHeader({ h1, h2 }: LoginHeaderProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col items-center text-center">
|
|
||||||
<h1 className="text-2xl font-bold">{h1 || '欢迎回来'}</h1>
|
|
||||||
<p className="text-muted-foreground text-balance">
|
|
||||||
{h2 || '登陆到您的账户'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { AuthAPI } from "@/lib/api/client";
|
|
||||||
import LoginHeader from "./LoginHeader";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { startAuthentication } from "@simplewebauthn/browser";
|
|
||||||
|
|
||||||
export default function PasskeyLoginPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LoginHeader h2="使用通行证登录到您的账户" />
|
|
||||||
<div className="h-37.5 flex justify-center items-center">
|
|
||||||
<span className="text-sm text-zinc-500 border rounded-2xl p-5">还没想好这里放点啥捏~</span>
|
|
||||||
</div>
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleSubmit() {
|
|
||||||
const optionsJSON = await AuthAPI.getLoginByPasskeyOptions();
|
|
||||||
const credentialResponse = await startAuthentication({ optionsJSON });
|
|
||||||
return AuthAPI.loginByPasskey(credentialResponse);
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
|
|
||||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
|
||||||
import { useState, useCallback } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import LoginHeader from "./LoginHeader";
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { HumanVerification } from "@/components/human-verification";
|
|
||||||
import { AuthAPI, SmsAPI } from "@/lib/api/client";
|
|
||||||
import { handleAPIError } from "@/lib/api/common";
|
|
||||||
|
|
||||||
export default function SmsLoginMode() {
|
|
||||||
const [phone, setPhone] = useState("");
|
|
||||||
const handleSendCode = useCallback(async () => {
|
|
||||||
await SmsAPI.sendLoginSms(phone)
|
|
||||||
.then(() => toast.success('验证码已发送!'))
|
|
||||||
.catch(e => handleAPIError(e, ({ message }) => toast.error(`${message}`)))
|
|
||||||
}, [phone]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LoginHeader />
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<Label htmlFor="phone">手机号</Label>
|
|
||||||
<Input
|
|
||||||
id="phone-login-mode-phone"
|
|
||||||
name="phone"
|
|
||||||
type="text"
|
|
||||||
placeholder="+86 手机号"
|
|
||||||
value={phone}
|
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
|
||||||
required />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<div className="flex items-center h-4">
|
|
||||||
<Label htmlFor="code">验证码</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1 overflow-hidden items-center flex-row-reverse">
|
|
||||||
<HumanVerification onSuccess={handleSendCode} >
|
|
||||||
<Button type="button" variant="secondary" disabled>
|
|
||||||
获取验证码
|
|
||||||
</Button>
|
|
||||||
</HumanVerification>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<InputOTP
|
|
||||||
id="phone-login-mode-code"
|
|
||||||
name="code"
|
|
||||||
maxLength={6}
|
|
||||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<InputOTPGroup className="w-full flex justify-between">
|
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<InputOTPSlot
|
|
||||||
key={i}
|
|
||||||
index={i}
|
|
||||||
className="flex-1 aspect-square"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button type="submit" className="w-full" disabled>
|
|
||||||
控制台还在施工,暂不开放注册功能噢~
|
|
||||||
</Button>
|
|
||||||
<div className="hidden" aria-hidden>
|
|
||||||
oi! 当你看到这里,如果有兴趣可以试试绕过前端校验进行注册,后端接口没封,不过登陆之后也确实没啥好玩的
|
|
||||||
玩的开心,如果发现重大漏洞,请手下留情喔~网站右下角有我的邮箱,告之后将以“哈基米(BNB)”答谢~
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleSubmit(formData: FormData) {
|
|
||||||
const phone = formData.get('phone')?.toString() || '';
|
|
||||||
const code = formData.get('code')?.toString() || '';
|
|
||||||
|
|
||||||
return AuthAPI.loginBySms(phone, code)
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
|
||||||
import { ThemeProvider } from "../components/theme-provider";
|
|
||||||
import { Toaster } from "sonner";
|
|
||||||
import { UserAPI } from "@/lib/api/server";
|
|
||||||
import { ClientProvider } from "./ClientProvider";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "特恩的日志",
|
|
||||||
description: "一名在各个领域反复横跳的程序员",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
const user = await UserAPI.me().catch(() => null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<html lang="zh-CN" suppressHydrationWarning>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<body
|
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`}
|
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
<ClientProvider initialUser={user}>
|
|
||||||
<ThemeProvider
|
|
||||||
attribute="class"
|
|
||||||
defaultTheme="system"
|
|
||||||
enableSystem
|
|
||||||
disableTransitionOnChange
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<Toaster />
|
|
||||||
</ThemeProvider>
|
|
||||||
</ClientProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { MetadataRoute } from 'next'
|
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
|
||||||
return {
|
|
||||||
rules: {
|
|
||||||
userAgent: '*',
|
|
||||||
allow: '/',
|
|
||||||
disallow: '/console',
|
|
||||||
},
|
|
||||||
sitemap: 'https://www.tonesc.cn/sitemap.xml',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { BlogAPI } from '@/lib/api/server'
|
|
||||||
import { base62 } from '@/lib/utils'
|
|
||||||
import { MetadataRoute } from 'next'
|
|
||||||
|
|
||||||
export const revalidate = 3600;
|
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
||||||
// 获取所有博客
|
|
||||||
const blogs = await BlogAPI.list().catch(() => [])
|
|
||||||
|
|
||||||
const blogUrls = blogs.map(blog => {
|
|
||||||
const cleanId = blog.id.replace(/-/g, '')
|
|
||||||
const encoded = base62.encode(Buffer.from(cleanId, 'hex'))
|
|
||||||
return {
|
|
||||||
url: `https://www.tonesc.cn/blog/${encoded}`,
|
|
||||||
lastModified: new Date(blog.updatedAt),
|
|
||||||
changeFrequency: 'weekly' as const,
|
|
||||||
priority: 0.8,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 静态页面
|
|
||||||
const staticUrls = [
|
|
||||||
{
|
|
||||||
url: 'https://www.tonesc.cn/',
|
|
||||||
lastModified: new Date(),
|
|
||||||
changeFrequency: 'yearly' as const,
|
|
||||||
priority: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: 'https://www.tonesc.cn/blog',
|
|
||||||
lastModified: new Date(),
|
|
||||||
changeFrequency: 'daily' as const,
|
|
||||||
priority: 0.9,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: 'https://www.tonesc.cn/resource',
|
|
||||||
lastModified: new Date(),
|
|
||||||
changeFrequency: 'monthly' as const,
|
|
||||||
priority: 0.7,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return [...staticUrls, ...blogUrls]
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import { Mail } from "lucide-react";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
|
|
||||||
const EMAIL = "tonesc.cn@gmail.com";
|
|
||||||
|
|
||||||
export default function Footer() {
|
|
||||||
return (
|
|
||||||
<footer className="border-t border-zinc-300">
|
|
||||||
<div className="bg-zinc-50 px-4 py-3 md:py-5 sm:px-10 md:px-20 flex flex-col sm:flex-row justify-between items-center gap-4 transition-all">
|
|
||||||
{/* 版权与备案信息 */}
|
|
||||||
<div className="text-center sm:text-left">
|
|
||||||
<a
|
|
||||||
href="https://beian.miit.gov.cn/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block text-sm text-zinc-500 hover:text-zinc-700 hover:underline focus:outline-none focus:underline"
|
|
||||||
>
|
|
||||||
备案号:渝ICP备2023009516号-1
|
|
||||||
</a>
|
|
||||||
<p className="mt-1 text-sm text-zinc-500">
|
|
||||||
© {new Date().getFullYear()} TONE Page. All rights reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 联系方式弹出框 */}
|
|
||||||
<address className="not-italic">
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant='outline' size='sm' >
|
|
||||||
<Mail className="text-zinc-600" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-fit">
|
|
||||||
<a
|
|
||||||
href={`mailto:${EMAIL}`}
|
|
||||||
className="text-sm text-zinc-800 hover:underline focus:outline-none focus:underline"
|
|
||||||
>
|
|
||||||
{EMAIL}
|
|
||||||
</a>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</address>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
} from "@/components/ui/drawer"
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { useUserStore } from "@/store/useUserStore";
|
|
||||||
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
{ name: '特恩(TONE)', path: '/' },
|
|
||||||
{ name: '资源', path: '/resource' },
|
|
||||||
{ name: '博客', path: '/blog' },
|
|
||||||
{ name: '控制台', path: '/console' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, path: string) => {
|
|
||||||
if (path === '/console') {
|
|
||||||
e.preventDefault();
|
|
||||||
router.push(userStore.user ? '/console' : '/console/login');
|
|
||||||
setShowMenu(false);
|
|
||||||
} else {
|
|
||||||
setShowMenu(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showMenu && menuButtonRef.current) {
|
|
||||||
menuButtonRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [showMenu]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<header className="sticky top-0 z-50 backdrop-blur-sm bg-white/40 shadow" role="banner" aria-label="网站顶部导航栏">
|
|
||||||
<div className="flex items-center justify-between px-10 md:h-18 md:px-20 h-14 duration-300" aria-label="主菜单">
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className={cn(
|
|
||||||
"cursor-pointer font-medium text-zinc-500 hover:text-zinc-800 border-b-4 border-transparent duration-200",
|
|
||||||
pathname === "/" && "text-zinc-800"
|
|
||||||
)}
|
|
||||||
aria-current={pathname === "/" ? "page" : undefined}
|
|
||||||
>
|
|
||||||
<span className="sr-only">特恩(TONE)</span>
|
|
||||||
{pathname === "/"
|
|
||||||
? <span className="text-2xl" aria-hidden="true" >🍭</span>
|
|
||||||
: <span className="md:text-lg" aria-hidden="true">特恩(TONE)</span>}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<nav className={cn(
|
|
||||||
"items-center gap-12 hidden sm:flex",
|
|
||||||
)}>
|
|
||||||
{menuItems.slice(1).map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
href={item.path}
|
|
||||||
className={cn(
|
|
||||||
"cursor-pointer md:text-lg font-medium text-zinc-500 hover:text-zinc-800 border-b-4 border-transparent duration-200",
|
|
||||||
pathname.startsWith(item.path) && "text-zinc-800 border-b-pink-500"
|
|
||||||
)}
|
|
||||||
onClick={e => handleClick(e, item.path)}
|
|
||||||
aria-current={pathname === item.path ? "page" : undefined}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<button
|
|
||||||
ref={menuButtonRef}
|
|
||||||
className="sm:hidden text-zinc-600"
|
|
||||||
onClick={() => setShowMenu(true)}
|
|
||||||
aria-label="打开主菜单"
|
|
||||||
>菜单</button>
|
|
||||||
</div>
|
|
||||||
</header >
|
|
||||||
|
|
||||||
<Drawer direction="right" open={showMenu} onOpenChange={setShowMenu}>
|
|
||||||
<DrawerContent>
|
|
||||||
<DrawerHeader>
|
|
||||||
<DrawerTitle className="flex justify-between">
|
|
||||||
<span>菜单</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowMenu(false)}
|
|
||||||
aria-label="关闭菜单"
|
|
||||||
>
|
|
||||||
<X className="size-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</DrawerTitle>
|
|
||||||
<DrawerDescription>请选择需要前往的页面</DrawerDescription>
|
|
||||||
</DrawerHeader>
|
|
||||||
<nav className="w-full flex flex-col px-4 gap-2" aria-label="移动设备主菜单">
|
|
||||||
{menuItems.slice(1).map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
href={item.path}
|
|
||||||
onClick={e => handleClick(e, item.path)}
|
|
||||||
aria-current={pathname === item.path ? "page" : undefined}
|
|
||||||
>
|
|
||||||
<Button className="w-full" size='lg'
|
|
||||||
variant={pathname.startsWith(item.path) ? 'default' : 'outline'}
|
|
||||||
>{item.name}</Button>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
import { ReactNode, useCallback, useEffect, useState } from "react"
|
|
||||||
|
|
||||||
interface HumanVerificationProps {
|
|
||||||
open?: boolean;
|
|
||||||
onOpenChange?: (open: boolean) => void;
|
|
||||||
onSuccess?: () => void;
|
|
||||||
onFail?: (reason?: string) => void;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HumanVerification({ open, onOpenChange, onSuccess, children }: HumanVerificationProps) {
|
|
||||||
const [i_open, i_setOpen] = useState(false);
|
|
||||||
const setOpen = useCallback((o: boolean) => {
|
|
||||||
i_setOpen(o);
|
|
||||||
onOpenChange?.(o);
|
|
||||||
}, [onOpenChange]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (i_open) {
|
|
||||||
setOpen(false);
|
|
||||||
onSuccess?.();
|
|
||||||
}
|
|
||||||
}, [i_open, onSuccess, setOpen]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open ?? i_open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
{children}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-80">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>人机验证</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
请拖动滑块以使得图片水平
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
||||||
return (
|
|
||||||
<fieldset
|
|
||||||
data-slot="field-set"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-6",
|
|
||||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLegend({
|
|
||||||
className,
|
|
||||||
variant = "legend",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
|
||||||
return (
|
|
||||||
<legend
|
|
||||||
data-slot="field-legend"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"mb-3 font-medium",
|
|
||||||
"data-[variant=legend]:text-base",
|
|
||||||
"data-[variant=label]:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-group"
|
|
||||||
className={cn(
|
|
||||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldVariants = cva(
|
|
||||||
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
orientation: {
|
|
||||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
||||||
horizontal: [
|
|
||||||
"flex-row items-center",
|
|
||||||
"[&>[data-slot=field-label]]:flex-auto",
|
|
||||||
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
||||||
],
|
|
||||||
responsive: [
|
|
||||||
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
|
||||||
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
||||||
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
orientation: "vertical",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Field({
|
|
||||||
className,
|
|
||||||
orientation = "vertical",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="field"
|
|
||||||
data-orientation={orientation}
|
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-content"
|
|
||||||
className={cn(
|
|
||||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Label>) {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
||||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
||||||
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="field-description"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
||||||
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
children?: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-separator"
|
|
||||||
data-content={!!children}
|
|
||||||
className={cn(
|
|
||||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
|
||||||
{children && (
|
|
||||||
<span
|
|
||||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
|
||||||
data-slot="field-separator-content"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldError({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
errors,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
errors?: Array<{ message?: string } | undefined>
|
|
||||||
}) {
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (children) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!errors?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueErrors = [
|
|
||||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (uniqueErrors?.length == 1) {
|
|
||||||
return uniqueErrors[0]?.message
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
||||||
{uniqueErrors.map(
|
|
||||||
(error, index) =>
|
|
||||||
error?.message && <li key={index}>{error.message}</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}, [children, errors])
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
data-slot="field-error"
|
|
||||||
className={cn("text-destructive text-sm font-normal", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Field,
|
|
||||||
FieldLabel,
|
|
||||||
FieldDescription,
|
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldContent,
|
|
||||||
FieldTitle,
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// import { UserAPI } from "@/lib/api/client";
|
|
||||||
// import useSWR from "swr";
|
|
||||||
|
|
||||||
// export function useUserMe({ onError }: { onError?: (e: any) => void } = {}) {
|
|
||||||
// const isClientSide = typeof window !== 'undefined';
|
|
||||||
|
|
||||||
// const { data: user, isLoading, error } = useSWR(
|
|
||||||
// '/api/user/me',
|
|
||||||
// async () => {
|
|
||||||
// if (isClientSide && !localStorage.getItem('token')) {
|
|
||||||
// throw Object.assign(new Error('未登录'), { statusCode: -1 });
|
|
||||||
// }
|
|
||||||
// return UserAPI.me();
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// onError: (error) => {
|
|
||||||
// if (error.statusCode === 401) {
|
|
||||||
// if (isClientSide) {
|
|
||||||
// localStorage.removeItem('token');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// onError?.(error);
|
|
||||||
// },
|
|
||||||
// revalidateIfStale: false,
|
|
||||||
// revalidateOnFocus: false,
|
|
||||||
// shouldRetryOnError: (err) => {
|
|
||||||
// if ([-1, 401].includes(err.statusCode)) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// return true;
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// user,
|
|
||||||
// isLoading,
|
|
||||||
// error
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { APIResponse, HttpMethod, normalizeAPIError } from './common';
|
|
||||||
|
|
||||||
interface ClientFetchRequestOptions extends RequestInit {
|
|
||||||
method?: HttpMethod;
|
|
||||||
body?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function clientFetch<T = unknown>(
|
|
||||||
endpoint: string,
|
|
||||||
options: ClientFetchRequestOptions = {}
|
|
||||||
): Promise<T> {
|
|
||||||
const defaultHeaders: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: options.method || 'GET',
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
body: options.body ?? JSON.stringify(options.body),
|
|
||||||
credentials: 'include',
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw JSON.parse(errorText);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: APIResponse<T> = await response.json();
|
|
||||||
|
|
||||||
if (!data.success) {
|
|
||||||
throw data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.data as T;
|
|
||||||
} catch (error) {
|
|
||||||
normalizeAPIError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export * as AuthAPI from './endpoints/auth.client'
|
|
||||||
export * as UserAPI from './endpoints/user.client'
|
|
||||||
export * as SmsAPI from './endpoints/sms.client'
|
|
||||||
export * as AdminAPI from './endpoints/admin.client'
|
|
||||||
export * as OSSAPI from './endpoints/oss.client'
|
|
||||||
export * as BlogAPI from './endpoints/blog.client'
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
||||||
|
|
||||||
export interface APIResponse<T = unknown> {
|
|
||||||
success: boolean;
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class APIError extends Error {
|
|
||||||
constructor(
|
|
||||||
message: string,
|
|
||||||
public status: number = 400,
|
|
||||||
public code: number = -1,
|
|
||||||
public data: unknown = null
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeAPIError(error: unknown): never {
|
|
||||||
if (error instanceof APIError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new APIError(
|
|
||||||
error.message || '未知错误',
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === 'object' && error !== null) {
|
|
||||||
const { message, status, code, data } = {
|
|
||||||
message: '未知错误',
|
|
||||||
status: 400,
|
|
||||||
code: -1,
|
|
||||||
data: null,
|
|
||||||
...error
|
|
||||||
};
|
|
||||||
|
|
||||||
throw new APIError(
|
|
||||||
message,
|
|
||||||
status,
|
|
||||||
code,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new APIError((error instanceof Error ? `${error.message}` : '') || '未知错误', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleAPIError(error: unknown, handler: (e: APIError) => void): void {
|
|
||||||
if (error instanceof APIError) {
|
|
||||||
return handler(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
normalizeAPIError(error)
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof APIError) {
|
|
||||||
return handler(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GeneralErrorHandler(e: APIError) {
|
|
||||||
toast.error(`${e.message}`)
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
import { Resource } from "@/lib/types/resource";
|
|
||||||
import { clientFetch } from "../client";
|
|
||||||
import { Blog } from "@/lib/types/blog";
|
|
||||||
import { BlogPermission } from "@/lib/types/Blog.Permission.enum";
|
|
||||||
import { Role } from "@/lib/types/role";
|
|
||||||
|
|
||||||
export interface UserEntity {
|
|
||||||
userId: string;
|
|
||||||
username: string;
|
|
||||||
nickname: string;
|
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
avatar?: string;
|
|
||||||
createdAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
roles: Role[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== Resource ========
|
|
||||||
export async function listResources() {
|
|
||||||
return clientFetch<Resource[]>('/api/admin/web/resource')
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateResourceParams {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
imageUrl: string;
|
|
||||||
link: string;
|
|
||||||
tags: {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
export async function createResource(data: CreateResourceParams) {
|
|
||||||
data.title = data.title.trim();
|
|
||||||
data.description = data.description.trim();
|
|
||||||
data.imageUrl = data.imageUrl.trim();
|
|
||||||
data.link = data.link.trim();
|
|
||||||
for (const tag of data.tags) {
|
|
||||||
tag.name = tag.name.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientFetch('/api/admin/web/resource', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getResource(id: string) {
|
|
||||||
return clientFetch<Resource>(`/api/admin/web/resource/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeResource(id: string) {
|
|
||||||
return clientFetch<void>(`/api/admin/web/resource/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateResourceParams {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
imageUrl: string;
|
|
||||||
link: string;
|
|
||||||
tags: {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
export async function updateResource(id: string, data: UpdateResourceParams) {
|
|
||||||
data.title = data.title.trim();
|
|
||||||
data.description = data.description.trim();
|
|
||||||
data.imageUrl = data.imageUrl.trim();
|
|
||||||
data.link = data.link.trim();
|
|
||||||
for (const tag of data.tags) {
|
|
||||||
tag.name = tag.name.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientFetch<Resource>(`/api/admin/web/resource/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ======== Blog ========
|
|
||||||
interface CreateBlogParams {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
contentUrl: string;
|
|
||||||
}
|
|
||||||
export async function createBlog(data: CreateBlogParams) {
|
|
||||||
return clientFetch<Blog>('/api/admin/web/blog', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBlog(id: string) {
|
|
||||||
return clientFetch<Blog>(`/api/admin/web/blog/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listBlogs() {
|
|
||||||
return clientFetch<Blog[]>('/api/admin/web/blog')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeBlog(id: string) {
|
|
||||||
// ? Blog
|
|
||||||
return clientFetch<Blog>(`/api/admin/web/blog/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface UpdateBlogParams {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
contentUrl: string;
|
|
||||||
permissions: BlogPermission[],
|
|
||||||
}
|
|
||||||
export async function updateBlog(id: string, data: UpdateBlogParams) {
|
|
||||||
data.title = data.title.trim();
|
|
||||||
data.description = data.description.trim();
|
|
||||||
data.contentUrl = data.contentUrl.trim();
|
|
||||||
|
|
||||||
return clientFetch<Blog>(`/api/admin/web/blog/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setBlogPassword(id: string, password: string) {
|
|
||||||
password = password.trim();
|
|
||||||
return clientFetch<boolean>(`/api/admin/web/blog/${id}/password`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ======== User ========
|
|
||||||
interface CreateUserParams {
|
|
||||||
username: string | null;
|
|
||||||
nickname: string | null;
|
|
||||||
email: string | null;
|
|
||||||
phone: string | null;
|
|
||||||
password: string | null;
|
|
||||||
}
|
|
||||||
export async function createUser(data: CreateUserParams) {
|
|
||||||
type Keys = keyof CreateUserParams;
|
|
||||||
for (const key in data) {
|
|
||||||
data[key as Keys] = data[key as Keys]?.trim() || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientFetch<UserEntity>("/api/admin/user", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUser(id: string) {
|
|
||||||
return clientFetch<UserEntity>(`/api/admin/user/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserListParams {
|
|
||||||
page?: number
|
|
||||||
pageSize?: number
|
|
||||||
}
|
|
||||||
export interface UserListResponse {
|
|
||||||
items: UserEntity[],
|
|
||||||
total: number
|
|
||||||
page: number
|
|
||||||
pageSize: number
|
|
||||||
}
|
|
||||||
export function listUsers(params?: UserListParams): Promise<UserListResponse> {
|
|
||||||
const searchParams = new URLSearchParams()
|
|
||||||
if (params?.page) searchParams.set('page', params.page.toString())
|
|
||||||
if (params?.pageSize) searchParams.set('pageSize', params.pageSize.toString())
|
|
||||||
|
|
||||||
return clientFetch<UserListResponse>('/api/admin/user')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeUser(userId: string, soft: boolean = true) {
|
|
||||||
/** 我也不知道后端返回的是个啥...没报错就当成功吧 */
|
|
||||||
return clientFetch<unknown>(`/api/admin/user/${userId}?soft=${soft}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setUserPassword(userId: string, password: string) {
|
|
||||||
password = password.trim();
|
|
||||||
|
|
||||||
return clientFetch<void>(`/api/admin/user/${userId}/password`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
password,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface updateUser {
|
|
||||||
username: string;
|
|
||||||
nickname: string;
|
|
||||||
email: string | null;
|
|
||||||
phone: string | null;
|
|
||||||
}
|
|
||||||
export async function updateUser(userId: string, user: updateUser) {
|
|
||||||
user.username = user.username.trim();
|
|
||||||
user.nickname = user.nickname.trim();
|
|
||||||
user.email = user.email?.trim() || null;
|
|
||||||
user.phone = user.phone?.trim() || null;
|
|
||||||
|
|
||||||
return clientFetch<UserEntity>(`/api/admin/user/${userId}`, {
|
|
||||||
body: JSON.stringify(user),
|
|
||||||
method: "PUT",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { User } from "@/lib/types/user";
|
|
||||||
import { clientFetch } from "../client";
|
|
||||||
import { APIError } from "../common";
|
|
||||||
import { AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON } from "@simplewebauthn/browser";
|
|
||||||
|
|
||||||
export async function loginByPassword(identifier: string, password: string) {
|
|
||||||
identifier = identifier.trim();
|
|
||||||
password = password.trim();
|
|
||||||
if (identifier.length === 0 || password.length === 0) {
|
|
||||||
throw new APIError('请输入账户和密码')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier.length < 1 || identifier.length > 254) {
|
|
||||||
throw new APIError('账户长度只能为1~254位')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 6 || password.length > 32) {
|
|
||||||
throw new APIError('密码长度只能为6~32位')
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientFetch<{ user: User }>('/api/auth/login/password', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
identifier,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loginBySms(phone: string, code: string) {
|
|
||||||
phone = phone.trim();
|
|
||||||
code = code.trim();
|
|
||||||
if (phone.length === 0 || code.length === 0) {
|
|
||||||
throw new APIError('请输入手机号及短信验证码')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
|
||||||
throw new APIError('请输入合法的中国大陆手机号');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (! /\d{6}/.test(code)) {
|
|
||||||
throw new APIError('密码长度只能为6~32位')
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientFetch<{ user: User }>('/api/auth/login/sms', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
phone,
|
|
||||||
code,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function logout() {
|
|
||||||
return clientFetch('/api/auth/logout', { method: 'POST' });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ======== PassKey ========
|
|
||||||
export async function getPasskeyRegisterOptions() {
|
|
||||||
return clientFetch<PublicKeyCredentialCreationOptionsJSON>('/api/auth/passkey/register/options', {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function passkeyRegister(name: string, credentialResponse: RegistrationResponseJSON) {
|
|
||||||
name = name.trim();
|
|
||||||
if (name.length === 0) {
|
|
||||||
throw new APIError('通行证名称不得为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientFetch<{ id: string; name: string; createdAt: string }>('/api/auth/passkey/register', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
name,
|
|
||||||
credentialResponse,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLoginByPasskeyOptions() {
|
|
||||||
return clientFetch<PublicKeyCredentialRequestOptionsJSON>('/api/auth/passkey/login/options', {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @lint-ignore */
|
|
||||||
export async function loginByPasskey(credentialResponse: AuthenticationResponseJSON) {
|
|
||||||
return clientFetch<{ user: User }>('/api/auth/passkey/login', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
credentialResponse,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { BlogComment } from "@/lib/types/blogComment";
|
|
||||||
import { clientFetch } from "../client";
|
|
||||||
|
|
||||||
export async function getBlog(id: string, password?: string) {
|
|
||||||
return clientFetch<{
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
createdAt: string;
|
|
||||||
content: string;
|
|
||||||
}>(`/api/blog/${id}` + (password ? `?p=${password}` : ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getComments(id: string) {
|
|
||||||
return clientFetch<BlogComment[]>(`/api/blog/${id}/comments`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createComment(blogId: string, content: string, parentId?: string) {
|
|
||||||
return clientFetch<BlogComment>(`/api/blog/${blogId}/comment`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
content,
|
|
||||||
parentId: parentId || null,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Blog } from "@/lib/types/blog";
|
|
||||||
import { serverFetch } from "../server";
|
|
||||||
|
|
||||||
export async function list() {
|
|
||||||
return serverFetch<Pick<Blog,
|
|
||||||
'id' | 'title' | 'description' | 'viewCount' | 'createdAt' | 'updatedAt' | 'deletedAt'
|
|
||||||
>[]>('/api/blog')
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { PublicResource } from "@/lib/types/resource";
|
|
||||||
import { serverFetch } from "../server";
|
|
||||||
|
|
||||||
export async function list() {
|
|
||||||
return serverFetch<PublicResource[]>('/api/resource')
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { clientFetch } from "../client";
|
|
||||||
import { APIError } from "../common";
|
|
||||||
|
|
||||||
export async function sendLoginSms(phone: string) {
|
|
||||||
phone = phone.trim();
|
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
|
||||||
throw new APIError('请输入合法的中国大陆手机号');
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientFetch('/api/sms/send/login', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ phone })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { User } from "@/lib/types/user";
|
|
||||||
import { clientFetch } from "../client";
|
|
||||||
import { APIError } from "../common";
|
|
||||||
|
|
||||||
export async function me() {
|
|
||||||
return clientFetch<User>('/api/user/me');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updatePassword(password: string) {
|
|
||||||
if (! /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/.test(password)) {
|
|
||||||
throw new APIError('新密码不符合规范,请重新输入')
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientFetch<null>('/api/user/password', {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({
|
|
||||||
password,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { User } from "@/lib/types/user";
|
|
||||||
import { serverFetch } from "../server";
|
|
||||||
|
|
||||||
export async function me() {
|
|
||||||
return serverFetch<User>('/api/user/me');
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { cookies, headers } from 'next/headers';
|
|
||||||
import { APIResponse, HttpMethod, normalizeAPIError } from './common';
|
|
||||||
|
|
||||||
interface ServerFetchRequestOptions extends RequestInit {
|
|
||||||
method?: HttpMethod;
|
|
||||||
body?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function serverFetch<T = unknown>(
|
|
||||||
endpoint: string,
|
|
||||||
options: ServerFetchRequestOptions = {}
|
|
||||||
): Promise<T> {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
const cookieHeader = cookieStore.toString();
|
|
||||||
|
|
||||||
const reqHeaders = new Headers(await headers());
|
|
||||||
const forwardedHeaders: Record<string, string> = {};
|
|
||||||
['user-agent', 'x-forwarded-for', 'x-real-ip'].forEach((key) => {
|
|
||||||
const value = reqHeaders.get(key);
|
|
||||||
if (value) forwardedHeaders[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultHeaders: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
|
||||||
...forwardedHeaders,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(new URL(endpoint, process.env.API_BASE).href, {
|
|
||||||
method: options.method || 'GET',
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
body: options.body ?? JSON.stringify(options.body),
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw JSON.parse(errorText);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: APIResponse<T> = await response.json();
|
|
||||||
if (!data.success) {
|
|
||||||
throw data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.data as T;
|
|
||||||
} catch (error) {
|
|
||||||
normalizeAPIError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export * as BlogAPI from './endpoints/blog.server';
|
|
||||||
export * as ResourceAPI from './endpoints/resource.server';
|
|
||||||
export * as UserAPI from './endpoints/user.server';
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tone-page-web",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev --turbopack",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
|
||||||
"@radix-ui/react-menubar": "^1.1.16",
|
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
|
||||||
"@types/ali-oss": "^6.16.13",
|
|
||||||
"add": "^2.0.6",
|
|
||||||
"alert": "^6.0.2",
|
|
||||||
"ali-oss": "^6.23.0",
|
|
||||||
"badge": "^1.0.3",
|
|
||||||
"base-x": "^5.0.1",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"dialog": "^0.3.1",
|
|
||||||
"drawer": "^0.0.2",
|
|
||||||
"highlight.js": "^11.11.1",
|
|
||||||
"input-otp": "^1.4.2",
|
|
||||||
"lucide-react": "^0.503.0",
|
|
||||||
"next": "15.3.1",
|
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"pagination": "^0.4.6",
|
|
||||||
"popover": "^2.4.1",
|
|
||||||
"proxy-agent": "^6.5.0",
|
|
||||||
"react": "^19.2.3",
|
|
||||||
"react-dom": "^19.2.3",
|
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"react-photo-view": "^1.2.7",
|
|
||||||
"rehype-highlight": "^7.0.2",
|
|
||||||
"rehype-raw": "^7.0.0",
|
|
||||||
"remark-gfm": "^4.0.1",
|
|
||||||
"select": "^1.1.2",
|
|
||||||
"sonner": "^2.0.7",
|
|
||||||
"swr": "^2.3.7",
|
|
||||||
"tailwind-merge": "^3.4.0",
|
|
||||||
"textarea": "^0.3.0",
|
|
||||||
"vaul": "^1.1.2",
|
|
||||||
"zod": "^3.25.76",
|
|
||||||
"zustand": "^5.0.9"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/eslintrc": "^3.3.3",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
|
||||||
"@types/node": "^20.19.26",
|
|
||||||
"@types/react": "^19.2.7",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"@types/webappsec-credential-management": "^0.6.9",
|
|
||||||
"eslint": "^9.39.1",
|
|
||||||
"eslint-config-next": "15.3.1",
|
|
||||||
"tailwindcss": "^4.1.18",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
|
||||||
"typescript": "^5.9.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 418 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 126 KiB |
@@ -1,27 +0,0 @@
|
|||||||
// store/useUserStore.ts
|
|
||||||
import { User } from '@/lib/types/user';
|
|
||||||
import { create } from 'zustand';
|
|
||||||
import { devtools } from 'zustand/middleware';
|
|
||||||
|
|
||||||
interface UserState {
|
|
||||||
user: User | null;
|
|
||||||
isLoading: boolean;
|
|
||||||
initialized: boolean;
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
setInitialized: () => void;
|
|
||||||
setUser: (user: User | null) => void;
|
|
||||||
clearUser: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUserStore = create<UserState>()(
|
|
||||||
devtools((set, get) => ({
|
|
||||||
user: null,
|
|
||||||
isLoading: false,
|
|
||||||
initialized: false,
|
|
||||||
|
|
||||||
setInitialized: () => set({ initialized: true }),
|
|
||||||
setUser: (user) => set({ user, isLoading: false }),
|
|
||||||
clearUser: () => set({ user: null }),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
@@ -17,33 +17,30 @@
|
|||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
"migration:generate": "typeorm migration:generate -d src/data-source.ts",
|
|
||||||
"migration:run": "typeorm migration:run -d dist/data-source.js",
|
|
||||||
"migration:revert": "typeorm migration:revert -d dist/data-source.js"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alicloud/credentials": "^2.4.3",
|
"@alicloud/credentials": "^2.4.3",
|
||||||
"@alicloud/dm20151123": "1.2.6",
|
"@alicloud/dm20151123": "1.2.6",
|
||||||
"@alicloud/dypnsapi20170525": "^2.0.0",
|
|
||||||
"@alicloud/dysmsapi20170525": "4.1.0",
|
"@alicloud/dysmsapi20170525": "4.1.0",
|
||||||
"@alicloud/openapi-client": "^0.4.14",
|
"@alicloud/openapi-client": "^0.4.14",
|
||||||
"@alicloud/tea-util": "^1.4.10",
|
"@alicloud/tea-util": "^1.4.10",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/jwt": "^11.0.0",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
"@nestjs/throttler": "^6.4.0",
|
"@nestjs/throttler": "^6.4.0",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"@simplewebauthn/server": "^13.2.2",
|
|
||||||
"@types/ali-oss": "^6.16.11",
|
"@types/ali-oss": "^6.16.11",
|
||||||
"ali-oss": "^6.23.0",
|
"ali-oss": "^6.23.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"cookie-parser": "^1.4.7",
|
|
||||||
"dotenv": "^17.2.3",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.15.6",
|
"pg": "^8.15.6",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
@@ -54,10 +51,10 @@
|
|||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
"@types/cookie-parser": "^1.4.10",
|
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
@@ -14,9 +14,6 @@ importers:
|
|||||||
'@alicloud/dm20151123':
|
'@alicloud/dm20151123':
|
||||||
specifier: 1.2.6
|
specifier: 1.2.6
|
||||||
version: 1.2.6
|
version: 1.2.6
|
||||||
'@alicloud/dypnsapi20170525':
|
|
||||||
specifier: ^2.0.0
|
|
||||||
version: 2.0.0
|
|
||||||
'@alicloud/dysmsapi20170525':
|
'@alicloud/dysmsapi20170525':
|
||||||
specifier: 4.1.0
|
specifier: 4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@@ -35,9 +32,15 @@ importers:
|
|||||||
'@nestjs/core':
|
'@nestjs/core':
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.4.17(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 10.4.17(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/jwt':
|
||||||
|
specifier: ^11.0.0
|
||||||
|
version: 11.0.0(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))
|
||||||
'@nestjs/mapped-types':
|
'@nestjs/mapped-types':
|
||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: 2.1.0(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
|
version: 2.1.0(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
|
||||||
|
'@nestjs/passport':
|
||||||
|
specifier: ^11.0.5
|
||||||
|
version: 11.0.5(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
||||||
'@nestjs/platform-express':
|
'@nestjs/platform-express':
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.4.17(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.17)
|
version: 10.4.17(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.17)
|
||||||
@@ -47,9 +50,6 @@ importers:
|
|||||||
'@nestjs/typeorm':
|
'@nestjs/typeorm':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.0.0(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.22(pg@8.15.6)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@20.17.31)(typescript@5.8.3)))
|
version: 11.0.0(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.22(pg@8.15.6)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@20.17.31)(typescript@5.8.3)))
|
||||||
'@simplewebauthn/server':
|
|
||||||
specifier: ^13.2.2
|
|
||||||
version: 13.2.2
|
|
||||||
'@types/ali-oss':
|
'@types/ali-oss':
|
||||||
specifier: ^6.16.11
|
specifier: ^6.16.11
|
||||||
version: 6.16.11
|
version: 6.16.11
|
||||||
@@ -62,15 +62,15 @@ importers:
|
|||||||
class-validator:
|
class-validator:
|
||||||
specifier: ^0.14.2
|
specifier: ^0.14.2
|
||||||
version: 0.14.2
|
version: 0.14.2
|
||||||
cookie-parser:
|
|
||||||
specifier: ^1.4.7
|
|
||||||
version: 1.4.7
|
|
||||||
dotenv:
|
|
||||||
specifier: ^17.2.3
|
|
||||||
version: 17.2.3
|
|
||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: ^9.0.2
|
specifier: ^9.0.2
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
|
passport:
|
||||||
|
specifier: ^0.7.0
|
||||||
|
version: 0.7.0
|
||||||
|
passport-jwt:
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.1
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.15.6
|
specifier: ^8.15.6
|
||||||
version: 8.15.6
|
version: 8.15.6
|
||||||
@@ -96,9 +96,6 @@ importers:
|
|||||||
'@nestjs/testing':
|
'@nestjs/testing':
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.4.17(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.17)(@nestjs/platform-express@10.4.17)
|
version: 10.4.17(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.17)(@nestjs/platform-express@10.4.17)
|
||||||
'@types/cookie-parser':
|
|
||||||
specifier: ^1.4.10
|
|
||||||
version: 1.4.10(@types/express@5.0.1)
|
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
@@ -108,6 +105,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.3.1
|
specifier: ^20.3.1
|
||||||
version: 20.17.31
|
version: 20.17.31
|
||||||
|
'@types/passport-jwt':
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.1
|
||||||
'@types/supertest':
|
'@types/supertest':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
@@ -159,9 +159,6 @@ packages:
|
|||||||
'@alicloud/credentials@2.4.3':
|
'@alicloud/credentials@2.4.3':
|
||||||
resolution: {integrity: sha512-r2thNtthchTz/c8/HryGSey1vY0UZx2FkAvb+vd+j7xhD/v/KUwnp8RJNQKNG3E4kfs4wSx2bgDSkcPAiXHQLQ==}
|
resolution: {integrity: sha512-r2thNtthchTz/c8/HryGSey1vY0UZx2FkAvb+vd+j7xhD/v/KUwnp8RJNQKNG3E4kfs4wSx2bgDSkcPAiXHQLQ==}
|
||||||
|
|
||||||
'@alicloud/credentials@2.4.4':
|
|
||||||
resolution: {integrity: sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q==}
|
|
||||||
|
|
||||||
'@alicloud/darabonba-array@0.1.0':
|
'@alicloud/darabonba-array@0.1.0':
|
||||||
resolution: {integrity: sha512-y4oM4O2uXiroUjfWBLEXRHMm1279rWpkWWNalF7DFQyO5awJ/e0d631prU4i10ytKzo8XJd12eCHmm3IOW85+g==}
|
resolution: {integrity: sha512-y4oM4O2uXiroUjfWBLEXRHMm1279rWpkWWNalF7DFQyO5awJ/e0d631prU4i10ytKzo8XJd12eCHmm3IOW85+g==}
|
||||||
|
|
||||||
@@ -183,9 +180,6 @@ packages:
|
|||||||
'@alicloud/dm20151123@1.2.6':
|
'@alicloud/dm20151123@1.2.6':
|
||||||
resolution: {integrity: sha512-6pYgy0D5zmUoxfRYwj0ysX4WPw8IfGimaw3ORFj6hF6lTxWpJ3tteOD72i8rw764eZ78TRc4UyET3U9qCaBeaA==}
|
resolution: {integrity: sha512-6pYgy0D5zmUoxfRYwj0ysX4WPw8IfGimaw3ORFj6hF6lTxWpJ3tteOD72i8rw764eZ78TRc4UyET3U9qCaBeaA==}
|
||||||
|
|
||||||
'@alicloud/dypnsapi20170525@2.0.0':
|
|
||||||
resolution: {integrity: sha512-eVh1dJ2HA82bBHt+YZFIBzPEYW80FK+TSpcxSR9o0W+FgfTqBaj6eeIHnN7NFhyDAD/3+HtZ146Pmvr51JEEAg==}
|
|
||||||
|
|
||||||
'@alicloud/dysmsapi20170525@4.1.0':
|
'@alicloud/dysmsapi20170525@4.1.0':
|
||||||
resolution: {integrity: sha512-oUmRp6DTI6gGNbrSQK4lW7EouHIB4C0DCbSEA121NvxHC9XKe4cqiPP2VDqgDQiIK43oiFaHKY3rj+IteOWekA==}
|
resolution: {integrity: sha512-oUmRp6DTI6gGNbrSQK4lW7EouHIB4C0DCbSEA121NvxHC9XKe4cqiPP2VDqgDQiIK43oiFaHKY3rj+IteOWekA==}
|
||||||
|
|
||||||
@@ -431,9 +425,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
|
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
'@hexagon/base64@1.1.28':
|
|
||||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
@@ -549,9 +540,6 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
'@levischuck/tiny-cbor@0.2.11':
|
|
||||||
resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
|
|
||||||
|
|
||||||
'@ljharb/through@2.3.14':
|
'@ljharb/through@2.3.14':
|
||||||
resolution: {integrity: sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==}
|
resolution: {integrity: sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -609,6 +597,11 @@ packages:
|
|||||||
'@nestjs/websockets':
|
'@nestjs/websockets':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nestjs/jwt@11.0.0':
|
||||||
|
resolution: {integrity: sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||||
|
|
||||||
'@nestjs/mapped-types@2.1.0':
|
'@nestjs/mapped-types@2.1.0':
|
||||||
resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==}
|
resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -622,6 +615,12 @@ packages:
|
|||||||
class-validator:
|
class-validator:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nestjs/passport@11.0.5':
|
||||||
|
resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||||
|
passport: ^0.5.0 || ^0.6.0 || ^0.7.0
|
||||||
|
|
||||||
'@nestjs/platform-express@10.4.17':
|
'@nestjs/platform-express@10.4.17':
|
||||||
resolution: {integrity: sha512-ovn4Wxney3QGBrqNPv0QLcCuH5QoAi6pb/GNWAz6B/NmBjZbs9/zl4a2beGDA2SaYre9w43YbfmHTm17PneP9w==}
|
resolution: {integrity: sha512-ovn4Wxney3QGBrqNPv0QLcCuH5QoAi6pb/GNWAz6B/NmBjZbs9/zl4a2beGDA2SaYre9w43YbfmHTm17PneP9w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -686,43 +685,6 @@ packages:
|
|||||||
'@paralleldrive/cuid2@2.2.2':
|
'@paralleldrive/cuid2@2.2.2':
|
||||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||||
|
|
||||||
'@peculiar/asn1-android@2.6.0':
|
|
||||||
resolution: {integrity: sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-cms@2.6.0':
|
|
||||||
resolution: {integrity: sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-csr@2.6.0':
|
|
||||||
resolution: {integrity: sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-ecc@2.6.0':
|
|
||||||
resolution: {integrity: sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-pfx@2.6.0':
|
|
||||||
resolution: {integrity: sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-pkcs8@2.6.0':
|
|
||||||
resolution: {integrity: sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-pkcs9@2.6.0':
|
|
||||||
resolution: {integrity: sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-rsa@2.6.0':
|
|
||||||
resolution: {integrity: sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-schema@2.6.0':
|
|
||||||
resolution: {integrity: sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-x509-attr@2.6.0':
|
|
||||||
resolution: {integrity: sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==}
|
|
||||||
|
|
||||||
'@peculiar/asn1-x509@2.6.0':
|
|
||||||
resolution: {integrity: sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==}
|
|
||||||
|
|
||||||
'@peculiar/x509@1.14.2':
|
|
||||||
resolution: {integrity: sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==}
|
|
||||||
engines: {node: '>=22.0.0'}
|
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -731,10 +693,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
|
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
'@simplewebauthn/server@13.2.2':
|
|
||||||
resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@sinclair/typebox@0.27.8':
|
'@sinclair/typebox@0.27.8':
|
||||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||||
|
|
||||||
@@ -787,11 +745,6 @@ packages:
|
|||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
|
|
||||||
'@types/cookie-parser@1.4.10':
|
|
||||||
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/express': '*'
|
|
||||||
|
|
||||||
'@types/cookiejar@2.1.5':
|
'@types/cookiejar@2.1.5':
|
||||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||||
|
|
||||||
@@ -831,6 +784,9 @@ packages:
|
|||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.7':
|
||||||
|
resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==}
|
||||||
|
|
||||||
'@types/methods@1.1.4':
|
'@types/methods@1.1.4':
|
||||||
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
||||||
|
|
||||||
@@ -846,6 +802,15 @@ packages:
|
|||||||
'@types/node@22.15.14':
|
'@types/node@22.15.14':
|
||||||
resolution: {integrity: sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==}
|
resolution: {integrity: sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==}
|
||||||
|
|
||||||
|
'@types/passport-jwt@4.0.1':
|
||||||
|
resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==}
|
||||||
|
|
||||||
|
'@types/passport-strategy@0.2.38':
|
||||||
|
resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==}
|
||||||
|
|
||||||
|
'@types/passport@1.0.17':
|
||||||
|
resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==}
|
||||||
|
|
||||||
'@types/qs@6.9.18':
|
'@types/qs@6.9.18':
|
||||||
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
|
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
|
||||||
|
|
||||||
@@ -1101,10 +1066,6 @@ packages:
|
|||||||
asap@2.0.6:
|
asap@2.0.6:
|
||||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||||
|
|
||||||
asn1js@3.0.7:
|
|
||||||
resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
async@3.2.6:
|
async@3.2.6:
|
||||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||||
|
|
||||||
@@ -1345,10 +1306,6 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
cookie-parser@1.4.7:
|
|
||||||
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
|
||||||
engines: {node: '>= 0.8.0'}
|
|
||||||
|
|
||||||
cookie-signature@1.0.6:
|
cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
@@ -1356,10 +1313,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
cookie@0.7.2:
|
|
||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
cookiejar@2.1.4:
|
cookiejar@2.1.4:
|
||||||
resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
|
resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
|
||||||
|
|
||||||
@@ -1486,10 +1439,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
dotenv@17.2.3:
|
|
||||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2560,6 +2509,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
passport-jwt@4.0.1:
|
||||||
|
resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==}
|
||||||
|
|
||||||
|
passport-strategy@1.0.0:
|
||||||
|
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
|
||||||
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
|
passport@0.7.0:
|
||||||
|
resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==}
|
||||||
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
path-exists@4.0.0:
|
path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2592,6 +2552,9 @@ packages:
|
|||||||
pause-stream@0.0.11:
|
pause-stream@0.0.11:
|
||||||
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
|
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
|
||||||
|
|
||||||
|
pause@0.0.1:
|
||||||
|
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
|
||||||
|
|
||||||
peek-readable@7.0.0:
|
peek-readable@7.0.0:
|
||||||
resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==}
|
resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2710,13 +2673,6 @@ packages:
|
|||||||
pure-rand@6.1.0:
|
pure-rand@6.1.0:
|
||||||
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
|
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
|
||||||
|
|
||||||
pvtsutils@1.3.6:
|
|
||||||
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
|
|
||||||
|
|
||||||
pvutils@1.1.5:
|
|
||||||
resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==}
|
|
||||||
engines: {node: '>=16.0.0'}
|
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@@ -3170,16 +3126,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
tslib@1.14.1:
|
|
||||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
tsyringe@4.10.0:
|
|
||||||
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
|
|
||||||
engines: {node: '>= 6.0.0'}
|
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -3462,15 +3411,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@alicloud/credentials@2.4.4':
|
|
||||||
dependencies:
|
|
||||||
'@alicloud/tea-typescript': 1.8.0
|
|
||||||
httpx: 2.3.3
|
|
||||||
ini: 1.3.8
|
|
||||||
kitx: 2.2.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@alicloud/darabonba-array@0.1.0':
|
'@alicloud/darabonba-array@0.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alicloud/tea-typescript': 1.8.0
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
@@ -3513,13 +3453,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@alicloud/dypnsapi20170525@2.0.0':
|
|
||||||
dependencies:
|
|
||||||
'@alicloud/openapi-core': 1.0.4
|
|
||||||
'@darabonba/typescript': 1.0.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@alicloud/dysmsapi20170525@4.1.0':
|
'@alicloud/dysmsapi20170525@4.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alicloud/openapi-core': 1.0.4
|
'@alicloud/openapi-core': 1.0.4
|
||||||
@@ -3536,7 +3469,7 @@ snapshots:
|
|||||||
|
|
||||||
'@alicloud/gateway-pop@0.0.6':
|
'@alicloud/gateway-pop@0.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alicloud/credentials': 2.4.4
|
'@alicloud/credentials': 2.4.3
|
||||||
'@alicloud/darabonba-array': 0.1.0
|
'@alicloud/darabonba-array': 0.1.0
|
||||||
'@alicloud/darabonba-encode-util': 0.0.2
|
'@alicloud/darabonba-encode-util': 0.0.2
|
||||||
'@alicloud/darabonba-map': 0.0.1
|
'@alicloud/darabonba-map': 0.0.1
|
||||||
@@ -3570,7 +3503,7 @@ snapshots:
|
|||||||
|
|
||||||
'@alicloud/openapi-core@1.0.4':
|
'@alicloud/openapi-core@1.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alicloud/credentials': 2.4.4
|
'@alicloud/credentials': 2.4.3
|
||||||
'@alicloud/gateway-pop': 0.0.6
|
'@alicloud/gateway-pop': 0.0.6
|
||||||
'@alicloud/gateway-spi': 0.0.8
|
'@alicloud/gateway-spi': 0.0.8
|
||||||
'@darabonba/typescript': 1.0.3
|
'@darabonba/typescript': 1.0.3
|
||||||
@@ -3881,8 +3814,6 @@ snapshots:
|
|||||||
|
|
||||||
'@eslint/js@8.57.1': {}
|
'@eslint/js@8.57.1': {}
|
||||||
|
|
||||||
'@hexagon/base64@1.1.28': {}
|
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
@@ -4103,8 +4034,6 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
'@levischuck/tiny-cbor@0.2.11': {}
|
|
||||||
|
|
||||||
'@ljharb/through@2.3.14':
|
'@ljharb/through@2.3.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@@ -4175,6 +4104,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
|
'@nestjs/jwt@11.0.0(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@types/jsonwebtoken': 9.0.7
|
||||||
|
jsonwebtoken: 9.0.2
|
||||||
|
|
||||||
'@nestjs/mapped-types@2.1.0(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)':
|
'@nestjs/mapped-types@2.1.0(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -4183,6 +4118,11 @@ snapshots:
|
|||||||
class-transformer: 0.5.1
|
class-transformer: 0.5.1
|
||||||
class-validator: 0.14.2
|
class-validator: 0.14.2
|
||||||
|
|
||||||
|
'@nestjs/passport@11.0.5(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
passport: 0.7.0
|
||||||
|
|
||||||
'@nestjs/platform-express@10.4.17(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.17)':
|
'@nestjs/platform-express@10.4.17(@nestjs/common@10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.17)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -4265,118 +4205,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
|
|
||||||
'@peculiar/asn1-android@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-cms@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
'@peculiar/asn1-x509-attr': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-csr@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-ecc@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-pfx@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-cms': 2.6.0
|
|
||||||
'@peculiar/asn1-pkcs8': 2.6.0
|
|
||||||
'@peculiar/asn1-rsa': 2.6.0
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-pkcs8@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-pkcs9@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-cms': 2.6.0
|
|
||||||
'@peculiar/asn1-pfx': 2.6.0
|
|
||||||
'@peculiar/asn1-pkcs8': 2.6.0
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
'@peculiar/asn1-x509-attr': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-rsa@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-schema@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
asn1js: 3.0.7
|
|
||||||
pvtsutils: 1.3.6
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-x509-attr@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/asn1-x509@2.6.0':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
asn1js: 3.0.7
|
|
||||||
pvtsutils: 1.3.6
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@peculiar/x509@1.14.2':
|
|
||||||
dependencies:
|
|
||||||
'@peculiar/asn1-cms': 2.6.0
|
|
||||||
'@peculiar/asn1-csr': 2.6.0
|
|
||||||
'@peculiar/asn1-ecc': 2.6.0
|
|
||||||
'@peculiar/asn1-pkcs9': 2.6.0
|
|
||||||
'@peculiar/asn1-rsa': 2.6.0
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
pvtsutils: 1.3.6
|
|
||||||
reflect-metadata: 0.2.2
|
|
||||||
tslib: 2.8.1
|
|
||||||
tsyringe: 4.10.0
|
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@pkgr/core@0.2.4': {}
|
'@pkgr/core@0.2.4': {}
|
||||||
|
|
||||||
'@simplewebauthn/server@13.2.2':
|
|
||||||
dependencies:
|
|
||||||
'@hexagon/base64': 1.1.28
|
|
||||||
'@levischuck/tiny-cbor': 0.2.11
|
|
||||||
'@peculiar/asn1-android': 2.6.0
|
|
||||||
'@peculiar/asn1-ecc': 2.6.0
|
|
||||||
'@peculiar/asn1-rsa': 2.6.0
|
|
||||||
'@peculiar/asn1-schema': 2.6.0
|
|
||||||
'@peculiar/asn1-x509': 2.6.0
|
|
||||||
'@peculiar/x509': 1.14.2
|
|
||||||
|
|
||||||
'@sinclair/typebox@0.27.8': {}
|
'@sinclair/typebox@0.27.8': {}
|
||||||
|
|
||||||
'@sinonjs/commons@3.0.1':
|
'@sinonjs/commons@3.0.1':
|
||||||
@@ -4439,10 +4272,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.31
|
'@types/node': 20.17.31
|
||||||
|
|
||||||
'@types/cookie-parser@1.4.10(@types/express@5.0.1)':
|
|
||||||
dependencies:
|
|
||||||
'@types/express': 5.0.1
|
|
||||||
|
|
||||||
'@types/cookiejar@2.1.5': {}
|
'@types/cookiejar@2.1.5': {}
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
@@ -4493,6 +4322,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.17.31
|
||||||
|
|
||||||
'@types/methods@1.1.4': {}
|
'@types/methods@1.1.4': {}
|
||||||
|
|
||||||
'@types/mime@1.3.5': {}
|
'@types/mime@1.3.5': {}
|
||||||
@@ -4507,6 +4340,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/passport-jwt@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/jsonwebtoken': 9.0.7
|
||||||
|
'@types/passport-strategy': 0.2.38
|
||||||
|
|
||||||
|
'@types/passport-strategy@0.2.38':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 5.0.1
|
||||||
|
'@types/passport': 1.0.17
|
||||||
|
|
||||||
|
'@types/passport@1.0.17':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 5.0.1
|
||||||
|
|
||||||
'@types/qs@6.9.18': {}
|
'@types/qs@6.9.18': {}
|
||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
@@ -4842,12 +4689,6 @@ snapshots:
|
|||||||
|
|
||||||
asap@2.0.6: {}
|
asap@2.0.6: {}
|
||||||
|
|
||||||
asn1js@3.0.7:
|
|
||||||
dependencies:
|
|
||||||
pvtsutils: 1.3.6
|
|
||||||
pvutils: 1.1.5
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
async@3.2.6: {}
|
async@3.2.6: {}
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
@@ -5121,17 +4962,10 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
cookie-parser@1.4.7:
|
|
||||||
dependencies:
|
|
||||||
cookie: 0.7.2
|
|
||||||
cookie-signature: 1.0.6
|
|
||||||
|
|
||||||
cookie-signature@1.0.6: {}
|
cookie-signature@1.0.6: {}
|
||||||
|
|
||||||
cookie@0.7.1: {}
|
cookie@0.7.1: {}
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
|
||||||
|
|
||||||
cookiejar@2.1.4: {}
|
cookiejar@2.1.4: {}
|
||||||
|
|
||||||
copy-to@2.0.1: {}
|
copy-to@2.0.1: {}
|
||||||
@@ -5236,8 +5070,6 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@16.4.7: {}
|
dotenv@16.4.7: {}
|
||||||
|
|
||||||
dotenv@17.2.3: {}
|
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -6549,6 +6381,19 @@ snapshots:
|
|||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
|
passport-jwt@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
jsonwebtoken: 9.0.2
|
||||||
|
passport-strategy: 1.0.0
|
||||||
|
|
||||||
|
passport-strategy@1.0.0: {}
|
||||||
|
|
||||||
|
passport@0.7.0:
|
||||||
|
dependencies:
|
||||||
|
passport-strategy: 1.0.0
|
||||||
|
pause: 0.0.1
|
||||||
|
utils-merge: 1.0.1
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-is-absolute@1.0.1: {}
|
path-is-absolute@1.0.1: {}
|
||||||
@@ -6572,6 +6417,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
through: 2.3.8
|
through: 2.3.8
|
||||||
|
|
||||||
|
pause@0.0.1: {}
|
||||||
|
|
||||||
peek-readable@7.0.0: {}
|
peek-readable@7.0.0: {}
|
||||||
|
|
||||||
pg-cloudflare@1.2.5:
|
pg-cloudflare@1.2.5:
|
||||||
@@ -6670,12 +6517,6 @@ snapshots:
|
|||||||
|
|
||||||
pure-rand@6.1.0: {}
|
pure-rand@6.1.0: {}
|
||||||
|
|
||||||
pvtsutils@1.3.6:
|
|
||||||
dependencies:
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
pvutils@1.1.5: {}
|
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
@@ -7142,14 +6983,8 @@ snapshots:
|
|||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
strip-bom: 3.0.0
|
strip-bom: 3.0.0
|
||||||
|
|
||||||
tslib@1.14.1: {}
|
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
tsyringe@4.10.0:
|
|
||||||
dependencies:
|
|
||||||
tslib: 1.14.1
|
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
@@ -8,19 +8,13 @@ import { AdminWebResourceController } from './controller/web/admin-web-resource.
|
|||||||
import { AdminWebBlogController } from './controller/web/admin-web-blog.controller';
|
import { AdminWebBlogController } from './controller/web/admin-web-blog.controller';
|
||||||
import { ResourceModule } from 'src/resource/resource.module';
|
import { ResourceModule } from 'src/resource/resource.module';
|
||||||
import { BlogModule } from 'src/blog/blog.module';
|
import { BlogModule } from 'src/blog/blog.module';
|
||||||
import { AuthModule } from 'src/auth/auth.module';
|
|
||||||
import { AdminResourceService } from './services/admin.resource.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
|
||||||
AdminResourceService,
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([User]),
|
TypeOrmModule.forFeature([User]),
|
||||||
UserModule,
|
UserModule,
|
||||||
ResourceModule,
|
ResourceModule,
|
||||||
BlogModule,
|
BlogModule,
|
||||||
AuthModule,
|
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
AdminController,
|
AdminController,
|
||||||
@@ -19,13 +19,13 @@ import { RemoveUserDto } from '../dto/admin-user/remove.dto';
|
|||||||
import { RolesGuard } from 'src/common/guard/roles.guard';
|
import { RolesGuard } from 'src/common/guard/roles.guard';
|
||||||
import { Roles } from 'src/common/decorators/role.decorator';
|
import { Roles } from 'src/common/decorators/role.decorator';
|
||||||
import { Role } from 'src/auth/role.enum';
|
import { Role } from 'src/auth/role.enum';
|
||||||
import { AuthGuard } from 'src/auth/guards/auth.guard';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
@Controller('admin/user')
|
@Controller('admin/user')
|
||||||
@UseGuards(AuthGuard, RolesGuard)
|
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
||||||
@Roles(Role.Admin)
|
@Roles(Role.Admin)
|
||||||
export class AdminUserController {
|
export class AdminUserController {
|
||||||
constructor(private readonly userService: UserService) { }
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async list(@Query() listDto: ListDto) {
|
async list(@Query() listDto: ListDto) {
|
||||||
@@ -41,7 +41,7 @@ export class AdminUserController {
|
|||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Body() createDto: CreateDto) {
|
async create(@Body() createDto: CreateDto) {
|
||||||
return this.userService.register({
|
return this.userService.create({
|
||||||
...createDto,
|
...createDto,
|
||||||
...(createDto.password &&
|
...(createDto.password &&
|
||||||
(() => {
|
(() => {
|
||||||
@@ -9,20 +9,20 @@ import {
|
|||||||
Put,
|
Put,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { CreateBlogDto } from 'src/admin/dto/admin-web/create-blog.dto';
|
import { CreateBlogDto } from 'src/admin/dto/admin-web/create-blog.dto';
|
||||||
import { SetBlogPasswordDto } from 'src/admin/dto/admin-web/set-blog-password.dto';
|
import { SetBlogPasswordDto } from 'src/admin/dto/admin-web/set-blog-password.dto';
|
||||||
import { UpdateBlogDto } from 'src/admin/dto/admin-web/update-blog.dto';
|
import { UpdateBlogDto } from 'src/admin/dto/admin-web/update-blog.dto';
|
||||||
import { AuthGuard } from 'src/auth/guards/auth.guard';
|
|
||||||
import { Role } from 'src/auth/role.enum';
|
import { Role } from 'src/auth/role.enum';
|
||||||
import { BlogService } from 'src/blog/blog.service';
|
import { BlogService } from 'src/blog/blog.service';
|
||||||
import { Roles } from 'src/common/decorators/role.decorator';
|
import { Roles } from 'src/common/decorators/role.decorator';
|
||||||
import { RolesGuard } from 'src/common/guard/roles.guard';
|
import { RolesGuard } from 'src/common/guard/roles.guard';
|
||||||
|
|
||||||
@Controller('/admin/web/blog')
|
@Controller('/admin/web/blog')
|
||||||
@UseGuards(AuthGuard, RolesGuard)
|
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
||||||
@Roles(Role.Admin)
|
@Roles(Role.Admin)
|
||||||
export class AdminWebBlogController {
|
export class AdminWebBlogController {
|
||||||
constructor(private readonly adminWebBlogService: BlogService) { }
|
constructor(private readonly adminWebBlogService: BlogService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async list() {
|
async list() {
|
||||||
@@ -9,19 +9,18 @@ import {
|
|||||||
Put,
|
Put,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { CreateResourceDto } from 'src/admin/dto/admin-web/create-resource.dto';
|
import { CreateResourceDto } from 'src/admin/dto/admin-web/create-resource.dto';
|
||||||
import { AdminResourceService } from 'src/admin/services/admin.resource.service';
|
|
||||||
import { AuthGuard } from 'src/auth/guards/auth.guard';
|
|
||||||
import { Role } from 'src/auth/role.enum';
|
import { Role } from 'src/auth/role.enum';
|
||||||
import { Roles } from 'src/common/decorators/role.decorator';
|
import { Roles } from 'src/common/decorators/role.decorator';
|
||||||
import { RolesGuard } from 'src/common/guard/roles.guard';
|
import { RolesGuard } from 'src/common/guard/roles.guard';
|
||||||
|
import { ResourceService } from 'src/resource/resource.service';
|
||||||
|
|
||||||
@Controller('/admin/web/resource')
|
@Controller('/admin/web/resource')
|
||||||
@UseGuards(AuthGuard, RolesGuard)
|
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
||||||
@Roles(Role.Admin)
|
@Roles(Role.Admin)
|
||||||
export class AdminWebResourceController {
|
export class AdminWebResourceController {
|
||||||
|
constructor(private readonly resourceService: ResourceService) {}
|
||||||
constructor(private readonly resourceService: AdminResourceService) { }
|
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async list() {
|
async list() {
|
||||||
@@ -43,10 +42,7 @@ export class AdminWebResourceController {
|
|||||||
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
|
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
|
||||||
@Body() data: CreateResourceDto,
|
@Body() data: CreateResourceDto,
|
||||||
) {
|
) {
|
||||||
return this.resourceService.update({
|
return this.resourceService.update(id, data);
|
||||||
...data,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user