├── .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 = "'] 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 += `
  • `) 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 | iBlog 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 | ![前端](https://github.com/swiftdo/vapor-blog/assets/12316547/c1df733b-5469-47c8-b213-b51d81130003) 31 | 32 | 后台: 33 | 34 | ![后台](https://github.com/swiftdo/vapor-blog/assets/12316547/2e5b6653-c7f9-4c05-bf56-616b382e56d6) 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 |
    4 |
    5 |
    登录
    6 |
    7 | 10 | 11 |
    12 | 13 |
    14 | 17 | 18 |
    19 | 20 | 21 |
    22 |
    23 | 24 | #endexport 25 | #endextend -------------------------------------------------------------------------------- /Resources/Views/auth/register.leaf: -------------------------------------------------------------------------------- 1 | #extend("basesimple"): 2 | #export("content"): 3 |
    4 |
    5 |
    注册
    6 |
    7 | 10 | 11 |
    12 |
    13 | 16 | 17 |
    18 |
    19 | 22 | 23 |
    24 |
    25 | 29 | 30 |
    31 | 32 |
    33 |
    34 | 37 | 38 |
    39 | 40 |
    41 | 42 |
    43 |
    44 | 45 | 46 | 50 | 53 | 54 | 55 | 85 | #endexport 86 | #endextend -------------------------------------------------------------------------------- /Resources/Views/backend/categoryMgt.leaf: -------------------------------------------------------------------------------- 1 | #extend("basebackend"): 2 | #export("contentRight"): 3 |
    15 |
    16 | 17 | 18 |
    19 |
    20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | #for(item in data.items): 37 | 38 | 43 | 48 | 53 | 56 | 59 | 60 | #endfor 61 | 62 |
    25 | 28 | 名称导航创建者操作
    39 | 42 | 44 |
    45 | #(item.name) 46 |
    47 |
    49 |
    50 | #if(item.isNav): 是 #else: 否 #endif 51 |
    52 |
    54 |
    #(item.owner.name)
    55 |
    57 | 58 |
    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 | 147 | 159 | 160 | #endexport 161 | #endextend -------------------------------------------------------------------------------- /Resources/Views/backend/index.leaf: -------------------------------------------------------------------------------- 1 | #extend("basebackend"): 2 | #export("contentRight"): 3 |
    4 |
    后台首页
    5 | logo 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 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | #for(item in data.items): 36 | 37 | 42 | 47 | 52 | 57 | 58 | #endfor 59 | 60 |
    25 | 28 | 名称创建者操作
    38 | 41 | 43 |
    44 | #(item.name) 45 |
    46 |
    48 |
    49 | #(item.owner.name) 50 |
    51 |
    53 |
    54 | 55 |
    56 |
    61 | #extend("partial/pageCtrl") 62 |
    63 | 64 |
    65 |
    66 |
    67 | 68 |
    修改标签
    69 | 70 | 71 |
    72 |
    73 |
    74 |
    75 | 76 | 133 | 134 | 135 | 136 | 144 | 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 | 47 | 50 | 55 | 58 | 59 | #endfor 60 | 61 |
    用户名邮箱角色操作
    43 |
    44 | #(item.name) 45 |
    46 |
    48 |
    #(item.email)
    49 |
    51 | #for(role in item.roles): 52 | #(role.name) 53 | #endfor 54 | 56 | 57 |
    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 | 30 | 38 | 41 | 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 | 64 | 65 | 66 | 74 | 77 | 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 | 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 |
    5 |
    6 |
    所有分类
    7 | 8 |
    9 | #for(category in data): 10 | #(category.name) 11 | #endfor 12 |
    13 |
    14 |
    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 |
    10 |
    #(article.owner.name)
    11 |
    #date(article.createdAt, "yyyy-MM-dd")
    12 | #(article.category.name) 13 |
    14 |

    #(article.title)

    15 |

    #(article.desc)

    16 |
    17 |
    18 | #for(tag in article.tags): 19 | ##(tag.name) 20 | #endfor 21 |
    22 |
    23 | 查看详情 24 |
    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 |
    10 |

    友链

    11 |
    12 | #for(link in links): 13 | #(link.title) 14 | #endfor 15 |
    16 | 17 | 赣ICP备2021010021号-1 18 | 24 |
    25 | 26 |
    27 |
    © 2023 swiftdo
    28 |
    Designed By swiftdo Powered By Vapor
    29 |
    30 | 31 | Github 32 | 33 |
    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 | --------------------------------------------------------------------------------