├── .dockerignore
├── .gitattributes
├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .vscode
└── launch.json
├── Conf
├── nginx.conf
└── supervisor.conf
├── Dockerfile
├── Package.resolved
├── Package.swift
├── Public
├── .gitkeep
├── favicon.ico
├── images
│ ├── bean.png
│ └── logo.png
├── js
│ ├── cdn.tailwindcss.com_3.3.2.js
│ └── markdown-toc.js
└── styles
│ └── markdown.css
├── README.md
├── Resources
└── Views
│ ├── auth
│ ├── login.leaf
│ └── register.leaf
│ ├── backend
│ ├── categoryMgt.leaf
│ ├── index.leaf
│ ├── linkMgt.leaf
│ ├── menuMgt.leaf
│ ├── permissionMgt.leaf
│ ├── postMgt.leaf
│ ├── roleMgt.leaf
│ ├── tagMgt.leaf
│ └── userMgt.leaf
│ ├── base.leaf
│ ├── basebackend.leaf
│ ├── basefront.leaf
│ ├── basesimple.leaf
│ ├── front
│ ├── author.leaf
│ ├── categories.leaf
│ ├── detail.leaf
│ ├── index.leaf
│ └── tags.leaf
│ └── partial
│ ├── footer.leaf
│ └── pageCtrl.leaf
├── Sources
├── App
│ ├── Controllers
│ │ ├── .gitkeep
│ │ ├── Api
│ │ │ └── AuthController.swift
│ │ └── Web
│ │ │ ├── WebAuthController.swift
│ │ │ ├── WebBackendController.swift
│ │ │ └── WebFrontController.swift
│ ├── Error
│ │ └── ApiError.swift
│ ├── LeafTag
│ │ └── JsonB64Tag.swift
│ ├── Middlewares
│ │ └── ErrorMiddleware.swift
│ ├── Migrations
│ │ └── V1
│ │ │ ├── CreateCategory.swift
│ │ │ ├── CreateComment.swift
│ │ │ ├── CreateEmailCode.swift
│ │ │ ├── CreateInvite.swift
│ │ │ ├── CreateLink.swift
│ │ │ ├── CreateMenu.swift
│ │ │ ├── CreateMessage.swift
│ │ │ ├── CreateMessageInfo.swift
│ │ │ ├── CreatePermission.swift
│ │ │ ├── CreatePermissionMenu.swift
│ │ │ ├── CreatePost.swift
│ │ │ ├── CreatePostTag.swift
│ │ │ ├── CreateReply.swift
│ │ │ ├── CreateRole.swift
│ │ │ ├── CreateRolePermission.swift
│ │ │ ├── CreateSideBar.swift
│ │ │ ├── CreateTag.swift
│ │ │ ├── CreateUser.swift
│ │ │ ├── CreateUserAuth.swift
│ │ │ └── CreateUserRole.swift
│ ├── Models
│ │ ├── DTO
│ │ │ ├── In
│ │ │ │ ├── In.swift
│ │ │ │ ├── InCategory.swift
│ │ │ │ ├── InCode.swift
│ │ │ │ ├── InComment.swift
│ │ │ │ ├── InDeleteIds.swift
│ │ │ │ ├── InLink.swift
│ │ │ │ ├── InLogin.swift
│ │ │ │ ├── InMenu.swift
│ │ │ │ ├── InPermission.swift
│ │ │ │ ├── InPost.swift
│ │ │ │ ├── InRefreshToken.swift
│ │ │ │ ├── InRegister.swift
│ │ │ │ ├── InReply.swift
│ │ │ │ ├── InResetpwd.swift
│ │ │ │ ├── InRole.swift
│ │ │ │ ├── InSearchPost.swift
│ │ │ │ ├── InTag.swift
│ │ │ │ ├── InUpdateCategory.swift
│ │ │ │ ├── InUpdateLink.swift
│ │ │ │ ├── InUpdateMenu.swift
│ │ │ │ ├── InUpdatePermission.swift
│ │ │ │ ├── InUpdatePost.swift
│ │ │ │ ├── InUpdateRole.swift
│ │ │ │ ├── InUpdateTag.swift
│ │ │ │ ├── InUpdateUser.swift
│ │ │ │ └── InUpdatepwd.swift
│ │ │ └── Out
│ │ │ │ ├── Category+Out.swift
│ │ │ │ ├── Comment+Out.swift
│ │ │ │ ├── Link+Out.swift
│ │ │ │ ├── Menu+Out.swift
│ │ │ │ ├── Message+Out.swift
│ │ │ │ ├── MessageInfo+Out.swift
│ │ │ │ ├── Out.swift
│ │ │ │ ├── OutJson.swift
│ │ │ │ ├── OutOk.swift
│ │ │ │ ├── OutStatus.swift
│ │ │ │ ├── OutToken.swift
│ │ │ │ ├── Permission+Out.swift
│ │ │ │ ├── Post+Out.swift
│ │ │ │ ├── Reply+Out.swift
│ │ │ │ ├── Role+Out.swift
│ │ │ │ ├── Tag+Out.swift
│ │ │ │ └── User+Out.swift
│ │ ├── Entities
│ │ │ ├── Category.swift
│ │ │ ├── Comment.swift
│ │ │ ├── EmailCode.swift
│ │ │ ├── Invite.swift
│ │ │ ├── Link.swift
│ │ │ ├── Menu.swift
│ │ │ ├── Message.swift
│ │ │ ├── MessageInfo.swift
│ │ │ ├── Permission.swift
│ │ │ ├── PermissionMenu.swift
│ │ │ ├── Post.swift
│ │ │ ├── PostTag.swift
│ │ │ ├── Reply.swift
│ │ │ ├── Role.swift
│ │ │ ├── RolePermission.swift
│ │ │ ├── SideBar.swift
│ │ │ ├── Tag.swift
│ │ │ ├── User.swift
│ │ │ ├── UserAuth.swift
│ │ │ └── UserRole.swift
│ │ └── MODEL
│ │ │ ├── EmailContent.swift
│ │ │ ├── RefreshToken.swift
│ │ │ ├── SessionToken.swift
│ │ │ └── WebSessionAuthenticator.swift
│ ├── Repositorys
│ │ ├── CategoryRepository.swift
│ │ ├── CommentRepository.swift
│ │ ├── Impl
│ │ │ ├── CategoryRepositoryImpl.swift
│ │ │ ├── CommentRepositoryImpl.swift
│ │ │ ├── InviteRepositoryImpl.swift
│ │ │ ├── LinkRepositoryImpl.swift
│ │ │ ├── MenuRepositoryImpl.swift
│ │ │ ├── PermissionRepositoryImpl.swift
│ │ │ ├── PostRepositoryImpl.swift
│ │ │ ├── ReplyRepositoryImpl.swift
│ │ │ ├── RoleRepositoryImpl.swift
│ │ │ ├── TagRepositoryImpl.swift
│ │ │ └── UserRepositoryImpl.swift
│ │ ├── InviteRepository.swift
│ │ ├── LinkRepository.swift
│ │ ├── MenuRepository.swift
│ │ ├── PermissionRepository.swift
│ │ ├── PostRepository.swift
│ │ ├── ReplyRepository.swift
│ │ ├── Repository.swift
│ │ ├── RoleRepository.swift
│ │ ├── TagReposigory.swift
│ │ └── UserRepository.swift
│ ├── Services
│ │ ├── AuthService.swift
│ │ ├── BackendService.swift
│ │ ├── Impl
│ │ │ ├── AuthServiceImpl.swift
│ │ │ └── BackendServiceImpl.swift
│ │ └── Service.swift
│ ├── Util
│ │ └── PageUtil.swift
│ ├── configure.swift
│ ├── constants.swift
│ ├── entrypoint.swift
│ ├── migrations.swift
│ ├── repositories.swift
│ ├── routes.swift
│ └── services.swift
└── SMTP
│ ├── Models
│ ├── Attachment.swift
│ ├── Email.swift
│ ├── EmailAddress.swift
│ ├── SMTPRequestAction.swift
│ ├── SMTPResponse.swift
│ └── SMTPServerConfiguration.swift
│ ├── Request+SMTP.swift
│ ├── SMTP.swift
│ ├── SMTPRequestEncoder.swift
│ ├── SMTPResponseDecoder.swift
│ └── SendEmailHandler.swift
├── Tests
└── AppTests
│ └── AppTests.swift
├── docker-compose.yml
└── package.sh
/.dockerignore:
--------------------------------------------------------------------------------
1 | .build/
2 | .swiftpm/
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.html linguist-language=swift
2 | *.css linguist-language=swift
3 | *.leaf linguist-language=swift
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Swift Vapor CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev # 更改为你的默认分支,例如master或main
7 | - main
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-20.04
12 |
13 | steps:
14 | - name: 拉取代码
15 | uses: actions/checkout@v3
16 |
17 | - name: 安装 SwiftEnv
18 | run: git clone https://github.com/kylef/swiftenv.git ~/.swiftenv
19 |
20 | - name: Setup environment
21 | run: |
22 | export SWIFTENV_ROOT="$HOME/.swiftenv"
23 | export PATH="$SWIFTENV_ROOT/bin:$PATH"
24 | eval "$(swiftenv init -)"
25 | echo "$PATH" >> $GITHUB_PATH
26 | echo $SWIFTENV_ROOT
27 |
28 |
29 | - name: 安装Swift
30 | run: |
31 | swiftenv install 5.8.1 --skip-existing
32 | swiftenv global 5.8.1
33 |
34 | - name: 编译项目
35 | run: |
36 | swift build -c release --static-swift-stdlib
37 | mv .build/x86_64-unknown-linux-gnu/release/App ./
38 | chmod +x ./App
39 |
40 | - name: 收集产物
41 | run: |
42 | zip -r output_filename.zip App Public Resources
43 |
44 | - name: 保存产物
45 | uses: actions/upload-artifact@v2
46 | with:
47 | name: vapor-app-artifact
48 | path: output_filename.zip
49 |
50 | - name: 上传产物
51 | uses: appleboy/scp-action@v0.1.4
52 | with:
53 | host: ${{ secrets.HOST }}
54 | username: ${{ secrets.USERNAME }}
55 | password: ${{ secrets.PASSWORD }}
56 | port: ${{ secrets.PORT }}
57 | source: output_filename.zip
58 | target: ${{secrets.TARGET_FOLD}}
59 |
60 | - name: 部署服务
61 | uses: appleboy/ssh-action@v1.0.0
62 | with:
63 | host: ${{ secrets.HOST }}
64 | username: ${{ secrets.USERNAME }}
65 | password: ${{ secrets.PASSWORD }}
66 | port: ${{ secrets.PORT }}
67 | script: |
68 | cd ${{secrets.TARGET_FOLD}}
69 | ls -all
70 | unzip -o output_filename.zip
71 | chmod +x App
72 | supervisorctl stop iblog-oldbird-run
73 | supervisorctl start iblog-oldbird-run
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Packages
2 | .build
3 | xcuserdata
4 | *.xcodeproj
5 | DerivedData/
6 | .DS_Store
7 | db.sqlite
8 | .swiftpm
9 | .env
10 | PackageApp.zip
11 | PackageApp
12 | .env.*
13 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "type": "lldb",
5 | "request": "launch",
6 | "sourceLanguages": ["swift"],
7 | "name": "Debug App",
8 | "program": "${workspaceFolder:hello}/.build/debug/App",
9 | "args": [],
10 | "cwd": "${workspaceFolder:hello}",
11 | "preLaunchTask": "swift: Build Debug App"
12 | },
13 | {
14 | "type": "lldb",
15 | "request": "launch",
16 | "sourceLanguages": ["swift"],
17 | "name": "Release App",
18 | "program": "${workspaceFolder:hello}/.build/release/App",
19 | "args": [],
20 | "cwd": "${workspaceFolder:hello}",
21 | "preLaunchTask": "swift: Build Release App"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/Conf/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | server_name ${server_domain};
3 | listen 443 ssl http2;
4 | listen 80;
5 |
6 | root ${project_root}/Public/;
7 |
8 | #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
9 | #error_page 404/404.html;
10 | #SSL-END
11 |
12 | #ERROR-PAGE-START 错误页配置,可以注释、删除或修改
13 | #error_page 404 /404.html;
14 | #error_page 502 /502.html;
15 | #ERROR-PAGE-END
16 |
17 | location @proxy {
18 | proxy_pass http://127.0.0.1:${server_port};
19 | proxy_pass_header Server;
20 | proxy_set_header Host $host;
21 | proxy_set_header X-Real-IP $remote_addr;
22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
23 | proxy_pass_header Server;
24 | proxy_connect_timeout 3s;
25 | proxy_read_timeout 10s;
26 | }
27 |
28 | location / {
29 | # First attempt to serve request as file, then
30 | # as directory, then fall back to displaying a 404.
31 | # try_files $uri $uri/ =404;
32 | try_files $uri @proxy; #这里是页面重定向、让80端口可以重定向到我们的服务器。
33 | #try_files $uri $uri/ /index.php?$query_string; #添加url重定向,>这在laravel文档中有写
34 | }
35 |
36 | #一键申请SSL证书验证目录相关设置
37 | location ~ \.well-known{
38 | allow all;
39 | }
40 | access_log ${nginx_log_root_path}/${server_domain}.log;
41 | error_log ${nginx_log_root_path}/${server_domain}.error.log;
42 | }
--------------------------------------------------------------------------------
/Conf/supervisor.conf:
--------------------------------------------------------------------------------
1 | [program:${server_domain}]
2 | command=${project_root}/Run serve --env production --auto-migrate --port ${server_port}
3 | directory=${project_root}
4 | autostart=true
5 | autorestart=true
6 | user=root
7 | stdout_logfile=/var/log/supervisor/%(program_name)-stdout.log
8 | stderr_logfile=/var/log/supervisor/%(program_name)-stderr.log
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # # ================================
2 | # # Build image
3 | # # ================================
4 | # FROM swift:5.8-jammy as build
5 |
6 | # # Install OS updates and, if needed, sqlite3
7 | # RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
8 | # && apt-get -q update \
9 | # && apt-get -q dist-upgrade -y\
10 | # && rm -rf /var/lib/apt/lists/*
11 |
12 | # # Set up a build area
13 | # WORKDIR /build
14 |
15 | # # First just resolve dependencies.
16 | # # This creates a cached layer that can be reused
17 | # # as long as your Package.swift/Package.resolved
18 | # # files do not change.
19 | # COPY ./Package.* ./
20 | # RUN swift package resolve
21 |
22 | # # Copy entire repo into container
23 | # COPY . .
24 |
25 | # # Build everything, with optimizations
26 | # RUN swift build -c release --static-swift-stdlib
27 |
28 | # # Switch to the staging area
29 | # WORKDIR /staging
30 |
31 | # # Copy main executable to staging area
32 | # RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./
33 |
34 | # # Copy resources bundled by SPM to staging area
35 | # RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
36 |
37 | # # Copy any resources from the public directory and views directory if the directories exist
38 | # # Ensure that by default, neither the directory nor any of its contents are writable.
39 | # RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
40 | # RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true
41 |
42 | # # ================================
43 | # # Run image
44 | # # ================================
45 | # FROM ubuntu:jammy
46 |
47 | # # Make sure all system packages are up to date, and install only essential packages.
48 | # RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
49 | # && apt-get -q update \
50 | # && apt-get -q dist-upgrade -y \
51 | # && apt-get -q install -y \
52 | # ca-certificates \
53 | # tzdata \
54 | # # If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
55 | # # libcurl4 \
56 | # # If your app or its dependencies import FoundationXML, also install `libxml2`.
57 | # # libxml2 \
58 | # && rm -r /var/lib/apt/lists/*
59 |
60 | # # Create a vapor user and group with /app as its home directory
61 | # RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor
62 |
63 | # # Switch to the new home directory
64 | # WORKDIR /app
65 |
66 | # # Copy built executable and any staged resources from builder
67 | # COPY --from=build --chown=vapor:vapor /staging /app
68 |
69 | # # Ensure all further commands run as the vapor user
70 | # USER vapor:vapor
71 |
72 | # # Let Docker bind to port 8080
73 | # EXPOSE 8080
74 |
75 | # # Start the Vapor service when the image is run, default to listening on 8080 in production environment
76 | # ENTRYPOINT ["./App"]
77 | # CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
78 |
79 | # ================================
80 | # Build image
81 | # ================================
82 | FROM swift:5.8-jammy as build
83 |
84 | # Install OS updates and, if needed, sqlite3
85 | RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
86 | && apt-get -q update \
87 | && apt-get -q dist-upgrade -y\
88 | && rm -rf /var/lib/apt/lists/*
89 |
90 | # Set up a build area
91 | WORKDIR /build
92 |
93 | # First just resolve dependencies.
94 | # This creates a cached layer that can be reused
95 | # as long as your Package.swift/Package.resolved
96 | # files do not change.
97 | COPY ./Package.* ./
98 | RUN swift package resolve --verbose
99 |
100 | # Copy entire repo into container
101 | COPY . .
102 |
103 | # Build everything, with optimizations
104 | RUN swift build -c release --verbose
105 |
106 | # Switch to the staging area
107 | WORKDIR /staging
108 |
109 | # Copy main executable to staging area
110 | RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./
111 |
112 | # Copy resources bundled by SPM to staging area
113 | RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
114 |
115 | # Copy any resources from the public directory and views directory if the directories exist
116 | # Ensure that by default, neither the directory nor any of its contents are writable.
117 | RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
118 | RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true
119 |
120 | RUN tar -czf /staging.tar.gz -C /staging .
121 |
122 | # 删除原始文件
123 | RUN rm -rf /staging
124 |
125 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.8
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "hello",
6 | platforms: [
7 | .macOS(.v12)
8 | ],
9 | dependencies: [
10 | // 💧 A server-side Swift web framework.
11 | .package(url: "https://github.com/vapor/vapor.git", from: "4.76.0"),
12 | .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
13 | .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
14 | .package(url: "https://github.com/vapor/jwt.git", from: "4.2.2"),
15 | .package(url: "https://github.com/vapor/leaf.git", from: "4.2.4"),
16 | .package(url: "https://github.com/Flight-School/AnyCodable",from: "0.6.0")
17 | ],
18 | targets: [
19 | .target(
20 | name: "SMTP",
21 | dependencies: [
22 | .product(name: "Vapor", package: "vapor")
23 | ]
24 | ),
25 | .executableTarget(
26 | name: "App",
27 | dependencies: [
28 | .product(name: "Vapor", package: "vapor"),
29 | .product(name: "Fluent", package: "fluent"),
30 | .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
31 | .product(name: "JWT", package: "jwt"),
32 | .product(name: "Leaf", package: "leaf"),
33 | .product(name: "AnyCodable", package: "AnyCodable"),
34 | .target(name: "SMTP"),
35 | ],
36 | swiftSettings: [
37 | .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
38 | ]
39 | ),
40 | .testTarget(name: "AppTests", dependencies: [
41 | .target(name: "App"),
42 | .product(name: "XCTVapor", package: "vapor"),
43 | ])
44 | ]
45 | )
46 |
--------------------------------------------------------------------------------
/Public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftdo/vapor-blog/1f61ad8e720cf65f1b118bccd2587cecb1499117/Public/.gitkeep
--------------------------------------------------------------------------------
/Public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftdo/vapor-blog/1f61ad8e720cf65f1b118bccd2587cecb1499117/Public/favicon.ico
--------------------------------------------------------------------------------
/Public/images/bean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftdo/vapor-blog/1f61ad8e720cf65f1b118bccd2587cecb1499117/Public/images/bean.png
--------------------------------------------------------------------------------
/Public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftdo/vapor-blog/1f61ad8e720cf65f1b118bccd2587cecb1499117/Public/images/logo.png
--------------------------------------------------------------------------------
/Public/js/markdown-toc.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | let baseColor = "white",
3 | showColor = "#09f",
4 | hoverColor = "#f60"
5 |
6 | function tocTokens2HTML(tokens) {
7 | let html = "
",
8 | index = 0,
9 | level = 1,
10 | levelStack = ['
']
11 |
12 | let tokensLength = tokens.length
13 | if (!tokensLength) return ""
14 | while (index < tokensLength) {
15 | if (tokens[index].level == level) {
16 | if (levelStack[levelStack.length - 1] == '') {
17 | html += levelStack.pop() // html += ''
18 | }
19 | html += `${tokens[index].text}`
20 |
21 | levelStack.push(``)
22 | index++
23 | } else if (tokens[index].level > level) {
24 | if (levelStack[levelStack.length - 1] == '') {
25 | html += ``)
27 | levelStack.push(``)
28 | } else {
29 | html += `')
31 | }
32 | level++
33 | } else {
34 | for (let i = (level - tokens[index].level) * 2 + 1; i > 0; i--) {
35 | html += levelStack.pop()
36 | }
37 | level = tokens[index].level
38 | }
39 | }
40 | for (let i = levelStack.length; i > 0; i--) {
41 | html += levelStack.pop()
42 | }
43 | return html
44 | }
45 |
46 | let chain = function () {
47 | return {
48 | anchorDOM: null,
49 | anchors: [],
50 | tocDOM: null,
51 | tocs: [],
52 | length: 0,
53 | index: 0,
54 | init: init,
55 | scroll: scroll
56 | }
57 | }
58 |
59 | function init(tocDOM_Name = 'markdown-toc', anchorDOM_Name = 'markdown-body', anchors_Name = 'markdown-body-anchor') {
60 | this.anchorDOM = document.getElementById(anchorDOM_Name)
61 | this.anchors = this.anchorDOM.getElementsByClassName(anchors_Name)
62 | this.tocDOM = document.getElementById(tocDOM_Name)
63 | this.tocs = this.tocDOM.querySelectorAll('a[href^="#"]')
64 | this.length = this.tocs.length
65 |
66 | if (!this.length) return
67 | this.tocs[this.index].style.color = showColor
68 | this.tocs.forEach(anchor => {
69 | function scrollToBodyIndex(id){
70 | let step = 80, distance = 45, speed = 10 // step < distance * 2
71 | let goalDom = document.getElementById(id.slice(1)),
72 | goal = goalDom.offsetTop - distance,
73 | now = document.documentElement.scrollTop + document.body.scrollTop
74 |
75 | step = goal > now ? step : -step
76 | let s = setInterval(function () {
77 | if (Math.abs(goal - now) < distance) {
78 | clearInterval(s)
79 | window.scrollTo(0, goal) // 修正
80 | window.location.hash = id // 为了修改 URL 也是不容易
81 | } else {
82 | now += step
83 | window.scrollBy(0, step)
84 | }
85 | }, speed)
86 | }
87 | anchor.addEventListener('click', function (e) {
88 | e.preventDefault();
89 | scrollToBodyIndex(this.getAttribute('href'))
90 | // 如果不需要在平滑滚动后修改 URL,下面这一句就够了
91 | // document.querySelector(this.getAttribute('href')).scrollIntoView({
92 | // behavior: 'smooth'
93 | // })
94 | })
95 | })
96 | }
97 |
98 | function scroll() {
99 | if (!this.length) return
100 | let anchors = this.anchors,
101 | tocs = this.tocs,
102 | length = this.length,
103 | index = this.index
104 |
105 | function top(e) {
106 | return e.getBoundingClientRect().top
107 | }
108 |
109 | function toggleFontColor(dom, last, now) {
110 | dom[last].style.color = baseColor
111 | dom[now].style.color = showColor
112 | }
113 |
114 | let scrollToTocIndex = () => {
115 | let goal = window.innerHeight / 3,
116 | now = top(tocs[index])
117 | if (Math.abs(goal - now) > 10) {
118 | this.tocDOM.scrollTop += now - goal;
119 | }
120 | }
121 |
122 | let distance = 45
123 | if (top(anchors[index]) < distance) {
124 | if (index > length - 2) return
125 | if (top(anchors[index + 1]) > distance) return
126 | toggleFontColor(tocs, this.index++, this.index)
127 | scrollToTocIndex()
128 | } else {
129 | if (index < 1) return
130 | // 一旦 now > distance 就已经进入上一个标题内容了
131 | toggleFontColor(tocs, this.index--, this.index)
132 | scrollToTocIndex()
133 | }
134 | }
135 |
136 | return markdownToc = {
137 | toHTML: tocTokens2HTML,
138 | chain: chain()
139 | }
140 | })()
--------------------------------------------------------------------------------
/Public/styles/markdown.css:
--------------------------------------------------------------------------------
1 | /*
2 | * toc
3 | */
4 | .markdown-toc {
5 | border-left: 1px solid #aaa;
6 | text-align: left;
7 | font-size: .875rem;
8 | padding: 10px;
9 | }
10 | .markdown-toc a {
11 | color: white;
12 | display: block;
13 | margin: 0 2px;
14 | text-decoration: none;
15 | }
16 |
17 | .markdown-toc ul li>:first-child {
18 | font-weight: bolder;
19 | margin-bottom: 8px;
20 | }
21 | .markdown-toc ul li ul li>:first-child {
22 | font-weight: normal;
23 | }
24 |
25 | /*
26 | * 图片
27 | */
28 | .markdown-body img {
29 | max-width: 100%;
30 | }
31 |
32 | /**
33 | * 标题
34 | */
35 | .markdown-body h1, h2, h3, h4, h5 {
36 | color: white;
37 | font-weight: 700;
38 | padding: 10px 0;
39 | margin: 1em 0;
40 | font-family: Georgia, Palatino, serif;
41 | }
42 | .markdown-body>:first-child {
43 | margin-top: 0px;
44 | }
45 | .markdown-body h1 {
46 | font-size: 2.0rem;
47 | }
48 | .markdown-body h2 {
49 | font-size: 1.6rem;
50 | }
51 | .markdown-body h1, h2 {
52 | border-bottom: 1px solid #EFEAEA;
53 | }
54 | .markdown-body h3 {
55 | font-size: 1.3rem;
56 | }
57 | .markdown-body h4 {
58 | font-size: 1.1rem;
59 | }
60 | .markdown-body h5 {
61 | font-size: 1rem;
62 | }
63 |
64 |
65 | /*
66 | * 字体
67 | */
68 | .markdown-body p {
69 | margin: .25rem 0 .5rem;
70 | font-size: 1rem;
71 | height: 1.6;
72 | }
73 | .markdown-body a {
74 | color: #09f;
75 | margin: 0 2px;
76 | padding: 0;
77 | vertical-align: baseline;
78 | text-decoration: none;
79 | }
80 | .markdown-body a:hover {
81 | color: #f60;
82 | }
83 |
84 | /*
85 | * 列表
86 | */
87 | .markdown-body ul, ol {
88 | padding: 0;
89 | margin: .5rem 0;
90 | }
91 | .markdown-body li {
92 | line-height: 1.5rem;
93 | }
94 | .markdown-body ul, ol {
95 | font-size: 1rem;
96 | line-height: 1.5rem;
97 | }
98 | .markdown-body ol ol, ul ol {
99 | list-style-type: lower-roman;
100 | }
101 |
102 | /*
103 | * 代码块
104 | */
105 | .markdown-body pre {
106 | font-size: 1rem;
107 | margin: 10px 0;
108 | background-color: #282c34;
109 | border-radius: 10px;
110 | padding: 10px;
111 | }
112 |
113 | /*
114 | * 引用块
115 | */
116 | .markdown-body blockquote {
117 | border-left: .25rem solid #dfe2e5;
118 | color: #6a737d;
119 | padding: 0 1rem;
120 | }
121 | .markdown-body blockquote>:first-child {
122 | margin-top: .5rem;
123 | }
124 |
125 | .markdown-body blockquote>:last-child {
126 | margin-bottom: .5rem;
127 | }
128 | .markdown-body blockquote cite {
129 | font-size: .875rem;
130 | line-height: 1.25rem;
131 | color: #bfbfbf;
132 | }
133 | .markdown-body blockquote cite:before {
134 | content: '\2014 \00A0';
135 | }
136 | .markdown-body blockquote p {
137 | color: #666;
138 | }
139 |
140 | /*
141 | * 分隔线
142 | */
143 | .markdown-body hr {
144 | text-align: left;
145 | color: #999;
146 | height: 2px;
147 | padding: 0;
148 | margin: 16px 0;
149 | background-color: #e7e7e7;
150 | border: 0 none;
151 | }
152 |
153 | /*
154 | * 表格
155 | */
156 | .markdown-body dl {
157 | padding: 0;
158 | margin-bottom: 1rem;
159 | }
160 | .markdown-body dl dt {
161 | padding: 10px 0;
162 | margin-top: 1rem;
163 | font-size: 1rem;
164 | font-style: italic;
165 | font-weight: bold;
166 | }
167 | .markdown-body dl dd {
168 | padding: 0 1rem;
169 | margin-bottom: 1rem;
170 | }
171 | .markdown-body dd {
172 | margin-left: 0;
173 | }
174 | .markdown-body table {
175 | *border-collapse: collapse; /* IE7 and lower */
176 | border-spacing: 0;
177 | width: 100%;
178 | border: solid #ccc 1px;
179 | margin: 10px 0;
180 | }
181 | .markdown-body table thead {
182 | background: #f7f7f7;
183 | }
184 | .markdown-body table thead tr:hover {
185 | background: #f7f7f7
186 | }
187 | .markdown-body table tr:nth-child(2n) {
188 | background: #f6f8fa;
189 | }
190 | .markdown-body table tr:hover {
191 | background: #fbf8e9;
192 | -o-transition: all .1s ease-in-out;
193 | -webkit-transition: all .1s ease-in-out;
194 | -moz-transition: all .1s ease-in-out;
195 | -ms-transition: all .1s ease-in-out;
196 | transition: all .1s ease-in-out;
197 | }
198 | .markdown-body table td {
199 | border-left: 1px solid #ccc;
200 | border-top: 1px solid #ccc;
201 | padding: .5rem .875rem;
202 | text-align: center;
203 | }
204 | .markdown-body table th {
205 | border-top: none;
206 | text-shadow: 0 1px 0 rgba(255,255,255,.5);
207 | padding: .5rem .875rem;
208 | border-left: 1px solid #ccc;
209 | text-align: center;
210 | }
211 | .markdown-body table td:first-child, table th:first-child {
212 | border-left: none;
213 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Swift Vapor blog system!
6 |
7 | * 在线预览:[https://iblog.oldbird.run](https://iblog.oldbird.run)
8 | * 管理员账号:admin@iblog.com admin123456
9 |
10 | ## iBlog———Vapor 博客系统实战项目
11 |
12 | 开发状态:开发中...
13 |
14 | ## 使用步骤
15 |
16 | * 安装 postgresql
17 | * 拉取代码,运行
18 | * 调用 https://localhost:xxx/web/backend/config 完成工程的一些初始化调用。
19 | * 浏览器打开页面:https://localhost:xxxx
20 |
21 | ## 相关文档
22 | * 页面预览:[页面预览](https://github.com/swiftdo/vapor-blog/wiki)
23 | * 开发文档:[点击前往](https://github.com/swiftdo/vapor-blog/wiki)
24 | * 源码:[https://github.com/swiftdo/vapor-blog](https://github.com/swiftdo/vapor-blog)
25 |
26 | ## 页面
27 |
28 | 网站:
29 |
30 | 
31 |
32 | 后台:
33 |
34 | 
35 |
36 |
37 | ## 首页侧边栏规划
38 | * 热门标签,按文章数排序, 20个
39 | * 最新发布,按文章时间排序, 10个
40 |
41 | ## TODO
42 |
43 | * [ ] 个人主页
44 | * [ ] 消息中心
45 | * [ ] 文章收藏、点赞
46 | * [ ] 作者关注
47 | * [ ] 文章统计
48 | * [ ] 后台主页
49 | * 博客数量
50 | * 今日发表
51 | * 今日访客
52 | * 历史访客
53 | * 网站访问人数
54 | * [ ] 操作日志
55 | * [ ] 留言功能
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Resources/Views/auth/login.leaf:
--------------------------------------------------------------------------------
1 | #extend("basesimple"):
2 | #export("content"):
3 |
23 |
24 | #endexport
25 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/auth/register.leaf:
--------------------------------------------------------------------------------
1 | #extend("basesimple"):
2 | #export("content"):
3 |
44 |
45 |
54 |
55 |
85 | #endexport
86 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/backend/categoryMgt.leaf:
--------------------------------------------------------------------------------
1 | #extend("basebackend"):
2 | #export("contentRight"):
3 |
15 |
16 |
17 |
18 |
19 |
20 |
63 | #extend("partial/pageCtrl")
64 |
65 |
66 |
67 |
68 |
69 |
70 |
修改分类
71 |
72 |
73 | 开启导航:
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
144 |
145 |
146 |
160 | #endexport
161 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/backend/index.leaf:
--------------------------------------------------------------------------------
1 | #extend("basebackend"):
2 | #export("contentRight"):
3 |
4 |
后台首页
5 |

6 |
7 | #if(user):
8 |
#(user.name)-#(user.email)-控制台
9 | #else:
10 |
未登录
11 | #endif
12 |
13 | #endexport
14 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/backend/tagMgt.leaf:
--------------------------------------------------------------------------------
1 | #extend("basebackend"):
2 | #export("contentRight"):
3 |
15 |
16 |
17 |
18 |
19 |
20 |
61 | #extend("partial/pageCtrl")
62 |
63 |
64 |
65 |
66 |
67 |
68 |
修改标签
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
133 |
134 |
135 |
145 | #endexport
146 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/backend/userMgt.leaf:
--------------------------------------------------------------------------------
1 | #extend("basebackend"):
2 |
3 | #export("head"):
4 |
5 |
6 |
7 | #endexport
8 |
9 | #export("contentRight"):
10 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 用户名 |
34 | 邮箱 |
35 | 角色 |
36 | 操作 |
37 |
38 |
39 |
40 | #for(item in data.items):
41 |
42 |
43 |
44 | #(item.name)
45 |
46 | |
47 |
48 | #(item.email)
49 | |
50 |
51 | #for(role in item.roles):
52 | #(role.name)
53 | #endfor
54 | |
55 |
56 |
57 | |
58 |
59 | #endfor
60 |
61 |
62 | #extend("partial/pageCtrl")
63 |
64 |
65 |
66 |
67 |
68 |
69 |
修改用户角色
70 |
71 |
72 |
77 |
78 |
79 |
80 |
81 |
82 |
145 |
146 | #endexport
147 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/base.leaf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | #(title)
9 |
10 |
11 |
12 |
13 |
14 |
20 |
21 | #import("head")
22 |
23 |
24 |
25 |
26 | #import("navbar")
27 | #import("content")
28 | #import("footer")
29 |
30 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/Resources/Views/basebackend.leaf:
--------------------------------------------------------------------------------
1 | #extend("base"):
2 | #export("navbar"):
3 |
4 |
28 |
29 |
42 | #endexport
43 |
44 | #export("content"):
45 |
46 |
47 |
48 |
59 |
60 |
#import("contentRight")
61 |
62 | #endexport
63 |
64 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/basefront.leaf:
--------------------------------------------------------------------------------
1 | #extend("base"):
2 | #export("head"):
3 |
10 | #endexport
11 |
12 | #export("navbar"):
13 |
14 |
15 |
16 |
17 |

18 |
19 |
博客
20 |
21 |
22 | #if(!hideNavCate):
23 |
24 |
34 |
35 | #endif
36 |
37 |
41 |
42 | #if(user):
43 |
44 |
49 |
50 | -
51 |
52 | 控制台
53 |
54 |
55 | - 退出
56 |
57 |
58 | #else:
59 |
登录
60 |
注册
61 | #endif
62 |
63 |
64 |
65 |
78 | #endexport
79 |
80 | #export("footer"):
81 | #extend("partial/footer")
82 | #endexport
83 |
84 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/basesimple.leaf:
--------------------------------------------------------------------------------
1 | #extend("base"):
2 | #export("navbar"):
3 |
4 |
5 |
6 |
7 |

8 |
9 |
博客
10 |
11 |
12 | #endexport
13 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/front/author.leaf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftdo/vapor-blog/1f61ad8e720cf65f1b118bccd2587cecb1499117/Resources/Views/front/author.leaf
--------------------------------------------------------------------------------
/Resources/Views/front/categories.leaf:
--------------------------------------------------------------------------------
1 | #extend("basefront"):
2 | #export("content"):
3 |
4 |
15 | #endexport
16 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/front/index.leaf:
--------------------------------------------------------------------------------
1 | #extend("basefront"):
2 | #export("content"):
3 |
4 |
5 |
6 |
7 | #for(article in data.items):
8 |
9 |
14 |
#(article.title)
15 |
#(article.desc)
16 |
17 |
18 | #for(tag in article.tags):
19 |
##(tag.name)
20 | #endfor
21 |
22 |
25 |
26 |
27 | #endfor
28 |
29 |
30 | #extend("partial/pageCtrl")
31 |
32 |
33 |
34 |
35 |
热门标签
36 |
37 | #for(tag in hotTags):
38 |
#(tag.name)
39 | #endfor
40 |
41 |
42 |
43 |
最新文章
44 |
45 | #for(post in newerPosts):
46 |
#(post.title)
47 | #endfor
48 |
49 |
50 |
51 |
52 |
53 | #endexport
54 | #endextend
55 |
--------------------------------------------------------------------------------
/Resources/Views/front/tags.leaf:
--------------------------------------------------------------------------------
1 | #extend("basefront"):
2 | #export("content"):
3 |
4 |
5 |
6 |
所有标签
7 |
8 | #for(tag in data):
9 |
#(tag.name)
10 | #endfor
11 |
12 |
13 |
14 | #endexport
15 | #endextend
--------------------------------------------------------------------------------
/Resources/Views/partial/footer.leaf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
VaporDemo
5 |
6 |
Vapor provides a safe, performant and easy to use foundation to build HTTP servers, backends and APIs in Swift
7 |
8 |
9 |
25 |
26 |
27 |
© 2023 swiftdo
28 |
Designed By swiftdo Powered By Vapor
29 |
34 |
35 |
--------------------------------------------------------------------------------
/Resources/Views/partial/pageCtrl.leaf:
--------------------------------------------------------------------------------
1 |
13 |
14 | #if(pageMeta.curPage == pageMeta.minPage):
15 |
16 | #else:
17 |
18 | #endif
19 | #if(pageMeta.showMinMore):
20 |
21 |
22 | #else:
23 | #endif
24 | #for(pageNum in pageMeta.showPages):
25 | #if(pageNum == pageMeta.curPage):
26 |
27 | #else:
28 |
29 | #endif
30 | #endfor
31 | #if(pageMeta.showMaxMore):
32 |
33 |
34 | #else:
35 | #endif
36 | #if(pageMeta.curPage == pageMeta.maxPage):
37 |
38 | #else:
39 |
40 | #endif
41 |
42 |
到第
43 |
44 |
页
45 |
46 |
共#(pageMeta.total)条
47 |
48 |
53 |
54 |
55 |
100 |
--------------------------------------------------------------------------------
/Sources/App/Controllers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftdo/vapor-blog/1f61ad8e720cf65f1b118bccd2587cecb1499117/Sources/App/Controllers/.gitkeep
--------------------------------------------------------------------------------
/Sources/App/Controllers/Web/WebAuthController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/14.
6 | //
7 |
8 | import Fluent
9 | import SMTP
10 | import Vapor
11 |
12 | struct WebAuthController: RouteCollection {
13 | func boot(routes: RoutesBuilder) throws {
14 | routes.get("register", use: toRegister)
15 | routes.get("login", use: toLogin)
16 |
17 | // 接口
18 | routes.post("register", use: register)
19 | routes.post("register", "code", use: getRegisterCode)
20 |
21 | let credentialsRoute = routes.grouped(WebCredentialsAuthenticator())
22 | credentialsRoute.post("login", use: login)
23 |
24 | // 退出
25 | let tokenGroup = routes.grouped(WebSessionAuthenticator(), User.guardMiddleware())
26 | tokenGroup.get("logout", use: logout)
27 | }
28 | }
29 |
30 | extension WebAuthController {
31 | /// 登录页面
32 | private func toLogin(_ req: Request) async throws -> View {
33 | return try await req.view.render("auth/login", ["title": "博客"])
34 | }
35 |
36 | /// 注册页面
37 | private func toRegister(_ req: Request) async throws -> View {
38 | return try await req.view.render("auth/register")
39 | }
40 |
41 | private func register(_ req: Request) async throws -> Response {
42 | let _ = try await req.services.auth.register()
43 | return req.redirect(to: "/web/auth/login")
44 | }
45 |
46 | private func getRegisterCode(_ req: Request) async throws -> OutJson {
47 | return try await req.services.auth.getRegisterCode()
48 | }
49 |
50 | private func login(_ req: Request) async throws -> Response {
51 | guard let user = req.auth.get(User.self) else {
52 | throw Abort(.unauthorized)
53 | }
54 | // 添加这一步才会设置session
55 | req.session.authenticate(user)
56 | // 获取token,传给前端
57 | return req.redirect(to: "/")
58 | }
59 |
60 | private func logout(_ req: Request) async throws -> Response {
61 | // let user = try req.auth.require(User.self)
62 | req.auth.logout(User.self)
63 | req.session.unauthenticate(User.self)
64 | return req.redirect(to: "/")
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/App/Controllers/Web/WebFrontController.swift:
--------------------------------------------------------------------------------
1 |
2 | import Fluent
3 | import Vapor
4 | import AnyCodable
5 |
6 | struct WebFrontController: RouteCollection {
7 | func boot(routes: RoutesBuilder) throws {
8 |
9 | let tokenGroup = routes.grouped(WebSessionAuthenticator())
10 | tokenGroup.get(use: toIndex)
11 | tokenGroup.get("index", use: toIndex)
12 | tokenGroup.get("detail", use: toDetail)
13 | tokenGroup.get("tags", use: toTags)
14 | tokenGroup.get("categories", use: toCategories)
15 | tokenGroup.get("list", use: toIndex)
16 |
17 | let authTokeGroup = tokenGroup.grouped(User.guardMiddleware())
18 |
19 | // 评论文章
20 | authTokeGroup.post("comment", use: addComment)
21 | // 对评论进行回复
22 | authTokeGroup.post("comment", "reply", use: commentAddReply)
23 | }
24 | }
25 |
26 | extension WebFrontController {
27 | private func frontWrapper(_ req: Request, cateId: UUID? = nil, hideNavCate: Bool = false, data: AnyEncodable? = nil, pageMeta:PageMetadata? = nil, extra: [String: AnyEncodable?]? = nil) async throws -> [String: AnyEncodable?] {
28 | let user = req.auth.get(User.self)
29 | let outUser = user?.asPublic()
30 | let cates = try await req.repositories.category.all(ownerId: outUser?.id)
31 | let links = try await req.repositories.link.all(ownerId: outUser?.id)
32 |
33 | var context: [String: AnyEncodable?] = [
34 | "cateId": .init(cateId),
35 | "user": .init(outUser),
36 | "data": data,
37 | "pageMeta": PageUtil.genPageMetadata(pageMeta: pageMeta),
38 | "categories": .init(cates),
39 | "hideNavCate": .init(hideNavCate),
40 | "links": .init(links)
41 | ]
42 | if let extra = extra {
43 | context.merge(extra) { $1 }
44 | }
45 | return context
46 | }
47 |
48 | private func toDetail(_ req: Request) async throws -> View {
49 | let user = req.auth.get(User.self)
50 | let postId: String = req.query["postId"]!
51 | let post = try await req.repositories.post.get(id: .init(uuidString: postId)!, ownerId: user?.requireID())
52 | // 获取文章的评论
53 | let comments = try await req.repositories.comment.all(topicId: .init(uuidString: postId)!, topicType: 1)
54 | return try await req.view.render("front/detail", frontWrapper(req,
55 | hideNavCate: true,
56 | data: .init(post),
57 | extra: ["comments": .init(comments)]
58 | ))
59 | }
60 |
61 | private func toIndex(_ req: Request) async throws -> View {
62 | let user = req.auth.get(User.self)
63 | let inIndex = try req.query.decode(InSearchPost.self)
64 | let posts = try await req.repositories.post.page(ownerId: user?.id, inIndex: inIndex)
65 | // * 热门标签,按文章数排序, 20个
66 | let hotTags = try await req.repositories.tag.hot(limit: 20)
67 | // * 最新发布,按文章时间排序, 10个
68 | let newerPosts = try await req.repositories.post.newer(limit: 10)
69 | // * 点击最多,按文章阅读数排序,10个
70 |
71 | return try await req.view.render("front/index",
72 | frontWrapper(req,
73 | cateId: inIndex.categoryId,
74 | data: .init(posts),
75 | pageMeta: posts.metadata,
76 | extra: [
77 | "listFor": .init(inIndex.listFor),
78 | "hotTags": .init(hotTags),
79 | "newerPosts": .init(newerPosts)
80 | ])
81 | )
82 | }
83 |
84 | // 所有标签
85 | private func toTags(_ req: Request) async throws -> View {
86 | let user = req.auth.get(User.self)
87 | let tags = try await req.repositories.tag.all(ownerId: user?.id)
88 | return try await req.view.render("front/tags",frontWrapper(req, data: .init(tags)))
89 | }
90 |
91 | // 所有分类
92 | private func toCategories(_ req: Request) async throws -> View {
93 | let user = req.auth.get(User.self)
94 | let categories = try await req.repositories.category.all(ownerId: user?.id)
95 | return try await req.view.render("front/categories",frontWrapper(req, data: .init(categories)))
96 | }
97 |
98 | // 添加评论
99 | private func addComment(_ req: Request) async throws -> OutJson {
100 | let user = try req.auth.require(User.self)
101 | try InComment.validate(content: req)
102 | let param = try req.content.decode(InComment.self)
103 | let _ = try await req.repositories.comment.add(param: param, fromUserId: user.requireID())
104 | return OutJson(success: OutOk())
105 | }
106 |
107 | // 获取文章的获取评论列表
108 | private func commentAddReply(_ req: Request) async throws -> OutJson {
109 | let user = try req.auth.require(User.self)
110 | try InReply.validate(content: req)
111 | let param = try req.content.decode(InReply.self)
112 | let _ = try await req.repositories.reply.add(param: param, fromUserId: user.requireID())
113 | return OutJson(success: OutOk())
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Sources/App/Error/ApiError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | struct ApiError {
11 | var content: OutStatus
12 |
13 | init(code: OutStatus) {
14 | self.content = code
15 | }
16 | }
17 |
18 | extension ApiError: AbortError {
19 | var status: HTTPResponseStatus { .ok }
20 | var reason: String {
21 | return self.content.message
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/App/LeafTag/JsonB64Tag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/18.
6 | //
7 |
8 | import Foundation
9 | import LeafKit
10 |
11 | enum JsonTagError: Error {
12 | case missingNameParameter
13 | }
14 |
15 | // UnsafeUnescapedLeafTag vs LeafTag 的区别
16 | struct Json2B64Tag: LeafTag {
17 | func render(_ ctx: LeafContext) throws -> LeafData {
18 | guard let name = ctx.parameters.first else {
19 | throw JsonTagError.missingNameParameter
20 | }
21 | return LeafData.string(name.jsonString.base64String())
22 | }
23 | }
24 |
25 | struct B642JsonTag: UnsafeUnescapedLeafTag {
26 | func render(_ ctx: LeafContext) throws -> LeafData {
27 | guard let name = ctx.parameters.first?.string else {
28 | throw JsonTagError.missingNameParameter
29 | }
30 | return LeafData.string("""
31 | var decoder = new TextDecoder();
32 | var decodedString = decoder.decode(new Uint8Array(Array.from(atob(\(name))).map(c => c.charCodeAt(0))));
33 | var \(name) = JSON.parse(decodedString);
34 | """)
35 | }
36 |
37 | }
38 |
39 | private extension LeafData {
40 | var jsonString: String {
41 | guard !isNil else {
42 | return "null"
43 | }
44 | switch celf {
45 | case .array:
46 | let items = array!.map { $0.jsonString }
47 | .joined(separator: ", ")
48 | return "[\(items)]"
49 | case .bool:
50 | return bool! ? "true" : "false"
51 | case .data:
52 | return "\"\(data!.base64EncodedString())\""
53 | case .dictionary:
54 | let items = dictionary!.map { key, value in "\"\(key)\": \(value.jsonString)" }
55 | .joined(separator: ", ")
56 | return "{\(items)}"
57 | case .double:
58 | return String(double!)
59 | case .int:
60 | return String(int!)
61 | case .string:
62 | let encoder = JSONEncoder()
63 | do {
64 | let encData = try encoder.encode(string!)
65 | return String(data: encData, encoding: .utf8) ?? ""
66 | } catch {
67 | return ""
68 | }
69 | case .void:
70 | return "null"
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/App/Middlewares/ErrorMiddleware.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | extension ErrorMiddleware {
11 | public static func custom(environment: Environment) -> ErrorMiddleware {
12 | return .init { req, error in
13 | let status: HTTPResponseStatus
14 | let reason: String
15 | let headers: HTTPHeaders
16 | var code: Int?
17 |
18 | switch error {
19 | case let abort as AbortError:
20 | reason = abort.reason
21 | status = abort.status
22 | headers = abort.headers
23 |
24 | if let apierror = abort as? ApiError {
25 | code = apierror.content.code
26 | }
27 |
28 | default:
29 | reason = environment.isRelease
30 | ? "Something went wrong."
31 | : String(describing: error)
32 | status = .internalServerError
33 | headers = [:]
34 | }
35 |
36 | req.logger.report(error: error)
37 | let response = Response(status: status, headers: headers)
38 |
39 | do {
40 | let errorResponse = OutStatus(code: code ?? 1008611, message: reason)
41 | response.body = try .init(data: JSONEncoder().encode(errorResponse))
42 | response.headers.replaceOrAdd(name: .contentType, value: "application/json; charset=utf-8")
43 | } catch {
44 | response.body = .init(string: "Oops: \(error)")
45 | response.headers.replaceOrAdd(name: .contentType, value: "text/plain; charset=utf-8")
46 | }
47 | return response
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateCategory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreateCategory: AsyncMigration {
12 |
13 | func prepare(on database: Database) async throws {
14 | try await database.schema(Category.schema)
15 | .id()
16 | .field(Category.FieldKeys.name, .string)
17 | .field(Category.FieldKeys.status, .int, .required)
18 | .field(Category.FieldKeys.isNav, .bool, .required)
19 | .field(Category.FieldKeys.ownerId, .uuid, .required)
20 | .field(Category.FieldKeys.createdAt, .datetime)
21 | .field(Category.FieldKeys.updatedAt, .datetime)
22 | .create()
23 | }
24 |
25 | func revert(on database: Database) async throws {
26 | try await database.schema(Category.schema).delete()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateComment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 |
8 |
9 | import Vapor
10 | import Fluent
11 |
12 | struct CreateComment: AsyncMigration {
13 | func prepare(on database: Database) async throws {
14 | try await database.schema(Comment.schema)
15 | .id()
16 | .field(Comment.FieldKeys.status, .int, .required)
17 | .field(Comment.FieldKeys.content, .string, .required)
18 | .field(Comment.FieldKeys.topicId, .uuid, .required)
19 | .field(Comment.FieldKeys.topicType, .int, .required)
20 | .field(Comment.FieldKeys.fromUid, .uuid, .required)
21 | .field(Comment.FieldKeys.createdAt, .datetime)
22 | .field(Comment.FieldKeys.updatedAt, .datetime)
23 | .create()
24 | }
25 |
26 | func revert(on database: Database) async throws {
27 | try await database.schema(Comment.schema).delete()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateEmailCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/14.
6 | //
7 |
8 | import Fluent
9 |
10 | struct CreateEmailCode: AsyncMigration {
11 | func prepare(on database: Database) async throws {
12 | try await database.schema(EmailCode.schema)
13 | .id()
14 | .field(EmailCode.FieldKeys.email, .string, .required)
15 | .field(EmailCode.FieldKeys.code, .string, .required)
16 | .field(EmailCode.FieldKeys.status, .int, .required)
17 | .field(EmailCode.FieldKeys.type, .string, .required)
18 | .field(EmailCode.FieldKeys.createdAt, .datetime)
19 | .field(EmailCode.FieldKeys.updatedAt, .datetime)
20 | .create()
21 | }
22 |
23 | func revert(on database: Database) async throws {
24 | try await database.schema(EmailCode.schema).delete()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateInvite.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/23.
6 | //
7 |
8 | import Fluent
9 |
10 | struct CreateInvite: AsyncMigration {
11 | func prepare(on database: Database) async throws {
12 | try await database.schema(Invite.schema)
13 | .id()
14 | .field(Invite.FieldKeys.code, .string)
15 | .create()
16 | }
17 |
18 | func revert(on database: Database) async throws {
19 | try await database.schema(Invite.schema).delete()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 |
8 |
9 | import Vapor
10 | import Fluent
11 |
12 | struct CreateLink: AsyncMigration {
13 | func prepare(on database: Database) async throws {
14 | try await database.schema(Link.schema)
15 | .id()
16 | .field(Link.FieldKeys.title, .string)
17 | .field(Link.FieldKeys.status, .int, .required)
18 | .field(Link.FieldKeys.href, .string)
19 | .field(Link.FieldKeys.weight, .int, .required)
20 | .field(Link.FieldKeys.ownerId, .uuid, .required)
21 | .field(Link.FieldKeys.createdAt, .datetime)
22 | .field(Link.FieldKeys.updatedAt, .datetime)
23 | .create()
24 | }
25 |
26 | func revert(on database: Database) async throws {
27 | try await database.schema(Link.schema).delete()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreateMenu: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(Menu.schema)
14 | .id()
15 | .field(Menu.FieldKeys.name, .string, .required)
16 | .field(Menu.FieldKeys.status, .int, .required)
17 | .field(Menu.FieldKeys.url, .string, .required)
18 | .field(Menu.FieldKeys.weight, .int, .required)
19 | .field(Menu.FieldKeys.parentId, .uuid)
20 | .field(Menu.FieldKeys.createdAt, .datetime)
21 | .field(Menu.FieldKeys.updatedAt, .datetime)
22 | .create()
23 | }
24 |
25 | func revert(on database: Database) async throws {
26 | try await database.schema(Menu.schema).delete()
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreateMessage: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(Message.schema)
14 | .id()
15 | .field(Message.FieldKeys.status, .int, .required)
16 | .field(Message.FieldKeys.senderId, .uuid, .required)
17 | .field(Message.FieldKeys.receiverId, .uuid, .required)
18 | .field(Message.FieldKeys.msgInfoId, .uuid, .required)
19 | .field(Message.FieldKeys.createdAt, .datetime)
20 | .field(Message.FieldKeys.updatedAt, .datetime)
21 | .create()
22 | }
23 |
24 | func revert(on database: Database) async throws {
25 | try await database.schema(Message.schema).delete()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateMessageInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreateMessageInfo: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(MessageInfo.schema)
14 | .id()
15 | .field(MessageInfo.FieldKeys.targetId, .uuid)
16 | .field(MessageInfo.FieldKeys.title, .string, .required)
17 | .field(MessageInfo.FieldKeys.content, .string, .required)
18 | .field(MessageInfo.FieldKeys.msgType, .int, .required)
19 | .field(MessageInfo.FieldKeys.createdAt, .datetime)
20 | .field(MessageInfo.FieldKeys.updatedAt, .datetime)
21 | .create()
22 | }
23 |
24 | func revert(on database: Database) async throws {
25 | try await database.schema(MessageInfo.schema).delete()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreatePermission.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 |
9 | import Vapor
10 | import Fluent
11 |
12 | struct CreatePermission: AsyncMigration {
13 | func prepare(on database: Database) async throws {
14 | try await database.schema(Permission.schema)
15 | .id()
16 | .field(Permission.FieldKeys.name, .string, .required)
17 | .field(Permission.FieldKeys.code, .string, .required)
18 | .field(Permission.FieldKeys.desc, .string)
19 | .field(Permission.FieldKeys.createdAt, .datetime)
20 | .field(Permission.FieldKeys.updatedAt, .datetime)
21 | .unique(on: Permission.FieldKeys.code)
22 | .create()
23 | }
24 |
25 | func revert(on database: Database) async throws {
26 | try await database.schema(Permission.schema).delete()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreatePermissionMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreatePermissionMenu: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(PermissionMenu.schema)
14 | .id()
15 | .field(PermissionMenu.FieldKeys.permissionId, .uuid, .required)
16 | .field(PermissionMenu.FieldKeys.menuId, .uuid, .required)
17 | .create()
18 | }
19 |
20 | func revert(on database: Database) async throws {
21 | try await database.schema(PermissionMenu.schema).delete()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreatePost.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreatePost: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(Post.schema)
14 | .id()
15 | .field(Post.FieldKeys.title, .string)
16 | .field(Post.FieldKeys.status, .int, .required)
17 | .field(Post.FieldKeys.desc, .string)
18 | .field(Post.FieldKeys.content, .string, .required)
19 | .field(Post.FieldKeys.ownerId, .uuid, .required)
20 | .field(Post.FieldKeys.categoryId, .uuid, .required)
21 | .field(Post.FieldKeys.createdAt, .datetime)
22 | .field(Post.FieldKeys.updatedAt, .datetime)
23 | .create()
24 | }
25 |
26 | func revert(on database: Database) async throws {
27 | try await database.schema(Post.schema).delete()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreatePostTag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 |
8 |
9 | import Vapor
10 | import Fluent
11 |
12 | struct CreatePostTag: AsyncMigration {
13 | func prepare(on database: Database) async throws {
14 | try await database.schema(PostTag.schema)
15 | .id()
16 | .field(PostTag.FieldKeys.postId, .uuid, .required)
17 | .field(PostTag.FieldKeys.tagId, .uuid, .required)
18 | .create()
19 | }
20 |
21 | func revert(on database: Database) async throws {
22 | try await database.schema(PostTag.schema).delete()
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateReply.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreateReply: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(Reply.schema)
14 | .id()
15 | .field(Reply.FieldKeys.status, .int, .required)
16 | .field(Reply.FieldKeys.content, .string, .required)
17 | .field(Reply.FieldKeys.targetId, .uuid, .required)
18 | .field(Reply.FieldKeys.targetType, .int, .required)
19 | .field(Reply.FieldKeys.fromUid, .uuid, .required)
20 | .field(Reply.FieldKeys.toUid, .uuid)
21 | .field(Reply.FieldKeys.commentId, .uuid, .required)
22 | .field(Reply.FieldKeys.createdAt, .datetime)
23 | .field(Reply.FieldKeys.updatedAt, .datetime)
24 | .create()
25 | }
26 |
27 | func revert(on database: Database) async throws {
28 | try await database.schema(Reply.schema).delete()
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateRole.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreateRole: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(Role.schema)
14 | .id()
15 | .field(Role.FieldKeys.name, .string, .required)
16 | .field(Role.FieldKeys.desc, .string)
17 | .field(Role.FieldKeys.createdAt, .datetime)
18 | .field(Role.FieldKeys.updatedAt, .datetime)
19 | .create()
20 | }
21 |
22 | func revert(on database: Database) async throws {
23 | try await database.schema(Role.schema).delete()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateRolePermission.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CreateRolePermission: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(RolePermission.schema)
14 | .id()
15 | .field(RolePermission.FieldKeys.permissionId, .uuid, .required)
16 | .field(RolePermission.FieldKeys.roleId, .uuid, .required)
17 | .create()
18 | }
19 |
20 | func revert(on database: Database) async throws {
21 | try await database.schema(RolePermission.schema).delete()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateSideBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 |
8 |
9 | import Vapor
10 | import Fluent
11 |
12 | struct CreateSideBar: AsyncMigration {
13 | func prepare(on database: Database) async throws {
14 | try await database.schema(SideBar.schema)
15 | .id()
16 | .field(SideBar.FieldKeys.title, .string)
17 | .field(SideBar.FieldKeys.status, .int, .required)
18 | .field(SideBar.FieldKeys.displayType, .int)
19 | .field(SideBar.FieldKeys.content, .string, .required)
20 | .field(SideBar.FieldKeys.ownerId, .uuid, .required)
21 | .field(SideBar.FieldKeys.createdAt, .datetime)
22 | .field(SideBar.FieldKeys.updatedAt, .datetime)
23 | .create()
24 | }
25 |
26 | func revert(on database: Database) async throws {
27 | try await database.schema(SideBar.schema).delete()
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateTag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 | import Vapor
8 | import Fluent
9 |
10 | struct CreateTag: AsyncMigration {
11 | func prepare(on database: Database) async throws {
12 | try await database.schema(Tag.schema)
13 | .id()
14 | .field(Tag.FieldKeys.name, .string)
15 | .field(Tag.FieldKeys.status, .int, .required)
16 | .field(Tag.FieldKeys.ownerId, .uuid, .required)
17 | .field(Tag.FieldKeys.createdAt, .datetime)
18 | .field(Tag.FieldKeys.updatedAt, .datetime)
19 | .create()
20 | }
21 |
22 | func revert(on database: Database) async throws {
23 | try await database.schema(Tag.schema).delete()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | struct CreateUser: AsyncMigration {
12 | func prepare(on database: Database) async throws {
13 | try await database.schema(User.schema)
14 | .id()
15 | .field(User.FieldKeys.name, .string, .required)
16 | .field(User.FieldKeys.email, .string, .required)
17 | .field(User.FieldKeys.isEmailVerified, .bool, .required, .custom("DEFAULT FALSE"))
18 | .field(User.FieldKeys.status, .int)
19 | .field(User.FieldKeys.isAdmin, .bool)
20 | .field(User.FieldKeys.inviteCode, .string)
21 | .field(User.FieldKeys.superiorId, .uuid)
22 | .field(User.FieldKeys.createdAt, .datetime)
23 | .field(User.FieldKeys.updatedAt, .datetime)
24 | .unique(on: User.FieldKeys.email)
25 | .create()
26 | }
27 |
28 | func revert(on database: Database) async throws {
29 | try await database.schema(User.schema).delete()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateUserAuth.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2022/8/30.
6 | //
7 |
8 | import Fluent
9 | struct CreateUserAuth: AsyncMigration {
10 | func prepare(on database: Database) async throws {
11 | try await database.schema(UserAuth.schema)
12 | .id()
13 | .field(UserAuth.FieldKeys.userId, .uuid, .references(User.schema, .id))
14 | .field(UserAuth.FieldKeys.authType, .string, .required)
15 | .field(UserAuth.FieldKeys.identifier, .string, .required)
16 | .field(UserAuth.FieldKeys.credential, .string, .required)
17 | .field(UserAuth.FieldKeys.createdAt, .datetime)
18 | .field(UserAuth.FieldKeys.updatedAt, .datetime)
19 | .create()
20 | }
21 |
22 | func revert(on database: Database) async throws {
23 | try await database.schema(UserAuth.schema).delete()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/App/Migrations/V1/CreateUserRole.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 |
9 |
10 | import Vapor
11 | import Fluent
12 |
13 | struct CreateUserRole: AsyncMigration {
14 | func prepare(on database: Database) async throws {
15 | try await database.schema(UserRole.schema)
16 | .id()
17 | .field(UserRole.FieldKeys.userId, .uuid, .required)
18 | .field(UserRole.FieldKeys.roleId, .uuid, .required)
19 | .create()
20 | }
21 |
22 | func revert(on database: Database) async throws {
23 | try await database.schema(UserRole.schema).delete()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/In.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | protocol In: Content {}
11 |
12 |
13 | // 特殊的整形,因为表单提交的时候,值一般都是字符串。
14 | enum SpecInt: Codable {
15 | case int(Int)
16 | case string(String)
17 |
18 | // 强制转为整形
19 | func castInt(def: Int = 1) -> Int {
20 | switch self {
21 | case .int(let v): return v
22 | case .string(let s): return Int(s) ?? def
23 | }
24 | }
25 |
26 | init(from decoder: Decoder) throws {
27 | let container = try decoder.singleValueContainer()
28 | if let intValue = try? container.decode(Int.self) {
29 | self = .int(intValue)
30 | } else if let stringValue = try? container.decode(String.self) {
31 | self = .string(stringValue)
32 | } else {
33 | throw DecodingError.typeMismatch(SpecInt.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Value cannot be decoded as Int or String."))
34 | }
35 | }
36 |
37 | func encode(to encoder: Encoder) throws {
38 | var container = encoder.singleValueContainer()
39 | switch self {
40 | case .int(let intValue):
41 | try container.encode(intValue)
42 | case .string(let stringValue):
43 | try container.encode(stringValue)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InCategory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InCategory: In {
12 | let name: String // 名称
13 | let isNav: Bool
14 | }
15 |
16 | extension InCategory: Validatable {
17 | static func validations(_ validations: inout Validations) {
18 | validations.add("name", as: String.self, is: .count(1...50))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/14.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InCode: In {
11 | let email: String
12 | }
13 |
14 | extension InCode: Validatable {
15 | static func validations(_ validations: inout Validations) {
16 | validations.add("email", as: String.self, is: .email)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InComment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/3.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InComment: In {
12 | let topicId: UUID
13 | let content: String
14 | let topicType: Int // 1: 文章
15 | }
16 |
17 | extension InComment: Validatable {
18 | static func validations(_ validations: inout Validations) {
19 |
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InDeleteIds.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/17.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InDeleteIds: In {
11 | let ids: [UUID]
12 | }
13 |
14 | extension InDeleteIds: Validatable {
15 | static func validations(_ validations: inout Validations) {
16 | validations.add("ids", as: [UUID].self, is: !.empty)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InLink: In {
12 | let title: String // 名称
13 | let href: String // href
14 | let weight: SpecInt
15 | }
16 |
17 | extension InLink: Validatable {
18 | static func validations(_ validations: inout Validations) {
19 | validations.add("title", as: String.self, is: .count(1...20))
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InLogin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InLogin: In {
11 | let email: String
12 | let password: String
13 | }
14 |
15 | extension InLogin: Validatable {
16 | static func validations(_ validations: inout Validations) {
17 | validations.add("email", as: String.self, is: .email)
18 | validations.add("password", as: String.self, is: .count(8...))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InMenu: In {
11 |
12 | let name: String
13 | let parentId: UUID?
14 | let weight: SpecInt
15 | let url: String
16 |
17 | init(name: String, weight: Int, url: String, parentId: UUID? = nil) {
18 | self.name = name
19 | self.weight = .int(weight)
20 | self.url = url
21 | self.parentId = parentId
22 | }
23 |
24 | }
25 |
26 | extension InMenu: Validatable {
27 | static func validations(_ validations: inout Validations) {
28 |
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InPermission.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InPermission: In {
11 | let name: String
12 | let code: String
13 | let desc: String?
14 |
15 | init(name: String, code: String, desc: String? = nil) {
16 | self.name = name
17 | self.code = code
18 | self.desc = desc
19 | }
20 | }
21 |
22 | extension InPermission: Validatable {
23 | static func validations(_ validations: inout Validations) {
24 |
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InPost.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InPost: In {
12 | let title: String // 名称
13 | let content: String
14 | let desc: String
15 | let categoryId: UUID
16 | let tagIds: [UUID]
17 | }
18 |
19 | extension InPost: Validatable {
20 | static func validations(_ validations: inout Validations) {
21 | validations.add("title", as: String.self, is: .count(1...100))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InRefreshToken.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/14.
6 | //
7 |
8 | struct InRefreshToken: In {
9 | var refreshToken: String
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InRegister.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InRegister: In {
11 | var name: String
12 | var email: String
13 | var password: String
14 | // 验证码
15 | var code: String
16 | // 邀请码
17 | var inviteCode: String?
18 | }
19 |
20 | extension InRegister: Validatable {
21 | // API 解码数据之前,对传入的请求进行验证
22 | static func validations(_ validations: inout Validations) {
23 | validations.add("name", as: String.self, is: !.empty)
24 | validations.add("email", as: String.self, is: .email)
25 | validations.add("password", as: String.self, is: .count(8...))
26 | validations.add("code", as: String.self, is: !.empty)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InReply.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/3.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InReply: In {
12 | let commentId: UUID // 根评论id
13 | let content: String
14 | let toUserId: UUID? // @xxx
15 | let targetId: UUID // 评论id, 或者回复id
16 | let targetType: SpecInt // 1: 评论 2:回复
17 | }
18 |
19 | extension InReply: Validatable {
20 | static func validations(_ validations: inout Validations) {
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InResetpwd.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/14.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InResetpwd: In {
11 | let email: String
12 | let pwd: String
13 | let code: String
14 | }
15 |
16 | extension InResetpwd: Validatable {
17 | static func validations(_ validations: inout Validations) {
18 | validations.add("email", as: String.self, is: .email)
19 | validations.add("pwd", as: String.self, is: .count(8...))
20 | validations.add("code", as: String.self, is: !.empty)
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InRole.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InRole: In {
11 | let name: String
12 | }
13 |
14 | extension InRole: Validatable {
15 | static func validations(_ validations: inout Validations) {
16 |
17 | }
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InSearchPost.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct InSearchPost: In {
11 | let categoryId: UUID?
12 | let tagId: UUID?
13 | let searchKey: String? // 搜索的词
14 | let listFor: String? // tag, category, search
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InTag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/9.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InTag: In {
12 | let name: String // tag 名称
13 | }
14 |
15 | extension InTag: Validatable {
16 | static func validations(_ validations: inout Validations) {
17 | validations.add("name", as: String.self, is: .count(1...10))
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdateCategory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InUpdateCategory: In {
12 | let name: String // tag 名称
13 | let id: UUID // tag 的id
14 | let isNav: Bool
15 | }
16 |
17 | extension InUpdateCategory: Validatable {
18 | static func validations(_ validations: inout Validations) {
19 | validations.add("name", as: String.self, is: .count(1...50))
20 | validations.add("id", as: String.self, is: !.empty)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdateLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InUpdateLink: In {
12 | let title: String // tag 名称
13 | let href: String
14 | let weight: SpecInt
15 | let id: UUID // tag 的id
16 | }
17 |
18 | extension InUpdateLink: Validatable {
19 | static func validations(_ validations: inout Validations) {
20 | validations.add("title", as: String.self, is: .count(1...10))
21 | validations.add("id", as: String.self, is: !.empty)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdateMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 | import Vapor
8 |
9 | struct InUpdateMenu: In {
10 | let id: UUID
11 | let name: String
12 | let parentId: UUID?
13 | let weight: SpecInt
14 | let url: String
15 | }
16 |
17 | extension InUpdateMenu: Validatable {
18 | static func validations(_ validations: inout Validations) {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdatePermission.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 | import Vapor
8 |
9 | struct InUpdatePermission: In {
10 | let name: String
11 | let code: String
12 | let desc: String?
13 | let id: UUID
14 | let menuIds: [UUID]
15 | }
16 |
17 | extension InUpdatePermission: Validatable {
18 | static func validations(_ validations: inout Validations) {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdatePost.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InUpdatePost: In {
12 | let title: String // 名称
13 | let id: UUID // tag 的id
14 | let desc: String
15 | let content: String
16 | let categoryId: UUID
17 | let tagIds: [UUID]
18 | }
19 |
20 | extension InUpdatePost: Validatable {
21 | static func validations(_ validations: inout Validations) {
22 | validations.add("title", as: String.self, is: .count(1...100))
23 | validations.add("id", as: String.self, is: !.empty)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdateRole.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InUpdateRole: In {
11 | let name: String
12 | let id: UUID
13 | let permissionIds: [UUID]
14 | }
15 |
16 | extension InUpdateRole: Validatable {
17 | static func validations(_ validations: inout Validations) {
18 |
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdateTag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/17.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InUpdateTag: In {
12 | let name: String // tag 名称
13 | let id: UUID // tag 的id
14 | }
15 |
16 | extension InUpdateTag: Validatable {
17 | static func validations(_ validations: inout Validations) {
18 | validations.add("name", as: String.self, is: .count(1...10))
19 | validations.add("id", as: String.self, is: !.empty)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdateUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/29.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 |
11 | struct InUpdateUser: In {
12 | let id: UUID
13 | let roleIds: [UUID] // 权限id
14 | }
15 |
16 | extension InUpdateUser: Validatable {
17 | static func validations(_ validations: inout Validations) {
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/In/InUpdatepwd.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/14.
6 | //
7 |
8 | import Vapor
9 |
10 | struct InUpdatepwd: In {
11 | let email: String // 邮箱
12 | let pwd: String // 旧密码
13 | let newpwd: String // 新密码
14 | }
15 |
16 | extension InUpdatepwd: Validatable {
17 | static func validations(_ validations: inout Validations) {
18 | validations.add("email", as: String.self, is: .email)
19 | validations.add("pwd", as: String.self, is: .count(8...))
20 | validations.add("newpwd", as: String.self, is: .count(8...))
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Category+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 |
10 | extension Category {
11 | struct Public: Out {
12 | let id: UUID?
13 | let name: String
14 | let status: Int
15 | let ownerId: UUID
16 | let isNav: Bool
17 | let owner: User.Public?
18 | }
19 |
20 | func asPublic() -> Public {
21 | return Public(
22 | id: self.id,
23 | name: self.name,
24 | status: self.status,
25 | ownerId: self.$owner.id,
26 | isNav: self.isNav,
27 | owner: self.$owner.value?.asPublic()
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Comment+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Vapor
9 |
10 | extension Comment {
11 |
12 | struct Public: Out {
13 | let id: UUID?
14 | let status: Int
15 | let content: String
16 | let topicId: UUID
17 | let topicType: Int
18 | let fromUid: UUID
19 | let fromUser: User.Public?
20 | let createdAt: Date?
21 | let replys: [Reply.Public]?
22 | }
23 |
24 | func asPublic() -> Public {
25 | return Public(id: self.id,
26 | status: self.status,
27 | content: self.content,
28 | topicId: self.topicId,
29 | topicType: self.topicType,
30 | fromUid: self.$fromUser.id,
31 | fromUser: self.$fromUser.value?.asPublic(),
32 | createdAt: self.createdAt,
33 | replys: self.$replys.value?.map({$0.asPublic(list: self.$replys.value ?? [])})
34 | )
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Link+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 |
10 | extension Link {
11 | struct Public: Out {
12 | let id: UUID?
13 | let title: String
14 | let status: Int
15 | let ownerId: UUID
16 | let href: String
17 | let weight: Int
18 | let owner: User.Public?
19 | }
20 |
21 | func asPublic() -> Public {
22 | return Public(
23 | id: self.id,
24 | title: self.title,
25 | status: self.status,
26 | ownerId: self.$owner.id,
27 | href: self.href,
28 | weight: self.weight,
29 | owner: self.$owner.value?.asPublic()
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Menu+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Menu {
11 | struct Public: Out {
12 | let id: UUID?
13 | let name: String
14 | let url: String
15 | let weight: Int
16 | let parentId: Menu.IDValue?
17 | let children: [Public]?
18 |
19 | func mergeWith(children: [Public]?) -> Public {
20 | return Public(id: self.id, name: self.name, url: self.url, weight: self.weight, parentId: self.parentId, children: children)
21 | }
22 | }
23 |
24 | func asPublic(with children: [Public]? = nil) -> Public {
25 | return Public(id: self.id, name: self.name, url: self.url, weight: self.weight, parentId: self.$parent.id, children: children)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Message+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Vapor
9 |
10 | extension Message {
11 |
12 | struct Public: Out {
13 | let id: UUID?
14 | let status: Int
15 | let senderId: UUID
16 | let receiverId: UUID
17 | let messageInfoId: UUID
18 | let createdAt: Date?
19 | let receiver: User.Public?
20 | let sender: User.Public?
21 |
22 | }
23 |
24 | func asPublic() -> Public {
25 | return Public(id: self.id,
26 | status: self.status,
27 | senderId: self.$sender.id,
28 | receiverId: self.$receiver.id,
29 | messageInfoId: self.$messageInfo.id,
30 | createdAt: self.createdAt,
31 | receiver: self.$receiver.value?.asPublic(),
32 | sender: self.$sender.value?.asPublic())
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/MessageInfo+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Vapor
9 |
10 | extension MessageInfo {
11 |
12 | struct Public: Out {
13 | let id: UUID?
14 | let targetId: UUID?
15 | let title: String
16 | let content: String
17 | let msgType: Int
18 | let createdAt: Date?
19 |
20 | }
21 |
22 | func asPublic() -> Public {
23 | return Public(id: self.id,
24 | targetId: self.targetId,
25 | title: self.title,
26 | content: self.content,
27 | msgType: self.msgType,
28 | createdAt: self.createdAt
29 | )
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | typealias Out = Content
12 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/OutJson.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | struct OutJson: Out, OutCodeMsg {
11 | var code: Int
12 | var message: String
13 | let data: T?
14 |
15 | init(code: OutStatus, data: T?) {
16 | self.code = code.code
17 | self.message = code.message
18 | self.data = data
19 | }
20 |
21 | init(success data: T) {
22 | self.init(code: .ok, data: data)
23 | }
24 |
25 | init(error code: OutStatus) {
26 | self.init(code: code, data: nil)
27 | }
28 |
29 | var isOk: Bool {
30 | return code == OutStatus.ok.code
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/OutOk.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | struct OutOk: Out {}
9 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/OutStatus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | protocol OutCodeMsg {
9 | var code: Int { get }
10 | var message: String { get }
11 | }
12 |
13 | struct OutStatus: Out, OutCodeMsg {
14 | var code: Int
15 | var message: String
16 |
17 | init(code: Int, message: String) {
18 | self.code = code
19 | self.message = message
20 | }
21 | }
22 |
23 | // 状态值
24 | extension OutStatus {
25 | static var ok = OutStatus(code: 0, message: "请求成功")
26 | static var userExist = OutStatus(code: 20, message: "用户已经存在")
27 | static var userNotExist = OutStatus(code: 21, message: "用户不存在")
28 | static var passwordError = OutStatus(code: 22, message: "密码错误")
29 | static var emailNotExist = OutStatus(code: 23, message: "邮箱不存在")
30 | static var invalidEmailOrPassword = OutStatus(code: 26, message: "邮箱或密码错误")
31 | static var invalidEmailCode: OutStatus = OutStatus(code: 27, message: "验证码错误")
32 | static var emailCodeExpired = OutStatus(code: 28, message: "验证码已过期,请重新获取")
33 | static var inviteUserNotExist = OutStatus(code: 29, message: "邀请码不存在")
34 | static var postNotExist = OutStatus(code: 30, message: "文章不存在")
35 | static var roleNotExist = OutStatus(code: 31, message: "角色不存在")
36 | static var permissionNotExist = OutStatus(code: 32, message: "权限不存在")
37 | static var userRoleNotExist = OutStatus(code: 33, message: "普通用户角色未设置")
38 | static var menuNotConfig = OutStatus(code: 34, message: "菜单未配置")
39 | static var configAlready = OutStatus(code: 35, message: "已初始化配置")
40 | static var dbNotSupport = OutStatus(code: 36, message: "数据库不支持sql查询")
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/OutToken.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/14.
6 | //
7 |
8 | struct OutToken: Out {
9 | let token: String
10 | let refreshToken: String
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Permission+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Permission {
11 | struct Public: Out {
12 | let id: UUID?
13 | let name: String
14 | let code: String
15 | let desc: String?
16 | let menus: [Menu.Public]?
17 | let menuIds:[UUID]?
18 | }
19 |
20 | func asPublic() -> Public {
21 | return Public(id: self.id,
22 | name: self.name,
23 | code: self.code,
24 | desc: self.desc,
25 | menus: self.$menus.value?.map({$0.asPublic()}),
26 | menuIds: self.$menus.value?.map({$0.id!})
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Post+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 | import FluentKit
10 |
11 | extension Post {
12 | struct Public: Out {
13 | let id: UUID?
14 | let title: String
15 | let status: Int
16 | let ownerId: UUID
17 | let desc: String
18 | let content: String
19 | let categoryId: UUID
20 | let tagIds: [UUID]?
21 | let category: Category.Public?
22 | let tags: [Tag.Public]?
23 | let owner: User.Public?
24 | let createdAt: Date?
25 | let updatedAt: Date?
26 | }
27 |
28 | func asPublic() -> Public {
29 | return Public(
30 | id: self.id,
31 | title: self.title,
32 | status: self.status,
33 | ownerId: self.$owner.id,
34 | desc: self.desc,
35 | content: self.content,
36 | categoryId: self.$category.id,
37 | tagIds: self.$tags.value?.map{ $0.id! },
38 | category: self.$category.value?.asPublic(),
39 | tags: self.$tags.value?.map { $0.asPublic() },
40 | owner: self.$owner.value?.asPublic(),
41 | createdAt: self.createdAt,
42 | updatedAt: self.updatedAt
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Reply+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Vapor
9 |
10 | extension Reply {
11 |
12 | struct Public: Out {
13 | let id: UUID?
14 | let status: Int
15 | let content: String
16 | let targetId: UUID
17 | let targetType: Int
18 | let fromUid: UUID
19 | let toUid: UUID?
20 | let fromUser: User.Public?
21 | let toUser: User.Public?
22 | let commentId: UUID
23 | let comment: Comment.Public?
24 | let createdAt: Date?
25 | let targetContent: String?
26 | }
27 |
28 | func asPublic(list: [Reply] = []) -> Public {
29 | let cnt = list.first { rep in
30 | rep.id == targetId
31 | }
32 | return Public(id: self.id,
33 | status: self.status,
34 | content: self.content,
35 | targetId: self.targetId,
36 | targetType: self.targetType,
37 | fromUid: self.$fromUser.id,
38 | toUid: self.$toUser.id,
39 | fromUser: self.$fromUser.value?.asPublic(),
40 | toUser: self.$toUser.value??.asPublic(),
41 | commentId: self.$comment.id,
42 | comment: self.$comment.value?.asPublic(),
43 | createdAt: self.createdAt,
44 | targetContent: cnt?.content
45 | )
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Role+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Role {
11 | struct Public: Out {
12 | let id: UUID?
13 | let name: String
14 | let desc: String?
15 | let permissions: [Permission.Public]?
16 | let permissionIds: [UUID]?
17 | }
18 |
19 | func asPublic() -> Public {
20 | return Public(id: self.id,
21 | name: self.name,
22 | desc: self.desc,
23 | permissions: self.$permissions.value?.map({$0.asPublic()}),
24 | permissionIds: self.$permissions.value?.map({$0.id!})
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/Tag+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/9.
6 | //
7 |
8 | import Vapor
9 |
10 | extension Tag {
11 | struct Public: Out {
12 | let id: UUID?
13 | let name: String
14 | let status: Int
15 | let ownerId: UUID
16 | let owner: User.Public?
17 | }
18 |
19 | func asPublic() -> Public {
20 | return Public(
21 | id: self.id,
22 | name: self.name,
23 | status: self.status,
24 | ownerId: self.$owner.id,
25 | owner: self.$owner.value?.asPublic()
26 | )
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/App/Models/DTO/Out/User+Out.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | extension User {
11 | struct Public: Out {
12 | let id: UUID?
13 | let name: String
14 | let email: String
15 | let isEmailVerified: Bool
16 | let status: Int
17 | let isAdmin: Bool
18 | let roles: [Role.Public]?
19 | let roleIds: [UUID]?
20 | }
21 |
22 | func asPublic() -> Public {
23 | return Public(
24 | id: self.id,
25 | name: self.name,
26 | email: self.email,
27 | isEmailVerified: self.isEmailVerified,
28 | status: self.status,
29 | isAdmin: self.isAdmin,
30 | roles: self.$roles.value?.map({$0.asPublic()}),
31 | roleIds: self.$roles.value?.map({ $0.id!})
32 | )
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Category.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/9.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class Category: Model {
12 |
13 | static let schema = "blog_category"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Field(key: FieldKeys.name)
19 | var name: String
20 |
21 | @Field(key: FieldKeys.status)
22 | var status: Int // 正常|删除
23 |
24 | @Field(key: FieldKeys.isNav)
25 | var isNav: Bool
26 |
27 | @Parent(key: FieldKeys.ownerId)
28 | var owner: User
29 |
30 | @Timestamp(key: FieldKeys.createdAt, on: .create)
31 | var createdAt: Date?
32 |
33 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
34 | var updatedAt: Date?
35 |
36 | init() {}
37 |
38 | init(id: UUID? = nil, name: String, ownerId: User.IDValue, status: Int = 1, isNav: Bool = true) {
39 | self.id = id
40 | self.name = name
41 | self.$owner.id = ownerId
42 | self.status = status
43 | self.isNav = isNav
44 | }
45 | }
46 |
47 | extension Category {
48 | struct FieldKeys {
49 | static var name: FieldKey { "name" }
50 | static var status: FieldKey { "status" }
51 | static var isNav: FieldKey { "is_nav" }
52 | static var ownerId: FieldKey { "owner_id" }
53 | static var createdAt: FieldKey { "created_at" }
54 | static var updatedAt: FieldKey { "updated_at" }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Comment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/9.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class Comment: Model {
12 |
13 | static let schema = "blog_comment"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Field(key: FieldKeys.status)
19 | var status: Int // 正常(1)|删除(0)
20 |
21 | @Field(key: FieldKeys.content)
22 | var content: String
23 |
24 | @Field(key: FieldKeys.topicId)
25 | var topicId: UUID
26 |
27 | @Field(key: FieldKeys.topicType)
28 | var topicType: Int // 1: 文章
29 |
30 | @Parent(key: FieldKeys.fromUid)
31 | var fromUser: User
32 |
33 | @Children(for: \.$comment)
34 | var replys: [Reply]
35 |
36 | @Timestamp(key: FieldKeys.createdAt, on: .create)
37 | var createdAt: Date?
38 |
39 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
40 | var updatedAt: Date?
41 |
42 |
43 | init() { }
44 |
45 | init(id: UUID? = nil, content: String, userId: UUID, topicId: UUID, topicType: Int, status: Int = 1) {
46 | self.id = id
47 | self.content = content
48 | self.$fromUser.id = userId
49 | self.topicId = topicId
50 | self.topicType = topicType
51 | self.status = status
52 | }
53 | }
54 |
55 | extension Comment {
56 | struct FieldKeys {
57 | static var status: FieldKey { "status" }
58 | static var topicId: FieldKey { "topic_id" }
59 | static var topicType: FieldKey { "topic_type" }
60 | static var content: FieldKey { "content" }
61 | static var fromUid: FieldKey { "from_uid" }
62 | static var createdAt: FieldKey { "created_at" }
63 | static var updatedAt: FieldKey { "updated_at" }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/EmailCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/14.
6 | //
7 | import Fluent
8 | import Vapor
9 |
10 | /// 邮箱验证码
11 | final class EmailCode: Model {
12 | static var schema: String = "blog_email_codes"
13 |
14 | @ID(key: .id)
15 | var id: UUID?
16 |
17 | @Field(key: FieldKeys.email)
18 | var email: String
19 |
20 | @Field(key: FieldKeys.code)
21 | var code: String
22 |
23 | @Field(key: FieldKeys.type)
24 | var type: String // register, resetpwd
25 |
26 | @Field(key: FieldKeys.status)
27 | var status: Int // 1 为已使用,0 为未使用,默认为 0
28 |
29 | @Timestamp(key: FieldKeys.createdAt, on: .create)
30 | var createdAt: Date?
31 |
32 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
33 | var updatedAt: Date?
34 |
35 | init() {}
36 |
37 | init(id: UUID? = nil, email: String, code: String, status: Int = 0, type: String) {
38 | self.id = id
39 | self.email = email
40 | self.code = code
41 | self.status = status
42 | self.type = type
43 | }
44 | }
45 |
46 | extension EmailCode {
47 | struct FieldKeys {
48 | static var email: FieldKey { "email" }
49 | static var code: FieldKey { "code" }
50 | static var type: FieldKey { "type" }
51 | static var status: FieldKey { "status" }
52 | static var createdAt: FieldKey { "created_at" }
53 | static var updatedAt: FieldKey { "updated_at" }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Invite.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/23.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 | import Fluent
11 |
12 | final class Invite: Model {
13 | static var schema: String = "blog_invites"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Field(key: FieldKeys.code)
19 | var code:String
20 |
21 | init() {}
22 |
23 | init(code: String) {
24 | self.code = code
25 | }
26 | }
27 |
28 |
29 | extension Invite {
30 | struct FieldKeys {
31 | static var code: FieldKey { "code" }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Link.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/9.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class Link: Model {
12 |
13 | static let schema = "blog_link"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Field(key: FieldKeys.title)
19 | var title: String
20 |
21 | @Field(key: FieldKeys.href)
22 | var href: String
23 |
24 | @Field(key: FieldKeys.status)
25 | var status: Int // 正常|删除
26 |
27 | @Field(key: FieldKeys.weight)
28 | var weight: Int
29 |
30 | @Parent(key: FieldKeys.ownerId)
31 | var owner: User
32 |
33 | @Timestamp(key: FieldKeys.createdAt, on: .create)
34 | var createdAt: Date?
35 |
36 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
37 | var updatedAt: Date?
38 |
39 | init() { }
40 |
41 | init(id: UUID? = nil, title: String, href: String, status: Int = 1, weight: Int = 1, ownerId: User.IDValue) {
42 | self.id = id
43 | self.title = title
44 | self.href = href
45 | self.status = status
46 | self.weight = weight
47 | self.$owner.id = ownerId
48 | }
49 | }
50 |
51 | extension Link {
52 | struct FieldKeys {
53 | static var status: FieldKey { "status" }
54 | static var title: FieldKey { "title" }
55 | static var href: FieldKey { "href" }
56 | static var weight: FieldKey { "weight" }
57 | static var ownerId: FieldKey { "owner_id" }
58 | static var createdAt: FieldKey { "created_at" }
59 | static var updatedAt: FieldKey { "updated_at" }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Menu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 |
9 | import Fluent
10 | import Vapor
11 |
12 | /// 菜单: 角色和权限用于控制用户的访问权限,而菜单是根据用户的权限动态生成的
13 | final class Menu: Model {
14 |
15 | static let schema = "blog_menu"
16 |
17 | @ID(key: .id)
18 | var id: UUID?
19 |
20 | @Field(key: FieldKeys.name)
21 | var name: String
22 |
23 | @Field(key: FieldKeys.url)
24 | var url: String
25 |
26 | @Field(key: FieldKeys.status)
27 | var status: Int // 正常|删除
28 |
29 | @Field(key: FieldKeys.weight)
30 | var weight: Int
31 |
32 | @OptionalParent(key: FieldKeys.parentId) // 指向自身的外键
33 | var parent: Menu?
34 |
35 | @Siblings(through: PermissionMenu.self, from: \.$menu, to: \.$permission)
36 | var permissions: [Permission]
37 |
38 | @Timestamp(key: FieldKeys.createdAt, on: .create)
39 | var createdAt: Date?
40 |
41 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
42 | var updatedAt: Date?
43 |
44 | init() { }
45 |
46 | init(id: UUID? = nil, name: String, url: String, status: Int = 1, weight: Int = 1, parentId: Menu.IDValue? = nil) {
47 | self.id = id
48 | self.name = name
49 | self.url = url
50 | self.status = status
51 | self.weight = weight
52 | self.$parent.id = parentId
53 | }
54 | }
55 |
56 | extension Menu {
57 | struct FieldKeys {
58 | static var status: FieldKey { "status" }
59 | static var parentId: FieldKey { "parent_id" }
60 | static var name: FieldKey { "name" }
61 | static var url: FieldKey { "url" }
62 | static var weight: FieldKey { "weight" }
63 | static var createdAt: FieldKey { "created_at" }
64 | static var updatedAt: FieldKey { "updated_at" }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Message.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | /// 通知记录
12 | final class Message: Model {
13 |
14 | static let schema = "blog_message"
15 |
16 | @ID(key: .id)
17 | var id: UUID?
18 |
19 | @Field(key: FieldKeys.status)
20 | var status: Int // 1已查看,0未查看
21 |
22 | @Parent(key: FieldKeys.senderId)
23 | var sender: User
24 |
25 | @Parent(key: FieldKeys.receiverId)
26 | var receiver: User // 正常|删除
27 |
28 | @Parent(key: FieldKeys.msgInfoId)
29 | var messageInfo: MessageInfo // 消息内容
30 |
31 | @Timestamp(key: FieldKeys.createdAt, on: .create)
32 | var createdAt: Date?
33 |
34 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
35 | var updatedAt: Date?
36 |
37 | init() { }
38 |
39 | init(id: UUID? = nil, senderId: User.IDValue, receiverId: User.IDValue, msgInfoId: MessageInfo.IDValue, status: Int = 0) {
40 | self.id = id
41 | self.$sender.id = senderId
42 | self.$messageInfo.id = msgInfoId
43 | self.$receiver.id = receiverId
44 | self.status = 0
45 | }
46 | }
47 |
48 | extension Message {
49 | struct FieldKeys {
50 | static var status: FieldKey { "status" }
51 | static var senderId: FieldKey { "sender_id" }
52 | static var receiverId: FieldKey { "receiver_id" }
53 | static var msgInfoId: FieldKey { "msg_info_id" }
54 | static var createdAt: FieldKey { "created_at" }
55 | static var updatedAt: FieldKey { "updated_at" }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/MessageInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 |
9 | import Fluent
10 | import Vapor
11 |
12 | /// 通知详情
13 | final class MessageInfo: Model {
14 |
15 | static let schema = "blog_message_info"
16 |
17 | @ID(key: .id)
18 | var id: UUID?
19 |
20 | @OptionalField(key: FieldKeys.targetId)
21 | var targetId: UUID?
22 |
23 | @Field(key: FieldKeys.title)
24 | var title: String
25 |
26 | @Field(key: FieldKeys.content)
27 | var content: String
28 |
29 | @Field(key: FieldKeys.msgType)
30 | var msgType: Int // 1评论,2回复评论,3关注,4喜欢,5系统通知
31 |
32 | @Timestamp(key: FieldKeys.createdAt, on: .create)
33 | var createdAt: Date?
34 |
35 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
36 | var updatedAt: Date?
37 |
38 | init() { }
39 |
40 | init(id: UUID? = nil, targetId: UUID?, msgType: Int, title: String, content: String) {
41 | self.id = id
42 | self.targetId = targetId
43 | self.msgType = msgType
44 | self.title = title
45 | self.content = content
46 | }
47 | }
48 |
49 | extension MessageInfo {
50 | struct FieldKeys {
51 | static var targetId: FieldKey { "target_id" }
52 | static var title: FieldKey { "title" }
53 | static var content: FieldKey { "content" }
54 | static var msgType: FieldKey { "msg_type"}
55 | static var createdAt: FieldKey { "created_at" }
56 | static var updatedAt: FieldKey { "updated_at" }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Permission.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 | import Vapor
8 | import Fluent
9 |
10 | /// 权限
11 | final class Permission: Model {
12 | static let schema = "blog_permission"
13 |
14 | @ID(key: .id)
15 | var id: UUID?
16 |
17 | // 权限名字
18 | @Field(key: FieldKeys.name)
19 | var name: String
20 |
21 | // 权限代码,需要唯一,后面校验权限的时候需要用到
22 | @Field(key: FieldKeys.code)
23 | var code: String
24 |
25 | @OptionalField(key: FieldKeys.desc)
26 | var desc: String?
27 |
28 | // 权限可以对应多个角色
29 | @Siblings(through: RolePermission.self, from: \.$permission, to: \.$role)
30 | var roles: [Role]
31 |
32 | @Siblings(through: PermissionMenu.self, from: \.$permission, to: \.$menu)
33 | var menus: [Menu]
34 |
35 | @Timestamp(key: FieldKeys.createdAt, on: .create)
36 | var createdAt: Date?
37 |
38 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
39 | var updatedAt: Date?
40 |
41 | init() {}
42 |
43 | init(id: UUID? = nil, name: String, desc: String? = nil, code: String) {
44 | self.id = id
45 | self.name = name
46 | self.desc = desc
47 | self.code = code
48 | }
49 | }
50 |
51 | extension Permission {
52 | struct FieldKeys {
53 | static var name: FieldKey { "name" }
54 | static var desc: FieldKey { "desc" }
55 | static var code: FieldKey { "code" }
56 | static var createdAt: FieldKey { "created_at" }
57 | static var updatedAt: FieldKey { "updated_at" }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/PermissionMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 |
9 | import Fluent
10 | import Vapor
11 |
12 | final class PermissionMenu: Model {
13 | static let schema = "blog_permision_menu"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Parent(key: FieldKeys.permissionId)
19 | var permission: Permission
20 |
21 | @Parent(key: FieldKeys.menuId)
22 | var menu: Menu
23 |
24 | init() { }
25 |
26 | init(id: UUID? = nil, permissionId: Permission.IDValue, menuId:Menu.IDValue) {
27 | self.id = id
28 | self.$permission.id = permissionId
29 | self.$menu.id = menuId
30 | }
31 | }
32 |
33 | extension PermissionMenu {
34 | struct FieldKeys {
35 | static var menuId: FieldKey { "menu_id" }
36 | static var permissionId: FieldKey { "permission_id" }
37 | static var createdAt: FieldKey { "created_at" }
38 | static var updatedAt: FieldKey { "updated_at" }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Post.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/9.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class Post: Model {
12 | static let schema = "blog_post"
13 |
14 | @ID(key: .id)
15 | var id: UUID?
16 |
17 | @Field(key: FieldKeys.title)
18 | var title: String
19 |
20 | @Field(key: FieldKeys.desc)
21 | var desc: String
22 |
23 | @Field(key: FieldKeys.content)
24 | var content: String
25 |
26 | @Field(key: FieldKeys.status)
27 | var status: Int // 正常(1)|删除(0)|草稿(2),
28 |
29 | @Parent(key: FieldKeys.ownerId)
30 | var owner: User
31 |
32 | @Parent(key: FieldKeys.categoryId)
33 | var category: Category
34 |
35 | @Siblings(through: PostTag.self, from: \.$post, to: \.$tag)
36 | public var tags: [Tag]
37 |
38 | @Timestamp(key: FieldKeys.createdAt, on: .create)
39 | var createdAt: Date?
40 |
41 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
42 | var updatedAt: Date?
43 |
44 |
45 | init() {}
46 |
47 | init(id: UUID? = nil, title: String, ownerId: User.IDValue, status: Int = 1, content: String, desc: String, categoryId: Category.IDValue) {
48 | self.id = id
49 | self.title = title
50 | self.desc = desc
51 | self.content = content
52 | self.status = status
53 | self.$category.id = categoryId
54 | self.$owner.id = ownerId
55 | }
56 | }
57 |
58 | extension Post {
59 | struct FieldKeys {
60 | static var title: FieldKey { "title" }
61 | static var desc: FieldKey { "desc" }
62 | static var content: FieldKey { "content" }
63 | static var status: FieldKey { "status" }
64 | static var ownerId: FieldKey { "owner_id" }
65 | static var categoryId: FieldKey { "category_id" }
66 | static var createdAt: FieldKey { "created_at" }
67 | static var updatedAt: FieldKey { "updated_at" }
68 | }
69 | }
70 |
71 |
72 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/PostTag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/11.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class PostTag: Model {
12 | static let schema = "blog_planet_tag"
13 |
14 | @ID(key: .id)
15 | var id: UUID?
16 |
17 | @Parent(key: FieldKeys.postId)
18 | var post: Post
19 |
20 | @Parent(key: FieldKeys.tagId)
21 | var tag: Tag
22 |
23 | init() { }
24 |
25 | init(id: UUID? = nil, post: Post, tag: Tag) throws {
26 | self.id = id
27 | self.$post.id = try post.requireID()
28 | self.$tag.id = try tag.requireID()
29 | }
30 | }
31 |
32 | extension PostTag {
33 | struct FieldKeys {
34 | static var postId: FieldKey { "post_id" }
35 | static var tagId: FieldKey { "tag_id" }
36 | static var createdAt: FieldKey { "created_at" }
37 | static var updatedAt: FieldKey { "updated_at" }
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Reply.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/1.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class Reply: Model {
12 |
13 | static let schema = "blog_reply"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Field(key: FieldKeys.status)
19 | var status: Int // 正常(1)|删除(0)
20 |
21 | @Field(key: FieldKeys.content)
22 | var content: String
23 |
24 | @Field(key: FieldKeys.targetId)
25 | var targetId: UUID
26 |
27 | @Field(key: FieldKeys.targetType)
28 | var targetType: Int // 1: 评论 2:回复
29 |
30 | @Parent(key: FieldKeys.fromUid)
31 | var fromUser: User
32 |
33 | @OptionalParent(key: FieldKeys.toUid)
34 | var toUser: User?
35 |
36 | @Parent(key: FieldKeys.commentId)
37 | var comment: Comment // 根评论
38 |
39 | @Timestamp(key: FieldKeys.createdAt, on: .create)
40 | var createdAt: Date?
41 |
42 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
43 | var updatedAt: Date?
44 |
45 | init() { }
46 |
47 | init(id: UUID? = nil, commentId: Comment.IDValue, content: String, userId: User.IDValue, toUid: User.IDValue?, targetId: UUID, targetType: Int, status: Int = 1) {
48 | self.id = id
49 | self.$comment.id = commentId
50 | self.content = content
51 | self.$fromUser.id = userId
52 | self.targetId = targetId
53 | self.targetType = targetType
54 | self.$toUser.id = toUid
55 | self.status = status
56 | }
57 | }
58 |
59 | extension Reply {
60 | struct FieldKeys {
61 | static var status: FieldKey { "status" }
62 | static var commentId: FieldKey { "comment_id" }
63 | static var targetId: FieldKey { "target_id" }
64 | static var targetType: FieldKey { "target_type" }
65 | static var content: FieldKey { "content" }
66 | static var fromUid: FieldKey { "from_uid" }
67 | static var toUid: FieldKey { "to_uid" }
68 | static var createdAt: FieldKey { "created_at" }
69 | static var updatedAt: FieldKey { "updated_at" }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Role.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | // 角色
12 | final class Role: Model {
13 | static let schema = "blog_role"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Field(key: FieldKeys.name)
19 | var name: String
20 |
21 | @OptionalField(key: FieldKeys.desc)
22 | var desc: String?
23 |
24 | // 该角色的用户
25 | @Siblings(through: UserRole.self, from: \.$role, to: \.$user)
26 | var users: [User]
27 |
28 | // 该角色能够使用的权限
29 | @Siblings(through: RolePermission.self, from: \.$role, to: \.$permission)
30 | var permissions: [Permission]
31 |
32 | @Timestamp(key: FieldKeys.createdAt, on: .create)
33 | var createdAt: Date?
34 |
35 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
36 | var updatedAt: Date?
37 |
38 | init() {}
39 |
40 | init(id: UUID? = nil, name: String, desc: String? = nil) {
41 | self.id = id
42 | self.name = name
43 | self.desc = desc
44 | }
45 | }
46 |
47 | extension Role {
48 | struct FieldKeys {
49 | static var name: FieldKey { "name" }
50 | static var desc: FieldKey { "desc" }
51 | static var createdAt: FieldKey { "created_at" }
52 | static var updatedAt: FieldKey { "updated_at" }
53 | }
54 | }
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/RolePermission.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class RolePermission: Model {
12 | static let schema = "blog_role_permision"
13 |
14 | @ID(key: .id)
15 | var id: UUID?
16 |
17 | @Parent(key: FieldKeys.permissionId)
18 | var permission: Permission
19 |
20 | @Parent(key: FieldKeys.roleId)
21 | var role: Role
22 |
23 | init() { }
24 |
25 | init(id: UUID? = nil, permissionId: Permission.IDValue, roleId:Role.IDValue) {
26 | self.id = id
27 | self.$permission.id = permissionId
28 | self.$role.id = roleId
29 | }
30 | }
31 |
32 | extension RolePermission {
33 | struct FieldKeys {
34 | static var roleId: FieldKey { "role_id" }
35 | static var permissionId: FieldKey { "permission_id" }
36 | static var createdAt: FieldKey { "created_at" }
37 | static var updatedAt: FieldKey { "updated_at" }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/SideBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/9.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class SideBar: Model {
12 |
13 | static let schema = "blog_sidebar"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Field(key: FieldKeys.title)
19 | var title: String
20 |
21 | @Field(key: FieldKeys.displayType)
22 | var displayType: Int // HTML(1)|最新文章(2)|最热文章(3)|最近评论(4)
23 |
24 | @Field(key: FieldKeys.status)
25 | var status: Int // 展示(1)|隐藏(0)
26 |
27 | @Field(key: FieldKeys.content)
28 | var content: String
29 |
30 | @Parent(key: FieldKeys.ownerId)
31 | var owner: User
32 |
33 | @Timestamp(key: FieldKeys.createdAt, on: .create)
34 | var createdAt: Date?
35 |
36 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
37 | var updatedAt: Date?
38 |
39 | init() { }
40 |
41 | init(id: UUID? = nil) throws {
42 | self.id = id
43 |
44 | }
45 | }
46 |
47 | extension SideBar {
48 | struct FieldKeys {
49 | static var status: FieldKey { "status" }
50 | static var title: FieldKey { "title" }
51 | static var displayType: FieldKey { "display_type" }
52 | static var content: FieldKey { "content" }
53 | static var ownerId: FieldKey { "owner_id" }
54 | static var createdAt: FieldKey { "created_at" }
55 | static var updatedAt: FieldKey { "updated_at" }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/Tag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/9.
6 | //
7 |
8 |
9 | import Fluent
10 | import Vapor
11 |
12 | final class Tag: Model {
13 |
14 | static let schema = "blog_tag"
15 |
16 | @ID(key: .id)
17 | var id: UUID?
18 |
19 | @Field(key: FieldKeys.name)
20 | var name: String
21 |
22 | @Field(key: FieldKeys.status)
23 | var status: Int // 正常(1)|删除(0),
24 |
25 | @Parent(key: FieldKeys.ownerId)
26 | var owner: User
27 |
28 | @Timestamp(key: FieldKeys.createdAt, on: .create)
29 | var createdAt: Date?
30 |
31 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
32 | var updatedAt: Date?
33 |
34 | // 获取文章
35 | @Siblings(through: PostTag.self, from: \.$tag, to: \.$post)
36 | public var posts: [Post]
37 |
38 |
39 | init() {}
40 |
41 | init(id: UUID? = nil, name: String, ownerId: User.IDValue, status: Int = 1) {
42 | self.id = id
43 | self.name = name
44 | self.$owner.id = ownerId
45 | self.status = status
46 | }
47 | }
48 |
49 | extension Tag {
50 | struct FieldKeys {
51 | static var name: FieldKey { "name" }
52 | static var status: FieldKey { "status" }
53 | static var ownerId: FieldKey { "owner_id" }
54 | static var createdAt: FieldKey { "created_at" }
55 | static var updatedAt: FieldKey { "updated_at" }
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/10.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | final class User: Model {
12 |
13 | static let schema = "blog_user"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Field(key: FieldKeys.name)
19 | var name: String
20 |
21 | @Field(key: FieldKeys.email)
22 | var email: String
23 |
24 | @Field(key: FieldKeys.isEmailVerified)
25 | var isEmailVerified: Bool // 邮箱状态
26 |
27 | @Field(key: FieldKeys.status)
28 | var status: Int // 1正常用户;2其他非正常用户
29 |
30 | @Field(key: FieldKeys.isAdmin)
31 | var isAdmin: Bool // 是否是管理员
32 |
33 | // 邀请码 inviteCode
34 | @OptionalField(key: FieldKeys.inviteCode)
35 | var inviteCode: String?
36 |
37 | // 上级ID superiorId
38 | @OptionalField(key: FieldKeys.superiorId)
39 | var superiorId: UUID?
40 |
41 | // 注册时间
42 | @Timestamp(key: FieldKeys.createdAt, on: .create)
43 | var createdAt: Date?
44 |
45 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
46 | var updatedAt: Date?
47 |
48 | // 多个角色
49 | @Siblings(through: UserRole.self, from: \.$user, to: \.$role)
50 | var roles: [Role]
51 |
52 | init() {}
53 |
54 | init(
55 | id: UUID? = nil, name: String, email: String, status: Int = 1, isAdmin: Bool = false,
56 | isEmailVerified: Bool = false, inviteCode: String? = nil, superiorId: UUID? = nil
57 | ) {
58 | self.id = id
59 | self.name = name
60 | self.email = email
61 | self.status = status
62 | self.isAdmin = isAdmin
63 | self.isEmailVerified = isEmailVerified
64 | self.inviteCode = inviteCode
65 | self.superiorId = superiorId
66 | }
67 | }
68 |
69 | extension User {
70 | struct FieldKeys {
71 | static var name: FieldKey { "name" }
72 | static var email: FieldKey { "email" }
73 | static var isEmailVerified: FieldKey { "is_email_verified" }
74 | static var status: FieldKey { "status" }
75 | static var isAdmin: FieldKey { "is_admin" }
76 | static var inviteCode: FieldKey { "invite_code" }
77 | static var superiorId: FieldKey { "superior_id" }
78 | static var createdAt: FieldKey { "created_at" }
79 | static var updatedAt: FieldKey { "updated_at" }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/UserAuth.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2022/8/30.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | /// 用户认证状态
12 | final class UserAuth: Model {
13 | static let schema = "blog_user_auth"
14 |
15 | @ID(key: .id)
16 | var id: UUID?
17 |
18 | @Parent(key: FieldKeys.userId)
19 | var user: User
20 |
21 | @Field(key: FieldKeys.authType)
22 | var authType: String // 认证类型:email
23 |
24 | @Field(key: FieldKeys.identifier)
25 | var identifier: String // 标志 (手机号,邮箱,用户名或第三方应用的唯一标识)
26 |
27 | @Field(key: FieldKeys.credential)
28 | var credential: String // 密码凭证(站内的保存密码, 站外的不保存或保存 token)
29 |
30 | @Timestamp(key: FieldKeys.createdAt, on: .create)
31 | var createdAt: Date?
32 |
33 | @Timestamp(key: FieldKeys.updatedAt, on: .update)
34 | var updatedAt: Date?
35 |
36 | init() {}
37 |
38 | init(
39 | id: UUID? = nil,
40 | userId: UUID,
41 | authType: String = "email",
42 | identifier: String,
43 | credential: String
44 | ) {
45 | self.id = id
46 | self.$user.id = userId
47 | self.authType = authType
48 | self.identifier = identifier
49 | self.credential = credential
50 | }
51 | }
52 |
53 | extension UserAuth {
54 | struct FieldKeys {
55 | static var userId: FieldKey { "user_id" }
56 | static var authType: FieldKey { "auth_type" }
57 | static var identifier: FieldKey { "identifier" }
58 | static var credential: FieldKey { "credential" }
59 | static var createdAt: FieldKey { "created_at" }
60 | static var updatedAt: FieldKey { "updated_at" }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/App/Models/Entities/UserRole.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 | import Fluent
8 | import Vapor
9 |
10 | final class UserRole: Model {
11 | static let schema = "blog_user_role"
12 |
13 | @ID(key: .id)
14 | var id: UUID?
15 |
16 | @Parent(key: FieldKeys.userId)
17 | var user: User
18 |
19 | @Parent(key: FieldKeys.roleId)
20 | var role: Role
21 |
22 | init() { }
23 |
24 | init(id: UUID? = nil, userId: User.IDValue, roleId:Role.IDValue) {
25 | self.id = id
26 | self.$user.id = userId
27 | self.$role.id = roleId
28 | }
29 | }
30 |
31 | extension UserRole {
32 | struct FieldKeys {
33 | static var roleId: FieldKey { "role_id" }
34 | static var userId: FieldKey { "user_id" }
35 | static var createdAt: FieldKey { "created_at" }
36 | static var updatedAt: FieldKey { "updated_at" }
37 | }
38 | }
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Sources/App/Models/MODEL/EmailContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/12.
6 | //
7 |
8 | struct EmailContent: Codable {
9 | let to: String
10 | let message: String
11 | let subject: String
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/App/Models/MODEL/RefreshToken.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/12.
6 | //
7 |
8 | import JWT
9 | import Vapor
10 |
11 | struct RefreshToken: JWTPayload {
12 | // Constants
13 | var expirationTime: TimeInterval = 60 * 60 * 24 * 30
14 |
15 | // Token Data
16 | var expiration: ExpirationClaim
17 | var userId: UUID
18 |
19 | init(userId: UUID) {
20 | self.userId = userId
21 | self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
22 | }
23 |
24 | init(user: User) throws {
25 | self.userId = try user.requireID()
26 | self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
27 | }
28 |
29 | func verify(using signer: JWTSigner) throws {
30 | try expiration.verifyNotExpired()
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/Sources/App/Models/MODEL/SessionToken.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/12.
6 | //
7 |
8 | import JWT
9 | import Vapor
10 |
11 | struct SessionToken: Content, Authenticatable, JWTPayload {
12 |
13 | // Constants
14 | var expirationTime: TimeInterval = 60 * 60
15 |
16 | // Token Data
17 | var expiration: ExpirationClaim
18 | var userId: UUID
19 |
20 | init(userId: UUID) {
21 | self.userId = userId
22 | self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
23 | }
24 |
25 | init(user: User) throws {
26 | self.userId = try user.requireID()
27 | self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
28 | }
29 |
30 | func verify(using signer: JWTSigner) throws {
31 | try expiration.verifyNotExpired()
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/Sources/App/Models/MODEL/WebSessionAuthenticator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/3.
6 | //
7 |
8 | import Vapor
9 | // session 登录和验证
10 |
11 | // 可认证的会话
12 | extension User: SessionAuthenticatable {
13 | typealias SessionID = User.IDValue
14 | var sessionID: SessionID { self.id! }
15 | }
16 |
17 | // 会话认证器
18 | struct WebSessionAuthenticator: AsyncSessionAuthenticator {
19 | typealias User = App.User
20 |
21 | func authenticate(sessionID: User.SessionID, for request: Request) async throws {
22 | let user = try await User.find(sessionID, on: request.db)
23 | if let user = user {
24 | request.auth.login(user)
25 | }
26 | }
27 | }
28 |
29 | // 登录
30 | struct WebCredentialsAuthenticator: AsyncCredentialsAuthenticator {
31 |
32 | typealias Credentials = InLogin
33 |
34 | func authenticate(credentials: Credentials, for request: Request) async throws {
35 | let (isValid, userAuth) = try await request.services.auth.isValidPwd(email: credentials.email, pwd: credentials.password)
36 | if isValid {
37 | let user = try await userAuth.$user.get(on: request.db)
38 | request.auth.login(user)
39 | }
40 | }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/CategoryRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | /// Category 增删改查
12 | protocol CategoryRepository: Repository {
13 | func all(ownerId: User.IDValue?) async throws -> [Category.Public]
14 | func add(param: InCategory, ownerId: User.IDValue) async throws -> Category
15 | func page(ownerId: User.IDValue) async throws -> Page
16 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
17 | func update(param: InUpdateCategory, ownerId: User.IDValue) async throws
18 | }
19 |
20 | extension RepositoryFactory {
21 | var category: CategoryRepository {
22 | guard let result = resolve(CategoryRepository.self) as? CategoryRepository else {
23 | fatalError("Category repository is not configured")
24 | }
25 | return result
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/CommentRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/3.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | /// Category 增删改查
12 | protocol CommentRepository: Repository {
13 | func add(param: InComment, fromUserId: User.IDValue) async throws -> Comment
14 | func all(topicId: UUID, topicType: Int) async throws -> [Comment.Public]
15 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
16 | }
17 |
18 | extension RepositoryFactory {
19 | var comment: CommentRepository {
20 | guard let result = resolve(CommentRepository.self) as? CommentRepository else {
21 | fatalError("Comment repository is not configured")
22 | }
23 | return result
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/CategoryRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CategoryRepositoryImpl: CategoryRepository {
12 |
13 | var req: Request
14 |
15 | init(_ req: Request) {
16 | self.req = req
17 | }
18 |
19 | func add(param: InCategory, ownerId: User.IDValue) async throws -> Category {
20 | let category = Category(name: param.name, ownerId: ownerId, isNav: param.isNav)
21 | try await category.create(on: req.db)
22 | return category
23 | }
24 |
25 | func page(ownerId: User.IDValue) async throws -> FluentKit.Page {
26 | return try await Category.query(on: req.db)
27 | .filter(\.$status == 1)
28 | .with(\.$owner)
29 | .sort(\.$createdAt, .descending)
30 | .paginate(for: req)
31 | .map({$0.asPublic()})
32 | }
33 |
34 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
35 | try await Category.query(on: req.db)
36 | .set(\.$status, to: 0)
37 | .group(.and) {group in
38 | group.filter(\.$id ~~ ids.ids).filter(\.$owner.$id == ownerId)
39 | }
40 | .update()
41 | }
42 |
43 | func update(param: InUpdateCategory, ownerId: User.IDValue) async throws {
44 | try await Category.query(on: req.db)
45 | .set(\.$name, to: param.name)
46 | .set(\.$isNav, to: param.isNav)
47 | .filter(\.$id == param.id)
48 | .update()
49 | }
50 |
51 | func all(ownerId: User.IDValue? = nil) async throws -> [Category.Public] {
52 | try await Category.query(on: req.db)
53 | .filter(\.$status == 1)
54 | .sort(\.$createdAt, .descending)
55 | .all()
56 | .map({$0.asPublic()})
57 | }
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/CommentRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/3.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct CommentRepositoryImpl: CommentRepository {
12 | var req: Request
13 |
14 | init(_ req: Request) {
15 | self.req = req
16 | }
17 |
18 | func add(param: InComment, fromUserId: User.IDValue) async throws -> Comment {
19 | let comment = Comment(content: param.content, userId: fromUserId, topicId: param.topicId, topicType: param.topicType)
20 | try await comment.create(on: req.db)
21 | return comment
22 | }
23 |
24 | func all(topicId: UUID, topicType: Int) async throws -> [Comment.Public] {
25 | let comments = try await Comment.query(on: req.db)
26 | .filter(\.$topicId == topicId)
27 | .filter(\.$topicType == topicType)
28 | .with(\.$fromUser)
29 | .with(\.$replys, { reply in
30 | reply.with(\.$fromUser)
31 | reply.with(\.$toUser)
32 | })
33 | .sort(\.$createdAt, .descending)
34 | .all()
35 | return comments.map({$0.asPublic()})
36 | }
37 |
38 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
39 | try await Comment.query(on: req.db).filter(\.$id ~~ ids.ids).delete()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/InviteRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/23.
6 | //
7 |
8 |
9 | import Vapor
10 | import Fluent
11 |
12 | struct InviteRepositoryImpl: InviteRepository {
13 |
14 | var req: Request
15 |
16 | init(_ req: Request) {
17 | self.req = req
18 | }
19 |
20 | func generateInviteCode() async throws -> String {
21 | let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
22 | var inviteCode = String((0..<6).map{ _ in letters.randomElement()! })
23 |
24 | while try await checkIfExists(inviteCode: inviteCode) {
25 | inviteCode = String((0..<6).map{ _ in letters.randomElement()! })
26 | }
27 |
28 | let invite = Invite(code: inviteCode)
29 | try await invite.save(on: req.db)
30 | return inviteCode
31 | }
32 |
33 | private func checkIfExists(inviteCode: String) async throws -> Bool {
34 | return try await Invite.query(on: req.db).filter(\.$code == inviteCode).first().map({_ in true }) ?? false
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/LinkRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct LinkRepositoryImpl: LinkRepository {
12 | var req: Request
13 |
14 | init(_ req: Request) {
15 | self.req = req
16 | }
17 |
18 | func add(param: InLink, ownerId: User.IDValue) async throws -> Link {
19 | let link = Link(title: param.title, href: param.href, weight: param.weight.castInt(), ownerId: ownerId)
20 | try await link.create(on: req.db)
21 | return link
22 | }
23 |
24 | func page(ownerId: User.IDValue) async throws -> FluentKit.Page {
25 | return try await Link.query(on: req.db)
26 | .with(\.$owner)
27 | .filter(\.$status == 1)
28 | .sort(\.$createdAt, .descending)
29 | .paginate(for:req)
30 | .map({$0.asPublic()})
31 | }
32 |
33 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
34 | try await Link.query(on: req.db)
35 | .set(\.$status, to: 0)
36 | .group(.and) {group in
37 | group.filter(\.$id ~~ ids.ids).filter(\.$owner.$id == ownerId)
38 | }
39 | .update()
40 | }
41 |
42 | func update(param: InUpdateLink, ownerId: User.IDValue) async throws {
43 | try await Link.query(on: req.db)
44 | .set(\.$title, to: param.title)
45 | .set(\.$weight, to: param.weight.castInt())
46 | .set(\.$href, to: param.href)
47 | .filter(\.$id == param.id)
48 | .update()
49 | }
50 |
51 | func all(ownerId: User.IDValue?) async throws -> [Link.Public] {
52 | return try await Link.query(on: req.db)
53 | .filter(\.$status == 1)
54 | .sort(\.$weight, .descending)
55 | .all()
56 | .map({$0.asPublic()})
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/MenuRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 | import Fluent
11 |
12 | struct MenuRepositoryImpl: MenuRepository {
13 | var req: Request
14 |
15 | init(_ req: Request) {
16 | self.req = req
17 | }
18 |
19 | func add(param: InMenu, ownerId: User.IDValue) async throws -> Menu {
20 | let item = Menu(name: param.name, url: param.url, weight: param.weight.castInt(), parentId: param.parentId)
21 | try await item.create(on: req.db)
22 | return item
23 | }
24 |
25 | func page(ownerId: User.IDValue?) async throws -> FluentKit.Page {
26 | return try await Menu.query(on: req.db)
27 | .filter(\.$status == 1)
28 | .sort(\.$weight, .descending)
29 | .sort(\.$createdAt, .descending)
30 | .paginate(for:req)
31 | .map({$0.asPublic()})
32 | }
33 |
34 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
35 | try await Menu.query(on: req.db)
36 | .set(\.$status, to: 0)
37 | .group(.and) { group in
38 | group.filter(\.$id ~~ ids.ids)
39 | }
40 | .update()
41 | }
42 |
43 | func update(param: InUpdateMenu, ownerId: User.IDValue) async throws {
44 | try await Menu.query(on: req.db)
45 | .set(\.$name, to: param.name)
46 | .set(\.$weight, to: param.weight.castInt())
47 | .set(\.$url, to: param.url)
48 | .filter(\.$id == param.id)
49 | .update()
50 | }
51 |
52 | func all(ownerId: User.IDValue?) async throws -> [Menu.Public] {
53 | return try await Menu.query(on: req.db)
54 | .filter(\.$status == 1)
55 | .sort(\.$weight, .descending)
56 | .all()
57 | .map({$0.asPublic()})
58 | }
59 |
60 | func all(permissions: [Permission]) async throws -> [Menu.Public] {
61 | let permissionIds = permissions.map { $0.id! }
62 | return try await Menu.query(on: req.db)
63 | .join(siblings: \.$permissions)
64 | .filter(Permission.self, \Permission.$id ~~ permissionIds)
65 | .filter(\.$status == 1)
66 | .with(\.$permissions)
67 | .sort(\.$weight, .descending)
68 | .all()
69 | .map({$0.asPublic()})
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/PermissionRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 | import Fluent
11 |
12 | struct PermissionRepositoryImpl: PermissionRepository {
13 | var req: Request
14 |
15 | init(_ req: Request) {
16 | self.req = req
17 | }
18 |
19 | func add(param: InPermission, ownerId: User.IDValue) async throws -> Permission {
20 | let item = Permission(name: param.name, desc: param.desc, code: param.code)
21 | try await item.create(on: req.db)
22 | return item
23 | }
24 |
25 | func page(ownerId: User.IDValue?) async throws -> FluentKit.Page {
26 | return try await Permission.query(on: req.db)
27 | .with(\.$menus)
28 | .sort(\.$createdAt, .descending)
29 | .paginate(for:req)
30 | .map({$0.asPublic()})
31 | }
32 |
33 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
34 | try await Permission.query(on: req.db)
35 | .filter(\.$id ~~ ids.ids)
36 | .delete(force: true)
37 | }
38 |
39 | func update(param: InUpdatePermission, ownerId: User.IDValue) async throws {
40 | try await req.db.transaction { db in
41 | try await Permission.query(on: db)
42 | .set(\.$name, to: param.name)
43 | .set(\.$desc, to: param.desc)
44 | .set(\.$code, to: param.code)
45 | .filter(\.$id == param.id)
46 | .update()
47 |
48 | guard let ret = try await Permission.query(on: db)
49 | .with(\.$menus)
50 | .filter(\.$id == param.id)
51 | .first()
52 | else {
53 | throw ApiError(code: .roleNotExist)
54 | }
55 | try await ret.$menus.detachAll(on: db)
56 | let menus = try await Menu.query(on: db).filter(\.$id ~~ param.menuIds).all()
57 | try await ret.$menus.attach(menus, on: db)
58 | }
59 | }
60 |
61 | func all(ownerId: User.IDValue?) async throws -> [Permission.Public] {
62 | return try await Permission.query(on: req.db)
63 | .sort(\.$createdAt, .descending)
64 | .all()
65 | .map({$0.asPublic()})
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/PostRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct PostRepositoryImpl: PostRepository {
12 |
13 | var req: Request
14 |
15 | init(_ req: Request) {
16 | self.req = req
17 | }
18 |
19 | func add(param: InPost, ownerId: User.IDValue) async throws -> Post {
20 | let post = Post(title: param.title, ownerId: ownerId, content: param.content, desc: param.desc, categoryId: param.categoryId)
21 | let tags = try await Tag.query(on: req.db).filter(\.$id ~~ param.tagIds).all()
22 | try await req.db.transaction { db in
23 | try await post.create(on: db)
24 | try await post.$tags.attach(tags, on: db)
25 | }
26 | return post
27 | }
28 |
29 | func page(ownerId: User.IDValue?, inIndex: InSearchPost?) async throws -> FluentKit.Page {
30 |
31 | let pageQuery = Post.query(on: req.db)
32 | .filter(\.$status == 1)
33 |
34 | if let inIndex = inIndex {
35 | if let cateId = inIndex.categoryId {
36 | pageQuery.filter(\.$category.$id == cateId)
37 | }
38 | if let tagId = inIndex.tagId {
39 | pageQuery.join(siblings: \.$tags).filter(Tag.self, \Tag.$id == tagId)
40 | }
41 |
42 | if let searchKey = inIndex.searchKey {
43 | pageQuery.filter(\.$title ~~ searchKey)
44 | }
45 | }
46 |
47 | return try await pageQuery
48 | .sort(\.$createdAt, .descending)
49 | .with(\.$tags)
50 | .with(\.$category)
51 | .with(\.$owner)
52 | .paginate(for: req)
53 | .map({ $0.asPublic() })
54 | }
55 |
56 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
57 | try await Post.query(on: req.db)
58 | .set(\.$status, to: 0)
59 | .group(.and) {group in
60 | group.filter(\.$id ~~ ids.ids).filter(\.$owner.$id == ownerId)
61 | }
62 | .update()
63 | }
64 |
65 | func update(param: InUpdatePost, ownerId: User.IDValue) async throws {
66 | try await req.db.transaction { db in
67 | try await Post.query(on: db)
68 | .set(\.$title, to: param.title)
69 | .set(\.$desc, to: param.desc)
70 | .set(\.$content, to: param.content)
71 | .set(\.$category.$id, to: param.categoryId)
72 | .filter(\.$id == param.id)
73 | .update()
74 |
75 | guard let ret = try await Post.query(on: db)
76 | .with(\.$tags)
77 | .filter(\.$id == param.id)
78 | .first()
79 | else {
80 | throw ApiError(code: .postNotExist)
81 | }
82 | try await ret.$tags.detachAll(on: db)
83 | let newTags = try await Tag.query(on: db).filter(\.$id ~~ param.tagIds).all()
84 | try await ret.$tags.attach(newTags, on: db)
85 | }
86 | }
87 |
88 | func get(id: Post.IDValue, ownerId: User.IDValue?) async throws -> Post.Public? {
89 | let post = try await Post.query(on: req.db)
90 | .filter(\.$id == id)
91 | .with(\.$owner)
92 | .with(\.$category)
93 | .with(\.$tags)
94 | .first()
95 | return post?.asPublic()
96 | }
97 |
98 | func newer(limit: Int) async throws -> [Post.Public] {
99 | let posts = try await Post.query(on: req.db)
100 | .filter(\.$status == 1)
101 | .sort(\.$createdAt, .descending)
102 | .limit(limit)
103 | .all()
104 | return posts.map({$0.asPublic()})
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/ReplyRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/3.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | struct ReplyRepositoryImpl: ReplyRepository {
12 | var req: Request
13 |
14 | init(_ req: Request) {
15 | self.req = req
16 | }
17 |
18 | func add(param: InReply, fromUserId: User.IDValue) async throws -> Reply {
19 | let reply = Reply(commentId: param.commentId,
20 | content: param.content,
21 | userId: fromUserId,
22 | toUid: param.toUserId,
23 | targetId: param.targetId,
24 | targetType: param.targetType.castInt())
25 | try await reply.create(on: req.db)
26 | return reply
27 | }
28 |
29 | func all(commentId: Comment.IDValue) async throws -> [Reply.Public] {
30 | let replys = try await Reply.query(on: req.db)
31 | .filter(\.$comment.$id == commentId)
32 | .with(\.$toUser)
33 | .with(\.$fromUser)
34 | .with(\.$comment)
35 | .all()
36 | return replys.map({$0.asPublic()})
37 | }
38 |
39 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
40 | try await Reply.query(on: req.db).filter(\.$id ~~ ids.ids).delete()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/RoleRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 | import Fluent
11 |
12 | struct RoleRepositoryImpl: RoleRepository {
13 |
14 | var req: Request
15 |
16 | init(_ req: Request) {
17 | self.req = req
18 | }
19 |
20 | func add(param: InRole, ownerId: User.IDValue) async throws -> Role {
21 | let item = Role(name: param.name)
22 | try await item.create(on: req.db)
23 | return item
24 | }
25 |
26 | func page(ownerId: User.IDValue?) async throws -> Page {
27 | return try await Role.query(on: req.db)
28 | .with(\.$permissions)
29 | .sort(\.$createdAt, .descending)
30 | .paginate(for:req)
31 | .map({$0.asPublic()})
32 | }
33 |
34 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
35 | try await Role.query(on: req.db)
36 | .filter(\.$id ~~ ids.ids)
37 | .delete()
38 | }
39 |
40 | func update(param: InUpdateRole, ownerId: Role.IDValue) async throws {
41 | try await req.db.transaction { db in
42 | try await Role.query(on: db)
43 | .set(\.$name, to: param.name)
44 | .filter(\.$id == param.id)
45 | .update()
46 |
47 | guard let ret = try await Role.query(on: db)
48 | .with(\.$permissions)
49 | .filter(\.$id == param.id)
50 | .first()
51 | else {
52 | throw ApiError(code: .roleNotExist)
53 | }
54 | try await ret.$permissions.detachAll(on: db)
55 | let perms = try await Permission.query(on: db).filter(\.$id ~~ param.permissionIds).all()
56 | try await ret.$permissions.attach(perms, on: db)
57 | }
58 | }
59 |
60 | func all(ownerId: User.IDValue?) async throws -> [Role.Public] {
61 | return try await Role.query(on: req.db)
62 | .all()
63 | .map({$0.asPublic()})
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/TagRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/9.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 | import FluentSQL
11 |
12 | struct TagRepositoryImpl: TagRepository {
13 | var req: Request
14 |
15 | init(_ req: Request) {
16 | self.req = req
17 | }
18 |
19 | func add(param: InTag, ownerId: User.IDValue) async throws -> Tag {
20 | let tag = Tag(name: param.name, ownerId: ownerId)
21 | try await tag.create(on: req.db)
22 | return tag
23 | }
24 |
25 | func page(ownerId: User.IDValue?) async throws -> Page {
26 | return try await Tag.query(on: req.db)
27 | .filter(\.$status == 1)
28 | .with(\.$owner)
29 | .sort(\.$createdAt, .descending)
30 | .paginate(for:req)
31 | .map({$0.asPublic()})
32 | }
33 |
34 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws {
35 | try await Tag.query(on: req.db)
36 | .set(\.$status, to: 0)
37 | .group(.and) {group in
38 | group.filter(\.$id ~~ ids.ids).filter(\.$owner.$id == ownerId)
39 | }
40 | .update()
41 | }
42 |
43 | func update(param: InUpdateTag, ownerId: User.IDValue) async throws {
44 | try await Tag.query(on: req.db)
45 | .set(\.$name, to: param.name)
46 | .filter(\.$id == param.id)
47 | .update()
48 | }
49 |
50 | func all(ownerId: User.IDValue?) async throws -> [Tag.Public] {
51 | try await Tag.query(on: req.db)
52 | .filter(\.$status == 1)
53 | .sort(\.$createdAt, .descending)
54 | .all()
55 | .map({$0.asPublic()})
56 | }
57 |
58 | func hot(limit: Int) async throws -> [Tag.Public] {
59 | if let sql = req.db as? SQLDatabase {
60 | // 底层数据库驱动程序是 SQL
61 | let query = """
62 | select tags.*
63 | from \(Tag.schema) tags
64 | left join \(PostTag.schema) pivot on tags.id = pivot.\(PostTag.FieldKeys.tagId)
65 | group by tags.id
66 | having count(pivot.\(PostTag.FieldKeys.tagId)) > 0
67 | order by count(pivot.\(PostTag.FieldKeys.tagId)) desc
68 | limit \(limit)
69 | """
70 | return try await sql.raw(.init(query)).all(decoding: Tag.self).map({$0.asPublic()})
71 | } else {
72 | throw ApiError(code: .postNotExist)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Impl/UserRepositoryImpl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 | import Foundation
11 |
12 | public struct UserRepositoryImpl: UserRepository {
13 | var req: Request
14 |
15 | public init(_ req: Request) {
16 | self.req = req
17 | }
18 |
19 | func getUser() async throws -> User {
20 | let payload = try req.auth.require(SessionToken.self)
21 | let user = try await User.find(payload.userId, on: req.db)
22 | guard let user = user else {
23 | throw ApiError(code: .userNotExist)
24 | }
25 | return user
26 | }
27 |
28 | func page(ownerId: User.IDValue?) async throws -> Page {
29 | return try await User.query(on: req.db)
30 | .with(\.$roles)
31 | .filter(\.$status == 1)
32 | .sort(\.$createdAt, .descending)
33 | .paginate(for:req)
34 | .map({$0.asPublic()})
35 | }
36 |
37 | func update(param: InUpdateUser, ownerId: User.IDValue?) async throws {
38 | try await req.db.transaction { db in
39 | // try await User.query(on: db)
40 | // .set(\.$title, to: param.title)
41 | // .filter(\.$id == param.id)
42 | // .update()
43 | guard let ret = try await User.query(on: db)
44 | .with(\.$roles)
45 | .filter(\.$id == param.id)
46 | .first()
47 | else {
48 | throw ApiError(code: .userNotExist)
49 | }
50 |
51 | try await ret.$roles.detachAll(on: db)
52 | let roles = try await Role.query(on: db).filter(\.$id ~~ param.roleIds).all()
53 | try await ret.$roles.attach(roles, on: db)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/InviteRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/23.
6 | //
7 | import Foundation
8 |
9 | protocol InviteRepository: Repository {
10 | func generateInviteCode() async throws -> String
11 | }
12 |
13 | extension RepositoryFactory {
14 | var invite: InviteRepository {
15 | guard let result = resolve(InviteRepository.self) as? InviteRepository else {
16 | fatalError("Invite repository is not configured")
17 | }
18 | return result
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/LinkRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | /// Link增删改查
12 | protocol LinkRepository: Repository {
13 | func add(param: InLink, ownerId: User.IDValue) async throws -> Link
14 | func page(ownerId: User.IDValue) async throws -> Page
15 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
16 | func update(param: InUpdateLink, ownerId: User.IDValue) async throws
17 | func all(ownerId: User.IDValue?) async throws -> [Link.Public]
18 | }
19 |
20 | extension RepositoryFactory {
21 | var link: LinkRepository {
22 | guard let result = resolve(LinkRepository.self) as? LinkRepository else {
23 | fatalError("Link repository is not configured")
24 | }
25 | return result
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/MenuRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | /// Menu增删改查
12 | protocol MenuRepository: Repository {
13 | func all(permissions: [Permission]) async throws -> [Menu.Public]
14 | func all(ownerId: User.IDValue?) async throws -> [Menu.Public]
15 | func add(param: InMenu, ownerId: User.IDValue) async throws -> Menu
16 | func page(ownerId: User.IDValue?) async throws -> Page
17 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
18 | func update(param: InUpdateMenu, ownerId: User.IDValue) async throws
19 | }
20 |
21 | extension RepositoryFactory {
22 | var menu: MenuRepository {
23 | guard let result = resolve(MenuRepository.self) as? MenuRepository else {
24 | fatalError("Menu repository is not configured")
25 | }
26 | return result
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/PermissionRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | /// tag 增删改查
12 | protocol PermissionRepository: Repository {
13 | func all(ownerId: User.IDValue?) async throws -> [Permission.Public]
14 | func add(param: InPermission, ownerId: User.IDValue) async throws -> Permission
15 | func page(ownerId: User.IDValue?) async throws -> Page
16 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
17 | func update(param: InUpdatePermission, ownerId: User.IDValue) async throws
18 | }
19 |
20 | extension RepositoryFactory {
21 | var permission: PermissionRepository {
22 | guard let result = resolve(PermissionRepository.self) as? PermissionRepository else {
23 | fatalError("Permission repository is not configured")
24 | }
25 | return result
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/PostRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/20.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | /// Link增删改查
12 | protocol PostRepository: Repository {
13 | func add(param: InPost, ownerId: User.IDValue) async throws -> Post
14 | func page(ownerId: User.IDValue?, inIndex: InSearchPost?) async throws -> Page
15 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
16 | func update(param: InUpdatePost, ownerId: User.IDValue) async throws
17 | func get(id: Post.IDValue, ownerId: User.IDValue?) async throws -> Post.Public?
18 | func newer(limit: Int) async throws -> [Post.Public]
19 | }
20 |
21 | extension PostRepository {
22 | func page(ownerId: User.IDValue?) async throws -> Page {
23 | return try await page(ownerId: ownerId, inIndex: nil)
24 | }
25 | }
26 |
27 | extension RepositoryFactory {
28 | var post: PostRepository {
29 | guard let result = resolve(PostRepository.self) as? PostRepository else {
30 | fatalError("Post repository is not configured")
31 | }
32 | return result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/ReplyRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/8/3.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 | import Fluent
11 |
12 | /// Category 增删改查
13 | protocol ReplyRepository: Repository {
14 | func add(param: InReply, fromUserId: User.IDValue) async throws -> Reply
15 | func all(commentId: Comment.IDValue) async throws -> [Reply.Public]
16 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
17 | }
18 |
19 | extension RepositoryFactory {
20 | var reply: ReplyRepository {
21 | guard let result = resolve(ReplyRepository.self) as? ReplyRepository else {
22 | fatalError("Reply repository is not configured")
23 | }
24 | return result
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/Repository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | // 主要目标是从数据库中高效地访问(查询)数据,并向服务层提供服务。
11 | public protocol Repository {
12 | init(_ req: Request)
13 | }
14 |
15 | public struct RepositoryId: Hashable, Equatable, CustomStringConvertible {
16 | public static func == (lhs: RepositoryId, rhs: RepositoryId) -> Bool {
17 | return lhs.type == rhs.type
18 | }
19 |
20 | public func hash(into hasher: inout Hasher) {
21 | hasher.combine(ObjectIdentifier(type))
22 | }
23 |
24 | internal let type: Any.Type
25 |
26 | public var description: String {
27 | return "\(type)"
28 | }
29 |
30 | init(_ type: Any.Type) {
31 | self.type = type
32 | }
33 | }
34 |
35 | public typealias RepositoryBuilder = (Request) -> Repository
36 |
37 | public final class RepositoryRegistry {
38 |
39 | private let app: Application
40 | private var builders: [RepositoryId: RepositoryBuilder]
41 |
42 | fileprivate init(_ app: Application) {
43 | self.app = app
44 | self.builders = [:]
45 | }
46 |
47 | fileprivate func builder(_ req: Request) -> RepositoryFactory {
48 | .init(req, self)
49 | }
50 |
51 | fileprivate func resolve(_ type: Any.Type, _ req: Request) -> Repository {
52 | let id = RepositoryId(type)
53 | guard let builder = builders[id] else {
54 | fatalError("Repository for id `\(id)` is not configured.")
55 | }
56 | return builder(req)
57 | }
58 |
59 | // TODO: 如何限制 type 继承自 Repository
60 | public func register(_ type: Any.Type, _ builder: @escaping RepositoryBuilder) {
61 | builders[RepositoryId(type)] = builder
62 | }
63 | }
64 |
65 | public struct RepositoryFactory {
66 | private var registry: RepositoryRegistry
67 | private var req: Request
68 |
69 | fileprivate init(_ req: Request, _ registry: RepositoryRegistry) {
70 | self.req = req
71 | self.registry = registry
72 | }
73 |
74 | public func resolve(_ type: Any.Type) -> Repository {
75 | registry.resolve(type, req)
76 | }
77 | }
78 |
79 | public extension Application {
80 | private struct Key: StorageKey {
81 | typealias Value = RepositoryRegistry
82 | }
83 |
84 | var repositories: RepositoryRegistry {
85 | if storage[Key.self] == nil {
86 | storage[Key.self] = .init(self)
87 | }
88 | return storage[Key.self]!
89 | }
90 | }
91 |
92 | public extension Request {
93 | var repositories: RepositoryFactory {
94 | application.repositories.builder(self)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/RoleRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/28.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | /// tag 增删改查
12 | protocol RoleRepository: Repository {
13 | func all(ownerId: User.IDValue?) async throws -> [Role.Public]
14 | func add(param: InRole, ownerId: User.IDValue) async throws -> Role
15 | func page(ownerId: User.IDValue?) async throws -> Page
16 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
17 | func update(param: InUpdateRole, ownerId: User.IDValue) async throws
18 | }
19 |
20 | extension RepositoryFactory {
21 | var role: RoleRepository {
22 | guard let result = resolve(RoleRepository.self) as? RoleRepository else {
23 | fatalError("Role repository is not configured")
24 | }
25 | return result
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/TagReposigory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/9.
6 | //
7 |
8 | import Vapor
9 | import Fluent
10 |
11 | /// tag 增删改查
12 | protocol TagRepository: Repository {
13 | func all(ownerId: User.IDValue?) async throws -> [Tag.Public]
14 | func add(param: InTag, ownerId: User.IDValue) async throws -> Tag
15 | func delete(ids: InDeleteIds, ownerId: User.IDValue) async throws
16 | func page(ownerId: User.IDValue?) async throws -> Page
17 | func update(param: InUpdateTag, ownerId: User.IDValue) async throws
18 | func hot(limit: Int) async throws -> [Tag.Public]
19 | }
20 |
21 | extension RepositoryFactory {
22 | var tag: TagRepository {
23 | guard let result = resolve(TagRepository.self) as? TagRepository else {
24 | fatalError("Tag repository is not configured")
25 | }
26 | return result
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/Sources/App/Repositorys/UserRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Fluent
9 | import Vapor
10 |
11 | protocol UserRepository: Repository {
12 | func getUser() async throws -> User
13 | // 用户列表
14 | func page(ownerId: User.IDValue?) async throws -> Page
15 | // 用户更新
16 | func update(param: InUpdateUser, ownerId: User.IDValue?) async throws
17 | }
18 |
19 | extension RepositoryFactory {
20 | var user: UserRepository {
21 | guard let result = resolve(UserRepository.self) as? UserRepository else {
22 | fatalError("User repository is not configured")
23 | }
24 | return result
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/App/Services/AuthService.swift:
--------------------------------------------------------------------------------
1 | import Fluent
2 | import Vapor
3 |
4 | protocol AuthService: Service {
5 | // 登录
6 | func login() async throws -> OutJson
7 | // 注册
8 | func register() async throws -> OutJson
9 | // 删除账号
10 | func deleteUser() async throws -> OutJson
11 | // 重置密码
12 | func resetpwd() async throws -> OutJson
13 | // 更新密码
14 | func updatepwd() async throws -> OutJson
15 | // 刷新登录token
16 | func refreshAccessToken() async throws -> OutJson
17 | // 获取注册验证码
18 | func getRegisterCode() async throws -> OutJson
19 | // 获取重置密码验证码
20 | func getResetPwdCode() async throws -> OutJson
21 | // 判断密码是否有效
22 | func isValidPwd(email: String, pwd: String) async throws -> (Bool, UserAuth)
23 | // 注册系统管理员
24 | func registerSystemAdmin() async throws -> User
25 | }
26 |
27 | extension ServiceFactory {
28 | var auth: AuthService {
29 | guard let result = resolve(AuthService.self) as? AuthService else {
30 | fatalError("AuthService is not configured")
31 | }
32 | return result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/App/Services/BackendService.swift:
--------------------------------------------------------------------------------
1 | import Fluent
2 | import Vapor
3 |
4 | protocol BackendService: Service {
5 | // 判断用户是否具有权限
6 | func checkPermission(by code:String, user: User) async throws -> Bool
7 |
8 | // 获取用户的菜单
9 | func menus(user: User) async throws -> [Menu.Public]
10 | }
11 |
12 | extension ServiceFactory {
13 | var backend: BackendService {
14 | guard let result = resolve(BackendService.self) as? BackendService else {
15 | fatalError("BackendService is not configured")
16 | }
17 | return result
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/Sources/App/Services/Impl/BackendServiceImpl.swift:
--------------------------------------------------------------------------------
1 | import Fluent
2 | import SMTP
3 | import Vapor
4 |
5 | public struct BackendServiceImpl: BackendService {
6 | var req: Request
7 |
8 | public init(_ req: Request) {
9 | self.req = req
10 | }
11 |
12 | func checkPermission(by code: String, user: User) async throws -> Bool {
13 | let allPerms = try await userPermission(user: user)
14 | return allPerms.contains(where: {$0.code == code})
15 | }
16 |
17 | func menus(user: User) async throws -> [Menu.Public] {
18 |
19 | let allPerms = try await userPermission(user: user)
20 | var menus:[Menu.Public] = []
21 | if (user.isAdmin) {
22 | // 系统管理员
23 | menus = try await req.repositories.menu.all(ownerId: user.id)
24 | } else {
25 | menus = try await req.repositories.menu.all(permissions: allPerms)
26 | }
27 | return buildMenuTree(menus: menus, parentId: nil)
28 | }
29 |
30 | // private
31 | private func buildMenuTree(menus: [Menu.Public], parentId: UUID?) -> [Menu.Public] {
32 | var tree: [Menu.Public] = []
33 | for menu in menus {
34 | if menu.parentId == parentId {
35 | let subMenu = menu.mergeWith(children: buildMenuTree(menus: menus, parentId: menu.id))
36 | tree.append(subMenu)
37 | }
38 | }
39 | return tree
40 | }
41 |
42 | // 获取用户所有权限
43 | private func userPermission(user: User) async throws -> [Permission] {
44 | var roles: [Role] = []
45 | if user.$roles.value == nil {
46 | roles = try await user.$roles.query(on: req.db).all()
47 | } else {
48 | roles = user.$roles.value!
49 | }
50 |
51 | // 并发执行
52 | var allPerms:[Permission] = []
53 | try await withThrowingTaskGroup(of: [Permission].self) { group in
54 | for role in roles {
55 | group.addTask { try await role.$permissions.query(on: req.db).all() }
56 | }
57 | for try await ret in group {
58 | allPerms.append(contentsOf: ret)
59 | }
60 | }
61 | return allPerms
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/App/Services/Service.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/6/13.
6 | //
7 |
8 | import Vapor
9 |
10 | // 服务层,跟 Controller 打交道
11 | public protocol Service {
12 | init(_ req: Request)
13 | }
14 |
15 | public struct ServiceId: Hashable, Equatable, CustomStringConvertible {
16 | public static func == (lhs: ServiceId, rhs: ServiceId) -> Bool {
17 | return lhs.type == rhs.type
18 | }
19 |
20 | public func hash(into hasher: inout Hasher) {
21 | hasher.combine(ObjectIdentifier(type))
22 | }
23 |
24 | internal let type: Any.Type
25 |
26 | public var description: String {
27 | return "\(type)"
28 | }
29 |
30 | init(_ type: Any.Type) {
31 | self.type = type
32 | }
33 | }
34 |
35 | public typealias ServiceBuilder = (Request) -> Service
36 |
37 | public final class ServiceRegistry {
38 |
39 | private let app: Application
40 | private var builders: [ServiceId: ServiceBuilder]
41 |
42 | fileprivate init(_ app: Application) {
43 | self.app = app
44 | self.builders = [:]
45 | }
46 |
47 | fileprivate func builder(_ req: Request) -> ServiceFactory {
48 | .init(req, self)
49 | }
50 |
51 | fileprivate func resolve(_ type: Any.Type, _ req: Request) -> Service {
52 | let id = ServiceId(type)
53 | guard let builder = builders[id] else {
54 | fatalError("Repository for id `\(id)` is not configured.")
55 | }
56 | return builder(req)
57 | }
58 |
59 | // TODO: 如何限制 type 继承自 Repository
60 | public func register(_ type: Any.Type, _ builder: @escaping ServiceBuilder) {
61 | builders[ServiceId(type)] = builder
62 | }
63 | }
64 |
65 | public struct ServiceFactory {
66 | private var registry: ServiceRegistry
67 | private var req: Request
68 |
69 | fileprivate init(_ req: Request, _ registry: ServiceRegistry) {
70 | self.req = req
71 | self.registry = registry
72 | }
73 |
74 | public func resolve(_ type: Any.Type) -> Service {
75 | registry.resolve(type, req)
76 | }
77 | }
78 |
79 | public extension Application {
80 | private struct Key: StorageKey {
81 | typealias Value = ServiceRegistry
82 | }
83 |
84 | var services: ServiceRegistry {
85 | if storage[Key.self] == nil {
86 | storage[Key.self] = .init(self)
87 | }
88 | return storage[Key.self]!
89 | }
90 | }
91 |
92 | public extension Request {
93 | var services: ServiceFactory {
94 | application.services.builder(self)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Sources/App/Util/PageUtil.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by laijihua on 2023/7/23.
6 | //
7 |
8 | import Foundation
9 | import AnyCodable
10 | import FluentKit
11 |
12 | final class PageUtil {
13 | static func genPageMetadata(pageMeta: PageMetadata?) -> AnyEncodable? {
14 | guard let pageMeta = pageMeta else {
15 | return nil
16 | }
17 |
18 | let maxPage = pageMeta.pageCount
19 | let curPage = pageMeta.page
20 |
21 | var showMaxMore: Bool = true
22 | var showMinMore: Bool = true
23 | var showPages: [Int] = []
24 |
25 | if (maxPage <= 3) {
26 | showMaxMore = false
27 | showMinMore = false
28 | showPages = [Int](1...maxPage)
29 | } else {
30 | if(curPage < 3) {
31 | showMinMore = false
32 | showMaxMore = true
33 | }
34 | else if (curPage > maxPage - 3) {
35 | showMinMore = true
36 | showMaxMore = false
37 | }
38 |
39 | if (curPage == 1) {
40 | showPages = [curPage, curPage + 1, curPage + 2]
41 | } else if (curPage == maxPage) {
42 | showPages = [curPage - 2, curPage - 1, curPage]
43 | } else {
44 | showPages = [curPage - 1,curPage, curPage + 1]
45 | }
46 | }
47 |
48 | return [
49 | "maxPage": maxPage,
50 | "minPage": 1,
51 | "curPage": curPage,
52 | "showMinMore": showMinMore,
53 | "showMaxMore": showMaxMore,
54 | "showPages": showPages,
55 | "total": pageMeta.total,
56 | "page":pageMeta.page,
57 | "per": pageMeta.per,
58 | "perOptions": [
59 | ["value": "10", "label": "10条/页"],
60 | ["value": "20", "label": "20条/页"],
61 | ["value": "30", "label": "30条/页"],
62 | ["value": "50", "label": "50条/页"]
63 | ],
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/App/configure.swift:
--------------------------------------------------------------------------------
1 | import Fluent
2 | import FluentPostgresDriver
3 | import JWT
4 | import Leaf
5 | import LeafKit
6 | import SMTP
7 | import Vapor
8 |
9 | // configures your application
10 | public func configure(_ app: Application) async throws {
11 | app.jwt.signers.use(.hs256(key: "blog123"))
12 | app.middleware = .init()
13 | app.middleware.use(RouteLoggingMiddleware(logLevel: .info))
14 | app.middleware.use(ErrorMiddleware.custom(environment: app.environment))
15 |
16 | app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
17 | app.views.use(.leaf)
18 | app.leaf.tags["jsonb"] = Json2B64Tag()
19 | app.leaf.tags["bjson"] = B642JsonTag()
20 |
21 | let corsConfiguration = CORSMiddleware.Configuration(
22 | allowedOrigin: .all,
23 | allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH],
24 | allowedHeaders: [
25 | .accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent,
26 | .accessControlAllowOrigin,
27 | ]
28 | )
29 | let cors = CORSMiddleware(configuration: corsConfiguration)
30 | // cores 需要放到最前面
31 | app.middleware.use(cors, at: .beginning)
32 |
33 | app.databases.use(.postgres(configuration: .init(
34 | hostname: Environment.get("DATABASE_HOST")!,
35 | port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
36 | username: Environment.get("DATABASE_USERNAME")!,
37 | password: Environment.get("DATABASE_PASSWORD"),
38 | database: Environment.get("DATABASE_NAME"),
39 | tls: .disable
40 | // tls: .prefer(try .init(configuration: .clientDefault))
41 | )
42 | ), as: .psql)
43 |
44 | // 配置session
45 | app.sessions.use(.fluent)
46 | app.migrations.add(SessionRecord.migration)
47 |
48 | /// 添加邮箱服务
49 | app.smtp.use(
50 | SMTPServerConfig(
51 | hostname: Environment.get("SMTP_HOST")!,
52 | port: Environment.get("SMTP_PORT").flatMap(Int.init(_:)) ?? 465,
53 | username: Environment.get("SMTP_USERNAME")!,
54 | password: Environment.get("SMTP_PASSWORD")!, // ZNZEIMJTGRWQSHOB, 第三方生成秘钥
55 | tlsConfiguration: .regularTLS
56 | )
57 | )
58 |
59 | try migrations(app)
60 | try routes(app)
61 | try services(app)
62 | try repositories(app)
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/App/constants.swift:
--------------------------------------------------------------------------------
1 | struct Constants {
2 | // 系统管理员
3 | static let superAdminName = "admin"
4 | static let superAdminEmail = "admin@iblog.com"
5 | static let superAdminPwd = "admin123456"
6 |
7 | // 普通用户角色
8 | static let userRoleName = "普通用户"
9 | static let userRoleAdmin = "管理员"
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/App/entrypoint.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 | import Dispatch
3 | import Logging
4 |
5 |
6 | /// This extension is temporary and can be removed once Vapor gets this support.
7 | private extension Vapor.Application {
8 | static let baseExecutionQueue = DispatchQueue(label: "vapor.codes.entrypoint")
9 |
10 | func runFromAsyncMainEntrypoint() async throws {
11 | try await withCheckedThrowingContinuation { continuation in
12 | Vapor.Application.baseExecutionQueue.async { [self] in
13 | do {
14 | try self.run()
15 | continuation.resume()
16 | } catch {
17 | continuation.resume(throwing: error)
18 | }
19 | }
20 | }
21 | }
22 | }
23 |
24 | @main
25 | enum Entrypoint {
26 | static func main() async throws {
27 | var env = try Environment.detect()
28 | try LoggingSystem.bootstrap(from: &env)
29 |
30 | let app = Application(env)
31 | defer { app.shutdown() }
32 |
33 | try await configure(app)
34 | try await app.runFromAsyncMainEntrypoint()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/App/migrations.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | func migrations(_ app: Application) throws {
4 | app.migrations.add(CreateInvite())
5 | app.migrations.add(CreateUser())
6 | app.migrations.add(CreateUserAuth())
7 | app.migrations.add(CreateCategory())
8 | app.migrations.add(CreateTag())
9 | app.migrations.add(CreateSideBar())
10 | app.migrations.add(CreatePost())
11 | app.migrations.add(CreatePostTag())
12 | app.migrations.add(CreateLink())
13 | app.migrations.add(CreateEmailCode())
14 |
15 | // 权限管理
16 | app.migrations.add(CreateRole())
17 | app.migrations.add(CreateMenu())
18 | app.migrations.add(CreatePermission())
19 | app.migrations.add(CreateUserRole())
20 | app.migrations.add(CreateRolePermission())
21 | app.migrations.add(CreatePermissionMenu())
22 |
23 | // 评论
24 | app.migrations.add(CreateComment())
25 | app.migrations.add(CreateReply())
26 |
27 | // 通知
28 | app.migrations.add(CreateMessageInfo())
29 | app.migrations.add(CreateMessage())
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/App/repositories.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | func repositories(_ app: Application) throws {
4 | app.repositories.register(UserRepository.self) { req in
5 | UserRepositoryImpl(req)
6 | }
7 | app.repositories.register(InviteRepository.self) { req in
8 | InviteRepositoryImpl(req)
9 | }
10 | app.repositories.register(TagRepository.self) { req in
11 | TagRepositoryImpl(req)
12 | }
13 | app.repositories.register(CategoryRepository.self) { req in
14 | CategoryRepositoryImpl(req)
15 | }
16 | app.repositories.register(PostRepository.self) { req in
17 | PostRepositoryImpl(req)
18 | }
19 | app.repositories.register(LinkRepository.self) { req in
20 | LinkRepositoryImpl(req)
21 | }
22 |
23 | app.repositories.register(RoleRepository.self) { req in
24 | RoleRepositoryImpl(req)
25 | }
26 | app.repositories.register(PermissionRepository.self) { req in
27 | PermissionRepositoryImpl(req)
28 | }
29 | app.repositories.register(MenuRepository.self) { req in
30 | MenuRepositoryImpl(req)
31 | }
32 |
33 | app.repositories.register(CommentRepository.self) { req in
34 | CommentRepositoryImpl(req)
35 | }
36 | app.repositories.register(ReplyRepository.self) { req in
37 | ReplyRepositoryImpl(req)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/App/routes.swift:
--------------------------------------------------------------------------------
1 | import Fluent
2 | import Vapor
3 |
4 | func routes(_ app: Application) throws {
5 |
6 | // Api
7 | let apiGroup = app.grouped("api")
8 | try apiGroup.register(collection: AuthController())
9 |
10 | // 前端
11 | try app.grouped(app.sessions.middleware).register(collection: WebFrontController())
12 |
13 | // 后台
14 | let webGroup = app.grouped("web").grouped(app.sessions.middleware)
15 | try webGroup.grouped("auth").register(collection: WebAuthController())
16 | try webGroup.grouped("backend").register(collection: WebBackendController())
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/App/services.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | func services(_ app: Application) throws {
4 | app.services.register(AuthService.self) { req in
5 | return AuthServiceImpl(req)
6 | }
7 | app.services.register(BackendService.self) { req in
8 | return BackendServiceImpl(req)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/SMTP/Models/Attachment.swift:
--------------------------------------------------------------------------------
1 |
2 |
3 | import Foundation
4 |
5 | public enum ContentDisposition: String {
6 | case inline, attachment
7 | }
8 |
9 | public struct Attachment {
10 | let name: String
11 | let contentType: String
12 | let data: Data
13 | var disposition: ContentDisposition = .attachment
14 | /// Use this ID to reference inline attachements in email body
15 | /// - EXAMPLE: \
16 | var contentId: String? = nil
17 |
18 | public init(name: String, contentType: String, data: Data, disposition: ContentDisposition = .attachment, contentId: String? = nil) {
19 | self.name = name
20 | self.contentType = contentType
21 | self.data = data
22 | self.disposition = disposition
23 | self.contentId = contentId
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SMTP/Models/EmailAddress.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct EmailAddress {
4 | let address: String
5 | let name: String?
6 |
7 | public init(address: String, name: String? = nil) {
8 | self.address = address
9 | self.name = name
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/SMTP/Models/SMTPRequestAction.swift:
--------------------------------------------------------------------------------
1 |
2 | enum SMTPRequestAction {
3 | case sayHello(serverName: String)
4 | case startTLS
5 | case beginAuthentication
6 | case authUser(String)
7 | case authPassword(String)
8 | case mailFrom(String)
9 | case recipient(String)
10 | case data
11 | case transferData(Email)
12 | case quit
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/SMTP/Models/SMTPResponse.swift:
--------------------------------------------------------------------------------
1 |
2 | enum SMTPResponse {
3 | case ok(Int, String)
4 | case error(String)
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/SMTP/Models/SMTPServerConfiguration.swift:
--------------------------------------------------------------------------------
1 |
2 | import NIO
3 |
4 | public struct SMTPServerConfig {
5 | public enum TLSConfiguration {
6 | case startTLS
7 | case regularTLS
8 | case unsafeNoTLS
9 | }
10 |
11 | public var hostname: String
12 | public var port: Int
13 | public var username: String
14 | public var password: String
15 | public var tlsConfiguration: TLSConfiguration
16 |
17 | public init(hostname: String, port: Int, username: String, password: String, tlsConfiguration: TLSConfiguration) {
18 | self.hostname = hostname
19 | self.port = port
20 | self.username = username
21 | self.password = password
22 | self.tlsConfiguration = tlsConfiguration
23 | }
24 |
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SMTP/Request+SMTP.swift:
--------------------------------------------------------------------------------
1 |
2 |
3 | import Vapor
4 |
5 | public extension Request {
6 | var smtp: SMTP {
7 | return SMTP(application: application, on: self.eventLoop)
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/Sources/SMTP/SMTP.swift:
--------------------------------------------------------------------------------
1 |
2 |
3 | import Vapor
4 | import NIO
5 | import NIOExtras
6 | import NIOTLS
7 | import NIOSSL
8 |
9 | public struct SMTP {
10 | public var eventLoopGroup: EventLoopGroup
11 |
12 | struct ConfigurationKey: StorageKey {
13 | typealias Value = SMTPServerConfig
14 | }
15 |
16 | public var configuration: SMTPServerConfig {
17 | get {
18 | guard self.application.storage[ConfigurationKey.self] != nil else{
19 | fatalError("Set SMTP Config using app.smtp.use()")
20 | }
21 | return self.application.storage[ConfigurationKey.self]!
22 |
23 | }
24 | nonmutating set {
25 | self.application.storage[ConfigurationKey.self] = newValue
26 | }
27 | }
28 |
29 |
30 | let application: Application
31 |
32 | init(application: Application){
33 | self.application = application
34 | self.eventLoopGroup = application.eventLoopGroup
35 | }
36 |
37 | public init(application: Application, on eventLoop: EventLoop) {
38 | self.application = application
39 | self.eventLoopGroup = eventLoop
40 | }
41 |
42 | public func use(_ config: SMTPServerConfig) {
43 | self.configuration = config
44 | }
45 |
46 | func configureBootstrap(group: EventLoopGroup,
47 | email: Email,
48 | emailSentPromise: EventLoopPromise) -> ClientBootstrap {
49 | return ClientBootstrap(group: group).channelInitializer { channel in
50 | var handlers: [ChannelHandler] = [
51 | ByteToMessageHandler(LineBasedFrameDecoder()),
52 | SMTPResponseDecoder(),
53 | MessageToByteHandler(SMTPRequestEncoder()),
54 | SendEmailHandler(configuration: self.configuration,
55 | email: email,
56 | allDonePromise: emailSentPromise)
57 | ]
58 |
59 | switch self.configuration.tlsConfiguration {
60 | case .regularTLS:
61 | do {
62 | let sslContext = try NIOSSLContext(configuration: .makeClientConfiguration())
63 | let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: self.configuration.hostname)
64 | handlers.insert(sslHandler, at: 0)
65 | }
66 | catch {
67 | return channel.eventLoop.makeFailedFuture(error)
68 | }
69 | //No additional ssl for other modes
70 | default:
71 | ()
72 | }
73 | return channel.pipeline.addHandlers(handlers, position: .last)
74 | }
75 | }
76 |
77 | public func send(_ email: Email) -> EventLoopFuture {
78 | let emailSentPromise: EventLoopPromise = self.eventLoopGroup.next().makePromise()
79 | let completedPromise: EventLoopPromise = self.eventLoopGroup.next().makePromise(of: Error?.self)
80 | let bootstrap: ClientBootstrap
81 |
82 | bootstrap = configureBootstrap(group: self.eventLoopGroup,
83 | email: email,
84 | emailSentPromise: emailSentPromise)
85 |
86 | let connection = bootstrap.connect(host: self.configuration.hostname, port: self.configuration.port)
87 |
88 | connection.cascadeFailure(to: emailSentPromise)
89 |
90 | emailSentPromise.futureResult.map {
91 | connection.whenSuccess { $0.close(promise: nil) }
92 | completedPromise.succeed(nil)
93 | }.whenFailure { error in
94 | connection.whenSuccess { $0.close(promise: nil) }
95 | completedPromise.succeed(error)
96 | }
97 | return completedPromise.futureResult
98 | }
99 | }
100 |
101 | public extension Application {
102 | var smtp: SMTP {
103 | return .init(application: self)
104 | }
105 | }
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/Sources/SMTP/SMTPRequestEncoder.swift:
--------------------------------------------------------------------------------
1 |
2 |
3 | import NIO
4 | import NIOFoundationCompat
5 | import Foundation
6 |
7 | final class SMTPRequestEncoder: MessageToByteEncoder {
8 | typealias OutboundIn = SMTPRequestAction
9 |
10 | func encode(data: SMTPRequestAction, out: inout ByteBuffer) throws {
11 | switch data {
12 | case .sayHello(serverName: let server):
13 | out.writeString("EHLO \(server)")
14 | case .startTLS:
15 | out.writeString("STARTTLS")
16 | case .mailFrom(let from):
17 | out.writeString("MAIL FROM:<\(from)>")
18 | case .recipient(let rcpt):
19 | out.writeString("RCPT TO:<\(rcpt)>")
20 | case .data:
21 | out.writeString("DATA")
22 | case .transferData(let email):
23 | email.write(to: &out)
24 | case .quit:
25 | out.writeString("QUIT")
26 | case .beginAuthentication:
27 | out.writeString("AUTH LOGIN")
28 | case .authUser(let user):
29 | let userData = Data(user.utf8)
30 | out.writeBytes(userData.base64EncodedData())
31 | case .authPassword(let password):
32 | let passwordData = Data(password.utf8)
33 | out.writeBytes(passwordData.base64EncodedData())
34 | }
35 |
36 | out.writeString("\r\n")
37 | }
38 |
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/SMTP/SMTPResponseDecoder.swift:
--------------------------------------------------------------------------------
1 |
2 |
3 | import NIO
4 |
5 | enum SMTPResponseDecoderError: Error {
6 | case malformedMessage
7 | }
8 |
9 | final class SMTPResponseDecoder: ChannelInboundHandler {
10 | typealias InboundIn = ByteBuffer
11 | typealias InboundOut = SMTPResponse
12 |
13 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
14 | var response = self.unwrapInboundIn(data)
15 |
16 | if let firstFourBytes = response.readString(length: 4), let code = Int(firstFourBytes.dropLast()) {
17 | let remainder = response.readString(length: response.readableBytes) ?? ""
18 |
19 | let firstChar = firstFourBytes.first!
20 | let fourthChar = firstFourBytes.last!
21 |
22 | switch (firstChar, fourthChar) {
23 | case ("2", " "), ("3", " "):
24 | let parsedMessage = SMTPResponse.ok(code, remainder)
25 | context.fireChannelRead(self.wrapInboundOut(parsedMessage))
26 | case(_,"-"):
27 | ()
28 | default:
29 | context.fireChannelRead(self.wrapInboundOut(.error(firstFourBytes + remainder)))
30 | }
31 | }
32 | else {
33 | context.fireErrorCaught(SMTPResponseDecoderError.malformedMessage)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/AppTests/AppTests.swift:
--------------------------------------------------------------------------------
1 | @testable import App
2 | import XCTVapor
3 |
4 | final class AppTests: XCTestCase {
5 | func testHelloWorld() async throws {
6 | let app = Application(.testing)
7 | defer { app.shutdown() }
8 | try await configure(app)
9 |
10 | try app.test(.GET, "hello", afterResponse: { res in
11 | XCTAssertEqual(res.status, .ok)
12 | XCTAssertEqual(res.body.string, "Hello, world!")
13 | })
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # Docker Compose file for Vapor
2 | #
3 | # Install Docker on your system to run and test
4 | # your Vapor app in a production-like environment.
5 | #
6 | # Note: This file is intended for testing and does not
7 | # implement best practices for a production deployment.
8 | #
9 | # Learn more: https://docs.docker.com/compose/reference/
10 | #
11 | # Build images: docker-compose build
12 | # Start app: docker-compose up app
13 | # Stop all: docker-compose down
14 | #
15 | version: '3.7'
16 |
17 | x-shared_environment: &shared_environment
18 | LOG_LEVEL: ${LOG_LEVEL:-debug}
19 |
20 | services:
21 | app:
22 | image: hello:latest
23 | build:
24 | context: .
25 | environment:
26 | <<: *shared_environment
27 | ports:
28 | - '8080:8080'
29 | # user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user.
30 | command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
31 |
--------------------------------------------------------------------------------
/package.sh:
--------------------------------------------------------------------------------
1 | docker build -t package-docker-image .
2 | docker create --name temporary-container package-docker-image
3 | docker cp temporary-container:/staging.tar.gz ./PackageApp.zip
4 | docker rm temporary-container
5 | open ./
6 |
--------------------------------------------------------------------------------