From b0f2b7e441faee47b11a329a966f18d6ad4b625f Mon Sep 17 00:00:00 2001 From: GuangGuang Date: Sun, 28 Jun 2020 15:49:17 +0800 Subject: [PATCH] first commit --- .dockerignore | 2 + .env | 5 + .env.development | 8 + .env.development.custom_name | 8 + .env.production | 5 + .env.production.custom_name | 5 + .env.staging | 5 + .env.testing | 8 + .gitignore | 9 + .travis.yml | 36 +++ Dockerfile | 41 ++++ Package.resolved | 223 +++++++++++++++++ Package.swift | 38 +++ README.md | 133 ++++++++++ Sources/App/Controllers/.gitkeep | 0 Sources/App/Controllers/EnvController.swift | 38 +++ .../App/Controllers/GalaxyController.swift | 33 +++ Sources/App/Controllers/StarController.swift | 33 +++ Sources/App/Controllers/TodoController.swift | 33 +++ Sources/App/Controllers/UserController.swift | 230 ++++++++++++++++++ Sources/App/Migrations/CreateGalaxy.swift | 14 ++ Sources/App/Migrations/CreateStar.swift | 17 ++ Sources/App/Migrations/CreateTodo.swift | 14 ++ Sources/App/Migrations/CreateUser.swift | 18 ++ Sources/App/Migrations/CreateUserToken.swift | 22 ++ Sources/App/Models/BusinessError.swift | 52 ++++ Sources/App/Models/Env.swift | 43 ++++ Sources/App/Models/Galaxy.swift | 25 ++ Sources/App/Models/Star.swift | 26 ++ Sources/App/Models/Todo.swift | 20 ++ Sources/App/Models/User.swift | 28 +++ Sources/App/Models/UserToken.swift | 38 +++ Sources/App/configure.swift | 105 ++++++++ Sources/App/routes.swift | 18 ++ Sources/Run/main.swift | 19 ++ .../Controllers/GalaxyController.http | 17 ++ .../Controllers/StarController.http | 33 +++ .../Controllers/TodoController.http | 29 +++ .../Controllers/UserController.http | 84 +++++++ Tests/AppRestApiTests/README.md | 34 +++ Tests/AppRestApiTests/http-request.http | 23 ++ Tests/AppRestApiTests/rest-client.env.json | 11 + .../rest-client.private.env.json | 14 ++ Tests/AppRestApiTests/scripts/my-script.js | 0 Tests/AppTests/AppTests.swift | 19 ++ docker-compose.yml | 74 ++++++ etc/nginx/cert.key | 28 +++ etc/nginx/cert.pem | 21 ++ etc/nginx/fastcgi.conf | 26 ++ etc/nginx/fastcgi.conf.default | 26 ++ etc/nginx/fastcgi_params | 25 ++ etc/nginx/fastcgi_params.default | 25 ++ etc/nginx/koi-utf | 109 +++++++++ etc/nginx/koi-win | 103 ++++++++ etc/nginx/mime.types | 97 ++++++++ etc/nginx/mime.types.default | 97 ++++++++ etc/nginx/nginx.conf | 156 ++++++++++++ etc/nginx/nginx.conf.default | 117 +++++++++ etc/nginx/scgi_params | 17 ++ etc/nginx/scgi_params.default | 17 ++ etc/nginx/uwsgi_params | 17 ++ etc/nginx/uwsgi_params.default | 17 ++ etc/nginx/win-utf | 126 ++++++++++ 63 files changed, 2714 insertions(+) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 .env.development create mode 100644 .env.development.custom_name create mode 100644 .env.production create mode 100644 .env.production.custom_name create mode 100644 .env.staging create mode 100644 .env.testing create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Dockerfile create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/App/Controllers/.gitkeep create mode 100644 Sources/App/Controllers/EnvController.swift create mode 100644 Sources/App/Controllers/GalaxyController.swift create mode 100644 Sources/App/Controllers/StarController.swift create mode 100644 Sources/App/Controllers/TodoController.swift create mode 100644 Sources/App/Controllers/UserController.swift create mode 100644 Sources/App/Migrations/CreateGalaxy.swift create mode 100644 Sources/App/Migrations/CreateStar.swift create mode 100644 Sources/App/Migrations/CreateTodo.swift create mode 100644 Sources/App/Migrations/CreateUser.swift create mode 100644 Sources/App/Migrations/CreateUserToken.swift create mode 100644 Sources/App/Models/BusinessError.swift create mode 100644 Sources/App/Models/Env.swift create mode 100644 Sources/App/Models/Galaxy.swift create mode 100644 Sources/App/Models/Star.swift create mode 100644 Sources/App/Models/Todo.swift create mode 100644 Sources/App/Models/User.swift create mode 100644 Sources/App/Models/UserToken.swift create mode 100644 Sources/App/configure.swift create mode 100644 Sources/App/routes.swift create mode 100644 Sources/Run/main.swift create mode 100644 Tests/AppRestApiTests/Controllers/GalaxyController.http create mode 100644 Tests/AppRestApiTests/Controllers/StarController.http create mode 100644 Tests/AppRestApiTests/Controllers/TodoController.http create mode 100644 Tests/AppRestApiTests/Controllers/UserController.http create mode 100644 Tests/AppRestApiTests/README.md create mode 100644 Tests/AppRestApiTests/http-request.http create mode 100644 Tests/AppRestApiTests/rest-client.env.json create mode 100644 Tests/AppRestApiTests/rest-client.private.env.json create mode 100644 Tests/AppRestApiTests/scripts/my-script.js create mode 100644 Tests/AppTests/AppTests.swift create mode 100644 docker-compose.yml create mode 100644 etc/nginx/cert.key create mode 100644 etc/nginx/cert.pem create mode 100644 etc/nginx/fastcgi.conf create mode 100644 etc/nginx/fastcgi.conf.default create mode 100644 etc/nginx/fastcgi_params create mode 100644 etc/nginx/fastcgi_params.default create mode 100644 etc/nginx/koi-utf create mode 100644 etc/nginx/koi-win create mode 100644 etc/nginx/mime.types create mode 100644 etc/nginx/mime.types.default create mode 100644 etc/nginx/nginx.conf create mode 100644 etc/nginx/nginx.conf.default create mode 100644 etc/nginx/scgi_params create mode 100644 etc/nginx/scgi_params.default create mode 100644 etc/nginx/uwsgi_params create mode 100644 etc/nginx/uwsgi_params.default create mode 100644 etc/nginx/win-utf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2d9f16e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.build/ +.swiftpm/ diff --git a/.env b/.env new file mode 100644 index 0000000..3ac76e7 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +HOST_NAME="127.0.0.1" +HOST_PORT=8080 + +DATABASE_HOST=localhost +DATABASE_NAME=vapor_database_fallback \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..fdafcef --- /dev/null +++ b/.env.development @@ -0,0 +1,8 @@ +HOST_NAME="127.0.0.1" +HOST_PORT=8080 + +DATABASE_HOST=localhost +DATABASE_NAME=vapor_database_dev + +DATABASE_USERNAME=vapor_username_dev +DATABASE_PASSWORD=vapor_password_dev diff --git a/.env.development.custom_name b/.env.development.custom_name new file mode 100644 index 0000000..3707d59 --- /dev/null +++ b/.env.development.custom_name @@ -0,0 +1,8 @@ +HOST_NAME="127.0.0.1" +HOST_PORT=8080 + +DATABASE_HOST=localhost +DATABASE_NAME=vapor_database_dev_custom + +DATABASE_USERNAME=vapor_username_dev_custom +DATABASE_PASSWORD=vapor_password_dev_custom diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..1be5609 --- /dev/null +++ b/.env.production @@ -0,0 +1,5 @@ +HOST_NAME="www.production.com" +HOST_PORT=8080 + +DATABASE_HOST=localhost +DATABASE_NAME=vapor_database_prod diff --git a/.env.production.custom_name b/.env.production.custom_name new file mode 100644 index 0000000..4e9d530 --- /dev/null +++ b/.env.production.custom_name @@ -0,0 +1,5 @@ +HOST_NAME="www.production.custom.com" +HOST_PORT=8080 + +DATABASE_HOST=localhost +DATABASE_NAME=vapor_database_prod_custom diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..7e48efc --- /dev/null +++ b/.env.staging @@ -0,0 +1,5 @@ +HOST_NAME="www.staging.com" +HOST_PORT=8080 + +DATABASE_HOST=localhost +DATABASE_NAME=vapor_database_stag diff --git a/.env.testing b/.env.testing new file mode 100644 index 0000000..df537da --- /dev/null +++ b/.env.testing @@ -0,0 +1,8 @@ +HOST_NAME="www.tesing.com" +HOST_PORT=8080 + +DATABASE_HOST=localhost +DATABASE_NAME=vapor_database_test + +DATABASE_USERNAME=vapor_username_test +DATABASE_PASSWORD=vapor_password_test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c56b687 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +Packages +.build +xcuserdata +*.xcodeproj +DerivedData/ +.DS_Store +db.sqlite +.swiftpm +.idea diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4ca2dd0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,36 @@ +language: generic +matrix: + include: + - os: osx + osx_image: xcode11.5 + env: SCHEME="Run" + + +before_install: + - gem install xcpretty + - brew tap vapor/tap + - brew update + - brew install vapor + + +script: + - swift build -Xswiftc -g -c debug + - swift package generate-xcodeproj + - xcodebuild -scheme $SCHEME -enableCodeCoverage YES test | xcpretty + + +after_success: + - bash <(curl -s https://codecov.io/bash) + +# +deploy: + provider: releases + api_key: + secure: Y1fkSiMXxU0wJEZ12PtfuWheTcIFD870iHpCRollKyVjobI50MF0yYWHm8jMtwHCYLKwmf+SZwp/w+emRZ6iho1n5yCo+zPeAcZSc2XCi7pzGzJ5kKvi3Lgh01BduTUI30+LqJ7XNk2HXH61Zl2eBZl4hFn6+Bc5BmCNyue2PKs2gVexpTub7X3abhdCR6frbdtC3lNW2HWiH/j4ccDE6Lmr1WJjFgM2QzoXVc56wWaUlXwOBPAmIG8AZM2nN7wKqV7znMJTNnE+hw7e1vtdLaV4QrNDDkya4f3Z1mgAJCNDQg8sIERvqdAg4NOh3Y090HR5QYHp8WejrvbKg/54KIdTE0L7TzLeYARG2NrqA0o4IezLGvbmlDL6vnwegNhSHp1IK89ZN2he/Tt4t0ONrcyKb7V4N6s5LKxzI24pvAjwG3pdqdO57iXus3CMksxaNW4F9ZaWaObSSi8zqFLqgyx8ZpTjy7zTso8xKyO/+/BpaD1BF8NJCzHKqgQmWbypyo4tNlRtyWsli7Eo2xcrEwTPU07CbYNbUnRfEj5hHc+Or/V0T6hgSnywaNs9crU9bedOn8Xh8myPamV17TGr282f7pwqCyIg+wms13PyhV5inNM7B7qbufXzQnM+Zu3OrBqKEVD7A1H3QxoLLhGJNe3gQKuFjXcC5Y/q68Y0WqQ= + file: ".build/debug/Run" + on: + tags: true + branch: master + repo: Guang1234567/vapor4_auth_template + skip_cleanup: 'true' + draft: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3203d24 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# ================================ +# Build image +# ================================ +FROM swift:5.2-bionic as build +WORKDIR /build + +# First just resolve dependencies. +# This creates a cached layer that can be reused +# as long as your Package.swift/Package.resolved +# files do not change. +COPY ./Package.* ./ +RUN swift package resolve + +# Copy entire repo into container +COPY . . + +# Compile with optimizations +RUN swift build --enable-test-discovery -c release + +# ================================ +# Run image +# ================================ +FROM swift:5.2-bionic-slim + +# Create a vapor user and group with /app as its home directory +RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor + +# Switch to the new home directory +WORKDIR /app + +# Copy build artifacts +COPY --from=build --chown=vapor:vapor /build/.build/release /app +# Uncomment the next line if you need to load resources from the `Public` directory +#COPY --from=build --chown=vapor:vapor /build/Public /app/Public + +# Ensure all further commands run as the vapor user +USER vapor:vapor + +# Start the Vapor service when the image is run, default to listening on 8080 in production environment +ENTRYPOINT ["./Run"] +CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..46db0e6 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,223 @@ +{ + "object": { + "pins": [ + { + "package": "async-http-client", + "repositoryURL": "https://github.com/swift-server/async-http-client.git", + "state": { + "branch": null, + "revision": "037b70291941fe43de668066eb6fb802c5e181d2", + "version": "1.1.1" + } + }, + { + "package": "async-kit", + "repositoryURL": "https://github.com/vapor/async-kit.git", + "state": { + "branch": null, + "revision": "8e2cad80dd9f62f0349a2a78ab8c81fb456f4d4a", + "version": "1.1.1" + } + }, + { + "package": "console-kit", + "repositoryURL": "https://github.com/vapor/console-kit.git", + "state": { + "branch": null, + "revision": "05c52a91631a6ca432275439f6456a5dace0d1db", + "version": "4.1.2" + } + }, + { + "package": "fluent", + "repositoryURL": "https://github.com/vapor/fluent.git", + "state": { + "branch": null, + "revision": "e681c93df3201a2d8ceef15e8a9a0634578df233", + "version": "4.0.0" + } + }, + { + "package": "fluent-kit", + "repositoryURL": "https://github.com/vapor/fluent-kit.git", + "state": { + "branch": null, + "revision": "ad5eef38c5168fdb4b77d5a73794a90e0ce3b9a5", + "version": "1.0.2" + } + }, + { + "package": "fluent-postgres-driver", + "repositoryURL": "https://github.com/vapor/fluent-postgres-driver.git", + "state": { + "branch": null, + "revision": "cf8ec53b87606dc799a07a0754a297b32cf9f592", + "version": "2.0.0" + } + }, + { + "package": "postgres-kit", + "repositoryURL": "https://github.com/vapor/postgres-kit.git", + "state": { + "branch": null, + "revision": "fca27ff90326e3612ed0197598a7ce467bfedfbd", + "version": "2.1.0" + } + }, + { + "package": "postgres-nio", + "repositoryURL": "https://github.com/vapor/postgres-nio.git", + "state": { + "branch": null, + "revision": "25aa31fdd55ea119edee270ce4bd7bd4bd59fdf7", + "version": "1.1.0" + } + }, + { + "package": "routing-kit", + "repositoryURL": "https://github.com/vapor/routing-kit.git", + "state": { + "branch": null, + "revision": "e7f2d5bd36dc65a9edb303541cb678515a7fece3", + "version": "4.1.0" + } + }, + { + "package": "RxSwift", + "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", + "state": { + "branch": null, + "revision": "002d325b0bdee94e7882e1114af5ff4fe1e96afa", + "version": "5.1.1" + } + }, + { + "package": "sql-kit", + "repositoryURL": "https://github.com/vapor/sql-kit.git", + "state": { + "branch": null, + "revision": "8b82edd5d918068f204e3ac4206d5d15f6740395", + "version": "3.5.0" + } + }, + { + "package": "swift-backtrace", + "repositoryURL": "https://github.com/swift-server/swift-backtrace.git", + "state": { + "branch": null, + "revision": "f2fd8c4845a123419c348e0bc4b3839c414077d5", + "version": "1.2.0" + } + }, + { + "package": "swift-crypto", + "repositoryURL": "https://github.com/apple/swift-crypto.git", + "state": { + "branch": null, + "revision": "9b9d1868601a199334da5d14f4ab2d37d4f8d0c5", + "version": "1.0.2" + } + }, + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", + "version": "1.2.0" + } + }, + { + "package": "swift-metrics", + "repositoryURL": "https://github.com/apple/swift-metrics.git", + "state": { + "branch": null, + "revision": "708b960b4605abb20bc55d65abf6bad607252200", + "version": "2.0.0" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "120acb15c39aa3217e9888e515de160378fbcc1e", + "version": "2.18.0" + } + }, + { + "package": "swift-nio-extras", + "repositoryURL": "https://github.com/apple/swift-nio-extras.git", + "state": { + "branch": null, + "revision": "7cd24c0efcf9700033f671b6a8eaa64a77dd0b72", + "version": "1.5.1" + } + }, + { + "package": "swift-nio-http2", + "repositoryURL": "https://github.com/apple/swift-nio-http2.git", + "state": { + "branch": null, + "revision": "c5d10f4165128c3d0cc0e3c0f0a8ef55947a73a6", + "version": "1.12.2" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "f0b118d9af6c4e78bc4f3f4fbb464020172b0bf4", + "version": "2.7.5" + } + }, + { + "package": "Swift_Atomics", + "repositoryURL": "https://github.com/Guang1234567/Swift_Atomics.git", + "state": { + "branch": "master", + "revision": "32a8c89a132c5d6d4330002dc65af2953306d3b6", + "version": null + } + }, + { + "package": "Swift_Boost_Context", + "repositoryURL": "https://github.com/Guang1234567/Swift_Boost_Context.git", + "state": { + "branch": "master", + "revision": "cdc7565f77c5308db073036d0e03277dcb0f9e1b", + "version": null + } + }, + { + "package": "Swift_Coroutine_NIO2", + "repositoryURL": "https://github.com/Guang1234567/Swift_Coroutine_NIO2.git", + "state": { + "branch": "master", + "revision": "1ce084dd0abc4c1331cb81b6f010d7d6db23605f", + "version": null + } + }, + { + "package": "vapor", + "repositoryURL": "https://github.com/vapor/vapor.git", + "state": { + "branch": null, + "revision": "dc2aa1e02e04a47b67cb0dabed628fe844900f30", + "version": "4.14.0" + } + }, + { + "package": "websocket-kit", + "repositoryURL": "https://github.com/vapor/websocket-kit.git", + "state": { + "branch": null, + "revision": "021edd1ca55451ad15b3e84da6b4064e4b877b34", + "version": "2.1.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..69fecd7 --- /dev/null +++ b/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version:5.2 +import PackageDescription + +let package = Package( + name: "demo_swift_server_vapor_auth", + platforms: [ + .macOS(.v10_15) + ], + dependencies: [ + // 💧 A server-side Swift web framework. + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), + .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), + .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"), + .package(url: "https://github.com/Guang1234567/Swift_Coroutine_NIO2.git", .branch("master")) + ], + targets: [ + .target( + name: "App", + dependencies: [ + .product(name: "Fluent", package: "fluent"), + .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), + .product(name: "Vapor", package: "vapor"), + "Swift_Coroutine_NIO2" + ], + swiftSettings: [ + // Enable better optimizations when building in Release configuration. Despite the use of + // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release + // builds. See for details. + .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) + ] + ), + .target(name: "Run", dependencies: [.target(name: "App")]), + .testTarget(name: "AppTests", dependencies: [ + .target(name: "App"), + .product(name: "XCTVapor", package: "vapor"), + ]) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..25d5bcb --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +

+ Auth Template +
+
+ + Documentation + + + Team Chat + + + MIT License + + + Continuous Integration + + + Swift 4.1 + +

+ + + +## Usage + +```bash +# 1) + +brew install vapor + +# 2) + +vapor new demo_swift_server_vapor_auth --template https://github.com/Guang1234567/vapor4_auth_template +``` + +## Develop on `localhost` + +### macOS + +1) Serving `HTTPS` by `nginx` + + 1) install `nginx` + + https://docs.vapor.codes/4.0/deploy/nginx/ + + ```bash + brew install nginx + ``` + + 2) cfg ngnix + + https://github.com/Guang1234567/vapor4_auth_template/blob/82953c8f1d70a335ca0dbe8541965f5fc2851587/etc/nginx/nginx.conf#L117-L153 + + 3) start `nginx` + + ```bash + nginx -c ~/dev_kit/workspace/demo_swift_server_vapor_auth/etc/nginx/nginx.conf + ``` + + 4) check `HTTPS` Env + + Using your smartphone which under the same wifi with your macbookpro to browse: + + ```bash + http://:8080/ + + + For example (Dont copy!): + http://10.0.0.28:8080/ + ``` + + then will see: + + ```html + Welcome to nginx! + + If you see this page, the nginx web server is successfully installed and working. Further configuration is required. + + For online documentation and support please refer to nginx.org. + Commercial support is available at nginx.com. + + Thank you for using nginx. + ``` + +2) Start vapor web application and serving on `http://127.0.0.1:8080` + + ```bash + swift build -Xswiftc -g -c debug && .build/debug/Run --log debug --env development.custom_name + ``` + + > note: + > + > [issue: Crashed when calling Bcrypt.hash](https://github.com/vapor/vapor/issues/2229#issuecomment-653721292) on + > + > - macOS catalina 10.15.6 with xcode 11.6 + > - macOS catalina 10.15.5 with xcode 11.5 + + +then access vapor web application via `HTTPS` + + - on smartphone + + ```bash + https://10.0.0.28/ + + or + + https://10.0.0.28:443/ + ``` + + - on PC + + ```bash + https://127.0.0.1/ + + or + + https://127.0.0.1:443/ + ``` + + then both will see: + + ```html + It works! + ``` + +3) Test API via CLion with `swift plugin` + +https://github.com/Guang1234567/vapor4_auth_template/blob/82953c8f1d70a335ca0dbe8541965f5fc2851587/Tests/AppRestApiTests/Controllers/UserController.http#L1-L9 + +### Ubuntu + +// in the process ... ( I dont have the Ubuntu -_-|| ) \ No newline at end of file diff --git a/Sources/App/Controllers/.gitkeep b/Sources/App/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Sources/App/Controllers/EnvController.swift b/Sources/App/Controllers/EnvController.swift new file mode 100644 index 0000000..d9eb1c9 --- /dev/null +++ b/Sources/App/Controllers/EnvController.swift @@ -0,0 +1,38 @@ +import Vapor + +struct EnvController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let env = routes.grouped("env") + env.get(use: index) + + } + + func index(req: Request) throws -> String { + let envDetail: String = + """ + + https://docs.vapor.codes/4.0/environment/ + + \(req.application.environment) + + Environment Variables: + + DATABASE_HOST = \(String(describing:Environment.process.DATABASE_HOST)) + DATABASE_NAME = \(String(describing:Environment.process.DATABASE_NAME)) + + """ + + let processEnv: Environment = req.application.environment + //let processInfo: ProcessInfo = .processInfo + //req.logger.info("Environment Variables:\n") + //req.logger.info("\(processInfo.environment)") + if processEnv.name.contains(Environment.production.name) { + req.logger.notice("\(envDetail)") + } else { + req.logger.info("\(envDetail)") + } + + return envDetail + } + +} diff --git a/Sources/App/Controllers/GalaxyController.swift b/Sources/App/Controllers/GalaxyController.swift new file mode 100644 index 0000000..23e9571 --- /dev/null +++ b/Sources/App/Controllers/GalaxyController.swift @@ -0,0 +1,33 @@ +import Fluent +import Vapor + +struct GalaxyController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let galaxies = routes.grouped("galaxies") + galaxies.get(use: index) + galaxies.post(use: create) + galaxies.group(":galaxyID") { galaxy in + galaxy.delete(use: delete) + } + } + + func index(req: Request) throws -> EventLoopFuture<[Galaxy]> { + return Galaxy.query(on: req.db).with(\.$stars).all() + } + + func create(req: Request) throws -> EventLoopFuture { + let galaxy = try req.content.decode(Galaxy.self) + return galaxy.save(on: req.db).map { + galaxy + } + } + + func delete(req: Request) throws -> EventLoopFuture { + return Galaxy.find(req.parameters.get("galaxyID"), on: req.db) + .unwrap(or: Abort(.notFound)) + .flatMap { + $0.delete(on: req.db) + } + .transform(to: .ok) + } +} diff --git a/Sources/App/Controllers/StarController.swift b/Sources/App/Controllers/StarController.swift new file mode 100644 index 0000000..95fdaf9 --- /dev/null +++ b/Sources/App/Controllers/StarController.swift @@ -0,0 +1,33 @@ +import Fluent +import Vapor + +struct StarController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let stars = routes.grouped("stars") + stars.get(use: index) + stars.post(use: create) + stars.group(":starID") { star in + star.delete(use: delete) + } + } + + func index(req: Request) throws -> EventLoopFuture<[Star]> { + return Star.query(on: req.db).all() + } + + func create(req: Request) throws -> EventLoopFuture { + let galaxy = try req.content.decode(Star.self) + return galaxy.save(on: req.db).map { + galaxy + } + } + + func delete(req: Request) throws -> EventLoopFuture { + return Star.find(req.parameters.get("starID"), on: req.db) + .unwrap(or: Abort(.notFound)) + .flatMap { + $0.delete(on: req.db) + } + .transform(to: .ok) + } +} diff --git a/Sources/App/Controllers/TodoController.swift b/Sources/App/Controllers/TodoController.swift new file mode 100644 index 0000000..483bd72 --- /dev/null +++ b/Sources/App/Controllers/TodoController.swift @@ -0,0 +1,33 @@ +import Fluent +import Vapor + +struct TodoController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let todos = UserController.tokenProtected(routes).grouped("todos") + todos.get(use: index) + todos.post(use: create) + todos.group(":todoID") { todo in + todo.delete(use: delete) + } + } + + func index(req: Request) throws -> EventLoopFuture<[Todo]> { + return Todo.query(on: req.db).all() + } + + func create(req: Request) throws -> EventLoopFuture { + let todo = try req.content.decode(Todo.self) + return todo.save(on: req.db).map { + todo + } + } + + func delete(req: Request) throws -> EventLoopFuture { + return Todo.find(req.parameters.get("todoID"), on: req.db) + .unwrap(or: Abort(.notFound)) + .flatMap { + $0.delete(on: req.db) + } + .transform(to: .ok) + } +} diff --git a/Sources/App/Controllers/UserController.swift b/Sources/App/Controllers/UserController.swift new file mode 100644 index 0000000..f8c4046 --- /dev/null +++ b/Sources/App/Controllers/UserController.swift @@ -0,0 +1,230 @@ +import Fluent +import Vapor +import Swift_Coroutine_NIO2 + + +/// Creates new users and logs them in. +struct UserController: RouteCollection { + + static func passwordProtected(_ routes: RoutesBuilder) -> RoutesBuilder { + routes.grouped(User.authenticator()) + .grouped(User.guardMiddleware()) + } + + static func passwordAndTokenProtected(_ routes: RoutesBuilder) -> RoutesBuilder { + routes.grouped(User.authenticator()) + .grouped(UserToken.authenticator()) + .grouped(User.guardMiddleware()) + } + + static func tokenProtected(_ routes: RoutesBuilder) -> RoutesBuilder { + routes.grouped(UserToken.authenticator()) + .grouped(User.guardMiddleware()) + } + + static func sessionProtected(_ routes: RoutesBuilder) -> RoutesBuilder { + routes.grouped(User.sessionAuthenticator()) + } + + func boot(routes: RoutesBuilder) throws { + let users = routes.grouped("users") + users.post(use: create) + + // basic / password auth protected routes + /* + let passwordProtected = UserController.passwordProtected(routes) + passwordProtected.post("login", use: login) + */ + + // https://docs.vapor.codes/4.0/authentication/#composing-methods + // This composition of authenticators results in a route that can be accessed by either password or token. + // Such a route could allow a user to login and generate a token, + // then continue to use that token to generate new tokens + let passwordAndTokenProtected = UserController.passwordAndTokenProtected(routes) + passwordAndTokenProtected.post("login", use: login) + + // bearer / token auth protected routes + let tokenProtected = UserController.tokenProtected(routes) + tokenProtected.post("logout", use: logout) + tokenProtected.get("me", use: me) + } + + func create(req: Request) throws -> EventLoopFuture { + + try User.Create.validate(req) + let create = try req.content.decode(User.Create.self) + guard create.password == create.confirmPassword else { + throw Abort(.badRequest, reason: "Passwords did not match") + } + + return req.password.async + .hash(create.password) + .map { (digest: String) -> User in + User( + name: create.name, + email: create.email, + passwordHash: digest + ) + } + .flatMap { user in + user.save(on: req.db) + .flatMapThrowing { + try User.Get( + id: user.requireID(), + name: user.name, + email: user.email) + } + } + } + + /* + func login(req: Request) throws -> EventLoopFuture { + let user = try req.auth.require(User.self) + let userTokenBeDeleted = req.auth.get(UserToken.self) + + return req.application.threadPool + .runIfActive(eventLoop: req.eventLoop, user.generateToken) + .flatMap { token in + let saveToken = token.save(on: req.db) + .map { + token + } + + if let userTokenBeDeleted = userTokenBeDeleted { + return UserToken.query(on: req.db) + .filter(\.$value == userTokenBeDeleted.value) + .delete(force: true) + .flatMap { + saveToken + } + } else { + return saveToken + } + } + } + */ + + func login(req: Request) throws -> EventLoopFuture { + + let user = try req.auth.require(User.self) + let userTokenBeDeleted = req.auth.get(UserToken.self) + + let eventLoop = req.eventLoop + let ioThreadPool = req.application.threadPool + return EventLoopFuture.coroutine(eventLoop) { co in + + try co.continueOn(ioThreadPool) + + let token = try user.generateToken() + + try co.continueOn(eventLoop) + + if let userTokenBeDeleted = userTokenBeDeleted { + try UserToken.query(on: req.db) + .filter(\.$value == userTokenBeDeleted.value) + .delete(force: true) + .await(co) + } + + return try token.save(on: req.db) + .map { + token + } + .await(co) + } + } + + func logout(req: Request) throws -> EventLoopFuture { + let user = try req.auth.require(User.self) + let userTokenBeDeleted = try req.auth.require(UserToken.self) + req.auth.logout(User.self) + + return UserToken.query(on: req.db) + .filter(\.$value == userTokenBeDeleted.value) + .delete(force: true) + .flatMapThrowing { + try UserToken( + id: userTokenBeDeleted.id, + value: userTokenBeDeleted.value, + userID: userTokenBeDeleted.user.requireID()) + } + } + + func me(req: Request) throws -> EventLoopFuture { + let myself = try req.auth.require(User.self) + return try req.eventLoop.makeSucceededFuture( + User.Get(id: myself.requireID(), + name: myself.name, + email: myself.email) + ) + } +} + + +extension User { + + /// DTO : for creating(registing) a user + struct Create: Content { + var name: String + var email: String + var password: String + var confirmPassword: String + } + + /// DTO : for avoiding response sensitive field (eg. User.passwordHash) + struct Get: Content { + var id: UUID + var name: String + var email: String + } + +} + + +extension User.Create: Validatable { + + static func validations(_ validations: inout Validations) { + validations.add("name", as: String.self, is: !.empty) + validations.add("email", as: String.self, is: .email) + validations.add("password", as: String.self, is: .count(6...)) + } + +} + + +extension User: ModelAuthenticatable { + static let usernameKey = \User.$email + static let passwordHashKey = \User.$passwordHash + + func verify(password: String) throws -> Bool { + try Bcrypt.verify(password, created: self.passwordHash) + } +} + + +extension User { + func generateToken() throws -> UserToken { + try .init( + value: [UInt8].random(count: 16).base64, + userID: self.requireID() + ) + } +} + + +extension UserToken: ModelTokenAuthenticatable { + static let valueKey = \UserToken.$value + static let userKey = \UserToken.$user + + var isValid: Bool { + guard let expiresAt = self.expiresAt else { + return false + } + + return expiresAt > .init() + } +} + + +// Allow this model to be persisted in sessions. +extension User: ModelSessionAuthenticatable {} diff --git a/Sources/App/Migrations/CreateGalaxy.swift b/Sources/App/Migrations/CreateGalaxy.swift new file mode 100644 index 0000000..32d5a46 --- /dev/null +++ b/Sources/App/Migrations/CreateGalaxy.swift @@ -0,0 +1,14 @@ +import Fluent + +struct CreateGalaxy: Migration { + func prepare(on database: Database) -> EventLoopFuture { + return database.schema(Galaxy.schema) + .id() + .field("name", .string, .required) + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + return database.schema(Galaxy.schema).delete() + } +} diff --git a/Sources/App/Migrations/CreateStar.swift b/Sources/App/Migrations/CreateStar.swift new file mode 100644 index 0000000..cbd916d --- /dev/null +++ b/Sources/App/Migrations/CreateStar.swift @@ -0,0 +1,17 @@ +import Fluent + +struct CreateStar: Migration { + // Prepares the database for storing Star models. + func prepare(on database: Database) -> EventLoopFuture { + database.schema(Star.schema) + .id() + .field("name", .string, .required) + .field("galaxy_id", .uuid, .references(Galaxy.schema, "id")) + .create() + } + + // Optionally reverts the changes made in the prepare method. + func revert(on database: Database) -> EventLoopFuture { + database.schema(Star.schema).delete() + } +} diff --git a/Sources/App/Migrations/CreateTodo.swift b/Sources/App/Migrations/CreateTodo.swift new file mode 100644 index 0000000..ece3e94 --- /dev/null +++ b/Sources/App/Migrations/CreateTodo.swift @@ -0,0 +1,14 @@ +import Fluent + +struct CreateTodo: Migration { + func prepare(on database: Database) -> EventLoopFuture { + return database.schema(Todo.schema) + .id() + .field("title", .string, .required) + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + return database.schema(Todo.schema).delete() + } +} diff --git a/Sources/App/Migrations/CreateUser.swift b/Sources/App/Migrations/CreateUser.swift new file mode 100644 index 0000000..5f0c7e2 --- /dev/null +++ b/Sources/App/Migrations/CreateUser.swift @@ -0,0 +1,18 @@ +import Fluent +import Vapor + +struct CreateUser: Migration { + + func prepare(on database: Database) -> EventLoopFuture { + database.schema(User.schema) + .id() + .field("name", .string, .required) + .field("email", .string, .required) + .field("password_hash", .string, .required) + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + database.schema(User.schema).delete() + } +} \ No newline at end of file diff --git a/Sources/App/Migrations/CreateUserToken.swift b/Sources/App/Migrations/CreateUserToken.swift new file mode 100644 index 0000000..fb6ed6d --- /dev/null +++ b/Sources/App/Migrations/CreateUserToken.swift @@ -0,0 +1,22 @@ +import Fluent +import Vapor + +struct CreateUserToken: Migration { + + func prepare(on database: Database) -> EventLoopFuture { + database.schema("user_tokens") + .id() + .field("value", .string, .required) + .field("user_id", .uuid, .required) + .field("created_at", .string) + .field("updated_at", .string) + .field("expires_at", .string) + .unique(on: "value") + .foreignKey("user_id", references: "users", "id", onDelete: .cascade) // auto delete user_token when delete user + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + database.schema("user_tokens").delete() + } +} \ No newline at end of file diff --git a/Sources/App/Models/BusinessError.swift b/Sources/App/Models/BusinessError.swift new file mode 100644 index 0000000..a8d5ba1 --- /dev/null +++ b/Sources/App/Models/BusinessError.swift @@ -0,0 +1,52 @@ +import Vapor + +/// Usage: +/// ``` +/// throw BusinessError.userNotLoggedIn.abort() +/// ``` +/// +/// Then will print below output in terminal: +/// ``` +/// [ ERROR ] Abort.401: User is not logged in. [request-id: 38440B66-E235-4B84-A9F1-260E4EDA2BEC] (App/Controllers/EnvController.swift:35) +/// ``` + +enum BusinessError { + + case userNotLoggedIn + + case invalidEmail(String) +} + +extension BusinessError { + + func abort( + file: String = #file, + function: String = #function, + line: UInt = #line, + column: UInt = #column, + stackTrace: StackTrace? = .capture(skip: 1) + ) -> Abort { + switch self { + case .userNotLoggedIn: + return Abort( + .unauthorized, + reason: "User is not logged in.", + file: file, + function: function, + line: line, + column: column, + stackTrace: stackTrace + ) + case .invalidEmail(let email): + return Abort( + .badRequest, + reason: "Email address is not valid: \(email).", + file: file, + function: function, + line: line, + column: column, + stackTrace: stackTrace + ) + } + } +} diff --git a/Sources/App/Models/Env.swift b/Sources/App/Models/Env.swift new file mode 100644 index 0000000..40ab861 --- /dev/null +++ b/Sources/App/Models/Env.swift @@ -0,0 +1,43 @@ +import Vapor + + +enum EnvError: Error { + + case dotEnvDev( + reason: String, + file: String = #file, + function: String = #function, + line: UInt = #line, + column: UInt = #column/*, + stackTrace: StackTrace? = .capture(skip: 1)*/ + ) + + case dotEnvProd( + reason: String, + file: String = #file, + function: String = #function, + line: UInt = #line, + column: UInt = #column/*, + stackTrace: StackTrace? = .capture(skip: 1)*/ + ) + +} + + +extension Environment { + static var staging: Environment { + .custom(name: "staging") + } +} + +extension Environment { + static var dev_custom_name: Environment { + .custom(name: "development.custom_name") + } +} + +extension Environment { + static var prod_custom_name: Environment { + .custom(name: "production.custom_name") + } +} diff --git a/Sources/App/Models/Galaxy.swift b/Sources/App/Models/Galaxy.swift new file mode 100644 index 0000000..c0d090b --- /dev/null +++ b/Sources/App/Models/Galaxy.swift @@ -0,0 +1,25 @@ +import Fluent +import Vapor + + +final class Galaxy: Model, Content { + + static let schema = "galaxies" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + @Children(for: \.$galaxy) + var stars: [Star] + + init() {} + + init(id: UUID?, name: String) { + self.id = id + self.name = name + } + +} diff --git a/Sources/App/Models/Star.swift b/Sources/App/Models/Star.swift new file mode 100644 index 0000000..17e9f05 --- /dev/null +++ b/Sources/App/Models/Star.swift @@ -0,0 +1,26 @@ +import Fluent +import Vapor + + +final class Star: Model, Content { + + static let schema = "stars" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + @Parent(key: "galaxy_id") + var galaxy: Galaxy + + init() {} + + init(id: UUID?, name: String, galaxyID: UUID) { + self.id = id + self.name = name + self.$galaxy.id = galaxyID + } + +} diff --git a/Sources/App/Models/Todo.swift b/Sources/App/Models/Todo.swift new file mode 100644 index 0000000..6518db2 --- /dev/null +++ b/Sources/App/Models/Todo.swift @@ -0,0 +1,20 @@ +import Fluent +import Vapor + +final class Todo: Model, Content { + + static let schema = "todos" + + @ID(key: .id) + var id: UUID? + + @Field(key: "title") + var title: String + + init() { } + + init(id: UUID? = nil, title: String) { + self.id = id + self.title = title + } +} diff --git a/Sources/App/Models/User.swift b/Sources/App/Models/User.swift new file mode 100644 index 0000000..03a17b1 --- /dev/null +++ b/Sources/App/Models/User.swift @@ -0,0 +1,28 @@ +import Fluent +import Vapor + +final class User: Model, Content { + + static let schema = "users" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + @Field(key: "email") + var email: String + + @Field(key: "password_hash") + var passwordHash: String + + init() {} + + init(id: UUID? = nil, name: String, email: String, passwordHash: String) { + self.id = id + self.name = name + self.email = email + self.passwordHash = passwordHash + } +} \ No newline at end of file diff --git a/Sources/App/Models/UserToken.swift b/Sources/App/Models/UserToken.swift new file mode 100644 index 0000000..7addb5c --- /dev/null +++ b/Sources/App/Models/UserToken.swift @@ -0,0 +1,38 @@ +import Fluent +import Vapor + +final class UserToken: Model, Content { + static let schema = "user_tokens" + + @ID(key: .id) + var id: UUID? + + @Field(key: "value") + var value: String + + @Parent(key: "user_id") + var user: User + + @Timestamp(key: "created_at", on: .create, format: .iso8601(withMilliseconds: true)) + var createdAt: Date? + + @Timestamp(key: "updated_at", on: .update, format: .iso8601(withMilliseconds: true)) + var updatedAt: Date? + + @Timestamp(key: "expires_at", on: .delete, format: .iso8601(withMilliseconds: true)) + var expiresAt: Date? + + init() {} + + init(id: UUID? = nil, + value: String, + userID: User.IDValue, + // set token to expire after 5 hours + expiresAt: Date = Date.init(timeInterval: 60 * 60 * 5, since: .init())) { + self.id = id + self.value = value + self.$user.id = userID + self.expiresAt = expiresAt + } +} + diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift new file mode 100644 index 0000000..bd6ac4a --- /dev/null +++ b/Sources/App/configure.swift @@ -0,0 +1,105 @@ +import Fluent +import FluentPostgresDriver +import Vapor + + +// configures your application +public func configure(_ app: Application) throws { + + // Always capture stack traces, regardless of log level. + StackTrace.isCaptureEnabled = true + + // https://docs.vapor.codes/4.0/passwords/#bcrypt + app.passwords.use(.bcrypt) + + try cfgHttpServer(app) + try cfgMiddleware(app) + try cfgDatabase(app) + + // register routes + try routes(app) +} + +private func cfgHttpServer(_ app: Application) throws { + let envName = app.environment.name + + guard let hostname = Environment.process.HOST_NAME else { + throw EnvError.dotEnvDev(reason: "Please configurate `HOST_NAME=XXX` in file('.env.\(envName)') !") + } + + guard let port = Environment.process.HOST_PORT else { + throw EnvError.dotEnvDev(reason: "Please configurate `HOST_PORT=XXX` in file('.env.\(envName)') !") + } + + app.http.server.configuration.hostname = hostname + app.http.server.configuration.port = Int(port) ?? 8080 + + // Enable TLS. + // ----------- + // Disable HTTP/1 support. + /* + app.http.server.configuration.supportVersions = [.two] + try app.http.server.configuration.tlsConfiguration = .forServer( + certificateChain: [ + .certificate(.init( + file: "/path/to/cert.pem", + format: .pem + )) + ], + privateKey: .file("/path/to/key.pem") + ) + */ +} + +private func cfgMiddleware(_ app: Application) throws { // Clear any existing middleware. + app.middleware = .init() + + app.middleware.use(CORSMiddleware(configuration: CORSMiddleware.Configuration( + allowedOrigin: .all, + allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH], + allowedHeaders: [.accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin] + ))) + + app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) + + app.sessions.use(.fluent) + app.middleware.use(app.sessions.middleware) + + app.middleware.use(ErrorMiddleware.default(environment: app.environment)) +} + +private func cfgDatabase(_ app: Application) throws { + let envName = app.environment.name + + guard let dbHostName = Environment.process.DATABASE_HOST else { + throw EnvError.dotEnvDev(reason: "Please configurate `DATABASE_HOST=XXX` in file('.env.\(envName)') !") + } + + guard let dbName = Environment.process.DATABASE_NAME else { + throw EnvError.dotEnvDev(reason: "Please configurate `DATABASE_NAME=XXX` in file('.env.\(envName)') !") + } + + guard let dbUserName = Environment.process.DATABASE_USERNAME else { + throw EnvError.dotEnvProd(reason: "Please `export DATABASE_USERNAME=XXX` in the terminal !") + } + + guard let dbPassword = Environment.process.DATABASE_PASSWORD else { + throw EnvError.dotEnvProd(reason: "Please `export DATABASE_PASSWORD=XXX` in the terminal !") + } + + app.databases.use( + .postgres( + hostname: dbHostName, + username: dbUserName, + password: dbPassword, + database: dbName + ), + as: .psql) + + app.migrations.add(SessionRecord.migration) + app.migrations.add(CreateTodo()) + app.migrations.add(CreateGalaxy()) + app.migrations.add(CreateStar()) + app.migrations.add(CreateUser()) + app.migrations.add(CreateUserToken()) +} diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift new file mode 100644 index 0000000..f0031c3 --- /dev/null +++ b/Sources/App/routes.swift @@ -0,0 +1,18 @@ +import Fluent +import Vapor + +func routes(_ app: Application) throws { + app.get { req in + return "It works!" + } + + app.get("hello") { req -> String in + return "Hello, world!" + } + + try app.register(collection: EnvController()) + try app.register(collection: UserController()) + try app.register(collection: TodoController()) + try app.register(collection: GalaxyController()) + try app.register(collection: StarController()) +} diff --git a/Sources/Run/main.swift b/Sources/Run/main.swift new file mode 100644 index 0000000..02515c1 --- /dev/null +++ b/Sources/Run/main.swift @@ -0,0 +1,19 @@ +import App +import Vapor + +// vapor run serve --env development.custom_name --log debug +// vapor run serve --env production.custom_name --log info + +// vapor run serve --env development +// vapor run serve --env production +// vapor run serve --env staging +// vapor run serve --env testing + +var env = try Environment.detect() +try LoggingSystem.bootstrap(from: &env) +let app = Application(env) +defer { + app.shutdown() +} +try configure(app) +try app.run() diff --git a/Tests/AppRestApiTests/Controllers/GalaxyController.http b/Tests/AppRestApiTests/Controllers/GalaxyController.http new file mode 100644 index 0000000..9dad2ed --- /dev/null +++ b/Tests/AppRestApiTests/Controllers/GalaxyController.http @@ -0,0 +1,17 @@ +GET http://{{host}}/galaxies HTTP/1.1 +Accept: application/json + +### + +POST http://{{host}}/galaxies HTTP/1.1 +#content-length: 21 +content-type: application/json + +{ + "name": "Milky Way" +} + +### + + + diff --git a/Tests/AppRestApiTests/Controllers/StarController.http b/Tests/AppRestApiTests/Controllers/StarController.http new file mode 100644 index 0000000..bbb9c04 --- /dev/null +++ b/Tests/AppRestApiTests/Controllers/StarController.http @@ -0,0 +1,33 @@ +### +POST http://{{host}}/galaxies HTTP/1.1 +content-type: application/json + +{ + "name": "银河系" +} + +> {% +client.test("Request executed successfully", function() { + client.assert(response.status === 200, "Response status is not 200"); + client.log("\n"); + client.log("galaxy.id = " + response.body.id); + client.log("\n\n"); +}); + +client.global.set("galaxy_for_star", response.body.id); +%} + + + +### +POST http://{{host}}/stars HTTP/1.1 +content-type: application/json + +{ + "name": "Sun", + "galaxy": { + "id": "{{galaxy_for_star}}" + } +} + +### \ No newline at end of file diff --git a/Tests/AppRestApiTests/Controllers/TodoController.http b/Tests/AppRestApiTests/Controllers/TodoController.http new file mode 100644 index 0000000..a8f624f --- /dev/null +++ b/Tests/AppRestApiTests/Controllers/TodoController.http @@ -0,0 +1,29 @@ +POST http://{{host}}/todos HTTP/1.1 +content-type: application/json +Authorization: Bearer {{userToken}} + +{ + "title": "todo_thing_001" +} + +> {% +client.test("Request executed successfully", function() { + client.assert(response.status === 200, "Response status is not 200"); +}); + +client.global.set("user_id_created", response.body.id); +%} + +### + + +GET http://{{host}}/todos HTTP/1.1 +Authorization: Bearer {{userToken}} + +### + + +DELETE http://{{host}}/todos/{{user_id_created}} HTTP/1.1 +Authorization: Bearer {{userToken}} + +### diff --git a/Tests/AppRestApiTests/Controllers/UserController.http b/Tests/AppRestApiTests/Controllers/UserController.http new file mode 100644 index 0000000..1ff0618 --- /dev/null +++ b/Tests/AppRestApiTests/Controllers/UserController.http @@ -0,0 +1,84 @@ +POST http://{{host}}/users HTTP/1.1 +content-type: application/json + +{ + "name": "Vapor", + "email": "test@vapor.codes", + "password": "secret", + "confirmPassword": "secret" +} + +### + +POST http://{{host}}/users HTTP/1.1 +content-type: application/json + +{ + "name": "Vapor_002", + "email": "test_002@vapor.codes", + "password": "secret", + "confirmPassword": "secret" +} + +### + +POST http://{{host}}/login HTTP/1.1 +# echo -n "test@vapor.codes:secret" | base64 +Authorization: Basic dGVzdEB2YXBvci5jb2RlczpzZWNyZXQ= + +> {% +client.test("Request executed successfully", function() { + client.assert(response.status === 200, "Response status is not 200"); + client.log("\n"); + client.log("userToken = " + response.body.value); + client.log("\n\n"); +}); + +client.global.set("userToken", response.body.value); +%} + +### + + +#POST http://{{host}}/login HTTP/1.1 +## echo -n "test_002@vapor.codes:secret" | base64 +#Authorization: Basic dGVzdF8wMDJAdmFwb3IuY29kZXM6c2VjcmV0 +# +#> {% +#client.test("Request executed successfully", function() { +# client.assert(response.status === 200, "Response status is not 200"); +# client.log("\n"); +# client.log("userToken = " + response.body.value); +# client.log("\n\n"); +#}); +# +#client.global.set("userToken", response.body.value); +#%} + +### + +GET http://{{host}}/me HTTP/1.1 +Authorization: Bearer {{userToken}} + +### + +POST http://{{host}}/login HTTP/1.1 +Authorization: Bearer {{userToken}} + +> {% +client.test("Request executed successfully", function() { + client.assert(response.status === 200, "Response status is not 200"); + client.log("\n"); + client.log("userToken = " + response.body.value); + client.log("\n\n"); +}); + +client.global.set("userToken", response.body.value); +%} + +### + +POST http://{{host}}/logout HTTP/1.1 +Authorization: Bearer {{userToken}} + +### diff --git a/Tests/AppRestApiTests/README.md b/Tests/AppRestApiTests/README.md new file mode 100644 index 0000000..0fb37b2 --- /dev/null +++ b/Tests/AppRestApiTests/README.md @@ -0,0 +1,34 @@ + + +## 术语表 + +- 环境 + + 名称 | 含义 + --- | ---- + dev | 开发环境 + uat | 测试环境 + pro | 正式环境 + + +## 文件 + +- rest-client.env.json + + 配置环境变量 + + +- rest-client.env.json + + 配置私密敏感的环境变量, 如: 用户名和密码 + + 不受版本控制, 请在 `.gitignore` 里忽略它. + + +## 使用教程 + +https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html#using-response-handler-scripts + +https://liqiang.io/post/http-debug-tools-with-jetbrains-712742e4 + +https://blog.csdn.net/BodyandSoul/article/details/103255866 \ No newline at end of file diff --git a/Tests/AppRestApiTests/http-request.http b/Tests/AppRestApiTests/http-request.http new file mode 100644 index 0000000..a85a6ee --- /dev/null +++ b/Tests/AppRestApiTests/http-request.http @@ -0,0 +1,23 @@ +GET http://{{host}} + +> {% +client.test("Baseline", function () { + client.assert(response.status == 200, '状态码为非200'); + client.log(response.headers) + client.log(response.body) + client.log(response.body.msg) +}); + +/** + * client.global.set 后,可以被后续请求通过{{demo}}获取该变量值,以实现更多连贯操作 + */ +client.global.set('demo', 'test'); +%} + +### + + + + + + diff --git a/Tests/AppRestApiTests/rest-client.env.json b/Tests/AppRestApiTests/rest-client.env.json new file mode 100644 index 0000000..e700167 --- /dev/null +++ b/Tests/AppRestApiTests/rest-client.env.json @@ -0,0 +1,11 @@ +{ + "dev": { + "host": "localhost:8080" + }, + "uat": { + "host": "localhost:8080" + }, + "prod": { + "host": "www.google.com" + } +} \ No newline at end of file diff --git a/Tests/AppRestApiTests/rest-client.private.env.json b/Tests/AppRestApiTests/rest-client.private.env.json new file mode 100644 index 0000000..63dace7 --- /dev/null +++ b/Tests/AppRestApiTests/rest-client.private.env.json @@ -0,0 +1,14 @@ +{ + "dev": { + "loginName": "***", + "password": "***" + }, + "uat": { + "loginName": "***", + "password": "***" + }, + "prod": { + "loginName": "***", + "password": "***" + } +} \ No newline at end of file diff --git a/Tests/AppRestApiTests/scripts/my-script.js b/Tests/AppRestApiTests/scripts/my-script.js new file mode 100644 index 0000000..e69de29 diff --git a/Tests/AppTests/AppTests.swift b/Tests/AppTests/AppTests.swift new file mode 100644 index 0000000..56f7a38 --- /dev/null +++ b/Tests/AppTests/AppTests.swift @@ -0,0 +1,19 @@ +@testable import App +import XCTVapor + +final class AppTests: XCTestCase { + func testHelloWorld() throws { + let app = Application(.testing) + defer { app.shutdown() } + try configure(app) + + let testEnv = try app.testable(method: .inMemory) + //let testEnv = try app.testable(method: .running) + //let testEnv = try app.testable(method: .running(port: 8123)) + + try testEnv.test(.GET, "hello") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "Hello, world!") + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..776fc01 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,74 @@ +# Docker Compose file for Vapor +# +# Install Docker on your system to run and test +# your Vapor app in a production-like environment. +# +# Note: This file is intended for testing and does not +# implement best practices for a production deployment. +# +# Learn more: https://docs.docker.com/compose/reference/ +# +# Build images: docker-compose build +# Start app: docker-compose up app +# Start database: docker-compose up db +# Run migrations: docker-compose up migrate +# Stop all: docker-compose down (add -v to wipe db) +# +version: '3.7' + +volumes: + db_data: + +x-shared_environment: &shared_environment + LOG_LEVEL: ${LOG_LEVEL:-debug} + DATABASE_HOST: db + DATABASE_NAME: vapor_database + DATABASE_USERNAME: vapor_username + DATABASE_PASSWORD: vapor_password + +services: + app: + image: demo_swift_server_vapor_auth:latest + build: + context: . + environment: + <<: *shared_environment + depends_on: + - db + ports: + - '8080:8080' +# user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user. + command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] + migrate: + image: demo_swift_server_vapor_auth:latest + build: + context: . + environment: + <<: *shared_environment + depends_on: + - db + command: ["migrate", "--yes"] + deploy: + replicas: 0 + revert: + image: demo_swift_server_vapor_auth:latest + build: + context: . + environment: + <<: *shared_environment + depends_on: + - db + command: ["migrate", "--revert", "--yes"] + deploy: + replicas: 0 + db: + image: postgres:12-alpine + volumes: + - db_data:/var/lib/postgresql/data/pgdata + environment: + PGDATA: /var/lib/postgresql/data/pgdata + POSTGRES_USER: vapor_username + POSTGRES_PASSWORD: vapor_password + POSTGRES_DB: vapor_database + ports: + - '5432:5432' diff --git a/etc/nginx/cert.key b/etc/nginx/cert.key new file mode 100644 index 0000000..be961bf --- /dev/null +++ b/etc/nginx/cert.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJX2ojt17ii/1f +yn4PA4lNuFpiymdh9VCMbB1SjwyqrDYXzz6f/s92BSS62VW3rCjQROdmw3wOjTna +VWTnZXFwrZxOqP+5vBjDzz3wgzFX79oV97yJUvkGHB95vRHU5rlfgihtXZ8I8f94 +57Hr64UJCtjt4AGehg2RCemZJRE6YD+ZoHi1YapRAT9ifJiOFBuL/hV5rkFNrb1F +XfOR8dOBCF723uiynEZGarof6EliydgzUcL4JL+M6iQvlBQ5a7r4uAUfVDv7y6wJ +fD+5+2i3y4XbATDSuTgE5PRHAoBN3RGwkKbej+V+2u+jXQx3jIAWm8hTbWO9etX8 +Jg796csrAgMBAAECggEAcFubplgPSkqf0k3yrj/j4Yimbg7PRloRWniMl98XSzeR +axGIuM79RLWz+8l+SWB22VZI4w7Z7migShFxnqb8d8l2uHEVVrX7/21n+wXIh29d ++PnkqZ6xMdaJPWzgQTGHZyIBmpwtO1mDmRt2K9OaxCPYTEm7vY7AjRfBzmBfT3kc +CsVBTVXoEAaTsfyuKUVplpqO5NzIf24/pSYoOVqZoe+yV+qV9lb6/+OixL8EhnYW +fZLBL5MvjCgfEvm9mpSD3YSprhM+fMXvPd1tIcamZFu8RQLib+e6/FIgBgu44MgO +uXOCZulZPMM5ywidOCA2nGhMHdHct5NPzkbu6+864QKBgQD6fb4ozBbb04AL2aWg +kv7myYx5gAppuVzxM/jbSSQj+5/q1+/D2bu4k1TAhIj7wiyF1wpn7iTKIYmHHev2 +8TUurgwl6QY/BnLfdt0YByuu521DuVMcgxr+M+g8Ik9RXq+ruMJ9rCr4CGYqeyd+ +R/UlG4nybQGy5Kx0SJVL1pgzuwKBgQDNzSLsJu7nV/8mBNmgwfAaQt+Rb5l8PqtE +wLYl54q6yB+xG6FYKCtOYoI15UK+sA+lWdOj6mNuz86PbHO1f9AfW7b5UCjIG96c ++31edvZ0DrPuKH2hXZVmBr70lmU+UZFU9Zkxi8E7k9B/lLEFRas7jny2vQ2ExgQE +/C4a4Aj3UQKBgEXnK1cVRVfJWRV1gowiGXJsUoBhpAZHDVHekBcZ/TTxdnpCEo4U +SBfFPcSP9+5SDLy4+xVv1uJ1o3yKdBwwygeh2JAHzI9wDESiVibrcVxZBxv8Lt5p +E/JWp/uIE5yAkQOaBuStVDxtk51NSwMpr5bQbZmyo+O5fwrgapGEbGsbAoGAVKXn +k1KAOU+MdsrP6LtcUrDm3B8W9m+gamsJgJW4DhN6VZKTznrXNSp3fEuMJHquNMi5 +sFYcB9wZjgyBm0mzBPLoU6uQ2p379n3Klyw/OoVBpNeqtNCVPp3PtTKW+gb6zAMo +u6RMGoE15QP3u79oKtr0mQUxpkb8KEDBhfRYO8ECgYEArlJ68drPJkWG+yDBynP1 +mrM80t3mDH4uUNnCVp9M/YI4TaxEY2ClJyBMYOPdiWWlj/t4x0fwinm8DpsQZ83c +mKOVdyg9z0GA78rZXurwGCRZ46ZYvKbFKQoczxny0N2NQc4+1ucDM+UDdKrF5dZF +eBcuvSke9K9wAz5zF8IB8Yw= +-----END PRIVATE KEY----- diff --git a/etc/nginx/cert.pem b/etc/nginx/cert.pem new file mode 100644 index 0000000..ada266a --- /dev/null +++ b/etc/nginx/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbjCCAlYCCQD/w0Nj8sG2HzANBgkqhkiG9w0BAQsFADB5MQswCQYDVQQGEwJD +TjEQMA4GA1UECAwHQmVpSmluZzEQMA4GA1UEBwwHQmVpSmluZzEMMAoGA1UECgwD +R0NEMQwwCgYDVQQLDANHQ0QxDDAKBgNVBAMMA0dDRDEcMBoGCSqGSIb3DQEJARYN +R0NEQGdtYWlsLmNvbTAeFw0yMDA3MTYxMDM3NTVaFw0zMDA3MTQxMDM3NTVaMHkx +CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlKaW5nMRAwDgYDVQQHDAdCZWlKaW5n +MQwwCgYDVQQKDANHQ0QxDDAKBgNVBAsMA0dDRDEMMAoGA1UEAwwDR0NEMRwwGgYJ +KoZIhvcNAQkBFg1HQ0RAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAyV9qI7de4ov9X8p+DwOJTbhaYspnYfVQjGwdUo8Mqqw2F88+n/7P +dgUkutlVt6wo0ETnZsN8Do052lVk52VxcK2cTqj/ubwYw8898IMxV+/aFfe8iVL5 +Bhwfeb0R1Oa5X4IobV2fCPH/eOex6+uFCQrY7eABnoYNkQnpmSUROmA/maB4tWGq +UQE/YnyYjhQbi/4Vea5BTa29RV3zkfHTgQhe9t7ospxGRmq6H+hJYsnYM1HC+CS/ +jOokL5QUOWu6+LgFH1Q7+8usCXw/uftot8uF2wEw0rk4BOT0RwKATd0RsJCm3o/l +ftrvo10Md4yAFpvIU21jvXrV/CYO/enLKwIDAQABMA0GCSqGSIb3DQEBCwUAA4IB +AQBNb48WpnG0pFqwETpXZgbycZZ8lPbQLvkGvhkQajYR9aDdMf75J4tGpHwPbDye +pR5oVfYDSjux2wLT5H0rCflFYr/aop5QOvUxQZHKeGmFLY+JiDw6PuW8+imm987t +ZNoNt2802FVrb8vCo64EDAw+OukhPGr6KM5iptntATmkGajXoKEKwB1of94reeS6 +i99VkULisaXDszpRaHvERN8W6BheTq1pjpY4A6t/4XNDOOjVs/idkV6Mc/xeKjDt +XkTP8VkfHTxzCPWZhB2rDrUJSz1E7Cf/5pSXoyMEpzCOup/w8ivcsz27AYxWEKCN +N+ZMFfly7iDg4JKlr5Jxs7Vl +-----END CERTIFICATE----- diff --git a/etc/nginx/fastcgi.conf b/etc/nginx/fastcgi.conf new file mode 100644 index 0000000..091738c --- /dev/null +++ b/etc/nginx/fastcgi.conf @@ -0,0 +1,26 @@ + +fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; diff --git a/etc/nginx/fastcgi.conf.default b/etc/nginx/fastcgi.conf.default new file mode 100644 index 0000000..091738c --- /dev/null +++ b/etc/nginx/fastcgi.conf.default @@ -0,0 +1,26 @@ + +fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; diff --git a/etc/nginx/fastcgi_params b/etc/nginx/fastcgi_params new file mode 100644 index 0000000..28decb9 --- /dev/null +++ b/etc/nginx/fastcgi_params @@ -0,0 +1,25 @@ + +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; diff --git a/etc/nginx/fastcgi_params.default b/etc/nginx/fastcgi_params.default new file mode 100644 index 0000000..28decb9 --- /dev/null +++ b/etc/nginx/fastcgi_params.default @@ -0,0 +1,25 @@ + +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; diff --git a/etc/nginx/koi-utf b/etc/nginx/koi-utf new file mode 100644 index 0000000..e7974ff --- /dev/null +++ b/etc/nginx/koi-utf @@ -0,0 +1,109 @@ + +# This map is not a full koi8-r <> utf8 map: it does not contain +# box-drawing and some other characters. Besides this map contains +# several koi8-u and Byelorussian letters which are not in koi8-r. +# If you need a full and standard map, use contrib/unicode2nginx/koi-utf +# map instead. + +charset_map koi8-r utf-8 { + + 80 E282AC ; # euro + + 95 E280A2 ; # bullet + + 9A C2A0 ; #   + + 9E C2B7 ; # · + + A3 D191 ; # small yo + A4 D194 ; # small Ukrainian ye + + A6 D196 ; # small Ukrainian i + A7 D197 ; # small Ukrainian yi + + AD D291 ; # small Ukrainian soft g + AE D19E ; # small Byelorussian short u + + B0 C2B0 ; # ° + + B3 D081 ; # capital YO + B4 D084 ; # capital Ukrainian YE + + B6 D086 ; # capital Ukrainian I + B7 D087 ; # capital Ukrainian YI + + B9 E28496 ; # numero sign + + BD D290 ; # capital Ukrainian soft G + BE D18E ; # capital Byelorussian short U + + BF C2A9 ; # (C) + + C0 D18E ; # small yu + C1 D0B0 ; # small a + C2 D0B1 ; # small b + C3 D186 ; # small ts + C4 D0B4 ; # small d + C5 D0B5 ; # small ye + C6 D184 ; # small f + C7 D0B3 ; # small g + C8 D185 ; # small kh + C9 D0B8 ; # small i + CA D0B9 ; # small j + CB D0BA ; # small k + CC D0BB ; # small l + CD D0BC ; # small m + CE D0BD ; # small n + CF D0BE ; # small o + + D0 D0BF ; # small p + D1 D18F ; # small ya + D2 D180 ; # small r + D3 D181 ; # small s + D4 D182 ; # small t + D5 D183 ; # small u + D6 D0B6 ; # small zh + D7 D0B2 ; # small v + D8 D18C ; # small soft sign + D9 D18B ; # small y + DA D0B7 ; # small z + DB D188 ; # small sh + DC D18D ; # small e + DD D189 ; # small shch + DE D187 ; # small ch + DF D18A ; # small hard sign + + E0 D0AE ; # capital YU + E1 D090 ; # capital A + E2 D091 ; # capital B + E3 D0A6 ; # capital TS + E4 D094 ; # capital D + E5 D095 ; # capital YE + E6 D0A4 ; # capital F + E7 D093 ; # capital G + E8 D0A5 ; # capital KH + E9 D098 ; # capital I + EA D099 ; # capital J + EB D09A ; # capital K + EC D09B ; # capital L + ED D09C ; # capital M + EE D09D ; # capital N + EF D09E ; # capital O + + F0 D09F ; # capital P + F1 D0AF ; # capital YA + F2 D0A0 ; # capital R + F3 D0A1 ; # capital S + F4 D0A2 ; # capital T + F5 D0A3 ; # capital U + F6 D096 ; # capital ZH + F7 D092 ; # capital V + F8 D0AC ; # capital soft sign + F9 D0AB ; # capital Y + FA D097 ; # capital Z + FB D0A8 ; # capital SH + FC D0AD ; # capital E + FD D0A9 ; # capital SHCH + FE D0A7 ; # capital CH + FF D0AA ; # capital hard sign +} diff --git a/etc/nginx/koi-win b/etc/nginx/koi-win new file mode 100644 index 0000000..72afabe --- /dev/null +++ b/etc/nginx/koi-win @@ -0,0 +1,103 @@ + +charset_map koi8-r windows-1251 { + + 80 88 ; # euro + + 95 95 ; # bullet + + 9A A0 ; #   + + 9E B7 ; # · + + A3 B8 ; # small yo + A4 BA ; # small Ukrainian ye + + A6 B3 ; # small Ukrainian i + A7 BF ; # small Ukrainian yi + + AD B4 ; # small Ukrainian soft g + AE A2 ; # small Byelorussian short u + + B0 B0 ; # ° + + B3 A8 ; # capital YO + B4 AA ; # capital Ukrainian YE + + B6 B2 ; # capital Ukrainian I + B7 AF ; # capital Ukrainian YI + + B9 B9 ; # numero sign + + BD A5 ; # capital Ukrainian soft G + BE A1 ; # capital Byelorussian short U + + BF A9 ; # (C) + + C0 FE ; # small yu + C1 E0 ; # small a + C2 E1 ; # small b + C3 F6 ; # small ts + C4 E4 ; # small d + C5 E5 ; # small ye + C6 F4 ; # small f + C7 E3 ; # small g + C8 F5 ; # small kh + C9 E8 ; # small i + CA E9 ; # small j + CB EA ; # small k + CC EB ; # small l + CD EC ; # small m + CE ED ; # small n + CF EE ; # small o + + D0 EF ; # small p + D1 FF ; # small ya + D2 F0 ; # small r + D3 F1 ; # small s + D4 F2 ; # small t + D5 F3 ; # small u + D6 E6 ; # small zh + D7 E2 ; # small v + D8 FC ; # small soft sign + D9 FB ; # small y + DA E7 ; # small z + DB F8 ; # small sh + DC FD ; # small e + DD F9 ; # small shch + DE F7 ; # small ch + DF FA ; # small hard sign + + E0 DE ; # capital YU + E1 C0 ; # capital A + E2 C1 ; # capital B + E3 D6 ; # capital TS + E4 C4 ; # capital D + E5 C5 ; # capital YE + E6 D4 ; # capital F + E7 C3 ; # capital G + E8 D5 ; # capital KH + E9 C8 ; # capital I + EA C9 ; # capital J + EB CA ; # capital K + EC CB ; # capital L + ED CC ; # capital M + EE CD ; # capital N + EF CE ; # capital O + + F0 CF ; # capital P + F1 DF ; # capital YA + F2 D0 ; # capital R + F3 D1 ; # capital S + F4 D2 ; # capital T + F5 D3 ; # capital U + F6 C6 ; # capital ZH + F7 C2 ; # capital V + F8 DC ; # capital soft sign + F9 DB ; # capital Y + FA C7 ; # capital Z + FB D8 ; # capital SH + FC DD ; # capital E + FD D9 ; # capital SHCH + FE D7 ; # capital CH + FF DA ; # capital hard sign +} diff --git a/etc/nginx/mime.types b/etc/nginx/mime.types new file mode 100644 index 0000000..2961256 --- /dev/null +++ b/etc/nginx/mime.types @@ -0,0 +1,97 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/etc/nginx/mime.types.default b/etc/nginx/mime.types.default new file mode 100644 index 0000000..2961256 --- /dev/null +++ b/etc/nginx/mime.types.default @@ -0,0 +1,97 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/etc/nginx/nginx.conf b/etc/nginx/nginx.conf new file mode 100644 index 0000000..a93e94a --- /dev/null +++ b/etc/nginx/nginx.conf @@ -0,0 +1,156 @@ + +#user nobody; +worker_processes 1; + +#error_log logs/error.log; +#error_log logs/error.log notice; +#error_log logs/error.log info; + +#pid logs/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include mime.types; + default_type application/octet-stream; + + #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + # '$status $body_bytes_sent "$http_referer" ' + # '"$http_user_agent" "$http_x_forwarded_for"'; + + #access_log logs/access.log main; + + sendfile on; + #tcp_nopush on; + + #keepalive_timeout 0; + keepalive_timeout 65; + + #gzip on; + + server { + listen 8080; + server_name localhost; + + #charset koi8-r; + + #access_log logs/host.access.log main; + + location / { + root html; + index index.html index.htm; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + + # proxy the PHP scripts to Apache listening on 127.0.0.1:80 + # + #location ~ \.php$ { + # proxy_pass http://127.0.0.1; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # root html; + # fastcgi_pass 127.0.0.1:9000; + # fastcgi_index index.php; + # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} + } + + + # another virtual host using mix of IP-, name-, and port-based configuration + # + #server { + # listen 8000; + # listen somename:8080; + # server_name somename alias another.alias; + + # location / { + # root html; + # index index.html index.htm; + # } + #} + + + # HTTPS server + # + #server { + # listen 443 ssl; + # server_name localhost; + + # ssl_certificate cert.pem; + # ssl_certificate_key cert.key; + + # ssl_session_cache shared:SSL:1m; + # ssl_session_timeout 5m; + + # ssl_ciphers HIGH:!aNULL:!MD5; + # ssl_prefer_server_ciphers on; + + # location / { + # root html; + # index index.html index.htm; + # } + #} + + server { + listen 443 ssl; + server_name localhost; + + ssl_certificate cert.pem; + ssl_certificate_key cert.key; + + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 1d; + + #ssl_ciphers HIGH:!aNULL:!MD5; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + ssl_prefer_server_ciphers on; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_trusted_certificate cert.pem; + ssl_stapling on; + ssl_stapling_verify on; + add_header Strict-Transport-Security max-age=15768000; + + root ~/dev_kit/sdk/swift_source/readdle/demo_swift_server_vapor_auth/Public/; + + location / { + try_files $uri @proxy; + } + + location @proxy { + proxy_pass http://127.0.0.1:8080; + proxy_pass_header Server; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_header Server; + proxy_connect_timeout 3s; + proxy_read_timeout 10s; + } + } + + include servers/*; +} diff --git a/etc/nginx/nginx.conf.default b/etc/nginx/nginx.conf.default new file mode 100644 index 0000000..f9ebe57 --- /dev/null +++ b/etc/nginx/nginx.conf.default @@ -0,0 +1,117 @@ + +#user nobody; +worker_processes 1; + +#error_log logs/error.log; +#error_log logs/error.log notice; +#error_log logs/error.log info; + +#pid logs/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include mime.types; + default_type application/octet-stream; + + #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + # '$status $body_bytes_sent "$http_referer" ' + # '"$http_user_agent" "$http_x_forwarded_for"'; + + #access_log logs/access.log main; + + sendfile on; + #tcp_nopush on; + + #keepalive_timeout 0; + keepalive_timeout 65; + + #gzip on; + + server { + listen 8080; + server_name localhost; + + #charset koi8-r; + + #access_log logs/host.access.log main; + + location / { + root html; + index index.html index.htm; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + + # proxy the PHP scripts to Apache listening on 127.0.0.1:80 + # + #location ~ \.php$ { + # proxy_pass http://127.0.0.1; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # root html; + # fastcgi_pass 127.0.0.1:9000; + # fastcgi_index index.php; + # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} + } + + + # another virtual host using mix of IP-, name-, and port-based configuration + # + #server { + # listen 8000; + # listen somename:8080; + # server_name somename alias another.alias; + + # location / { + # root html; + # index index.html index.htm; + # } + #} + + + # HTTPS server + # + #server { + # listen 443 ssl; + # server_name localhost; + + # ssl_certificate cert.pem; + # ssl_certificate_key cert.key; + + # ssl_session_cache shared:SSL:1m; + # ssl_session_timeout 5m; + + # ssl_ciphers HIGH:!aNULL:!MD5; + # ssl_prefer_server_ciphers on; + + # location / { + # root html; + # index index.html index.htm; + # } + #} + include servers/*; +} diff --git a/etc/nginx/scgi_params b/etc/nginx/scgi_params new file mode 100644 index 0000000..6d4ce4f --- /dev/null +++ b/etc/nginx/scgi_params @@ -0,0 +1,17 @@ + +scgi_param REQUEST_METHOD $request_method; +scgi_param REQUEST_URI $request_uri; +scgi_param QUERY_STRING $query_string; +scgi_param CONTENT_TYPE $content_type; + +scgi_param DOCUMENT_URI $document_uri; +scgi_param DOCUMENT_ROOT $document_root; +scgi_param SCGI 1; +scgi_param SERVER_PROTOCOL $server_protocol; +scgi_param REQUEST_SCHEME $scheme; +scgi_param HTTPS $https if_not_empty; + +scgi_param REMOTE_ADDR $remote_addr; +scgi_param REMOTE_PORT $remote_port; +scgi_param SERVER_PORT $server_port; +scgi_param SERVER_NAME $server_name; diff --git a/etc/nginx/scgi_params.default b/etc/nginx/scgi_params.default new file mode 100644 index 0000000..6d4ce4f --- /dev/null +++ b/etc/nginx/scgi_params.default @@ -0,0 +1,17 @@ + +scgi_param REQUEST_METHOD $request_method; +scgi_param REQUEST_URI $request_uri; +scgi_param QUERY_STRING $query_string; +scgi_param CONTENT_TYPE $content_type; + +scgi_param DOCUMENT_URI $document_uri; +scgi_param DOCUMENT_ROOT $document_root; +scgi_param SCGI 1; +scgi_param SERVER_PROTOCOL $server_protocol; +scgi_param REQUEST_SCHEME $scheme; +scgi_param HTTPS $https if_not_empty; + +scgi_param REMOTE_ADDR $remote_addr; +scgi_param REMOTE_PORT $remote_port; +scgi_param SERVER_PORT $server_port; +scgi_param SERVER_NAME $server_name; diff --git a/etc/nginx/uwsgi_params b/etc/nginx/uwsgi_params new file mode 100644 index 0000000..09c732c --- /dev/null +++ b/etc/nginx/uwsgi_params @@ -0,0 +1,17 @@ + +uwsgi_param QUERY_STRING $query_string; +uwsgi_param REQUEST_METHOD $request_method; +uwsgi_param CONTENT_TYPE $content_type; +uwsgi_param CONTENT_LENGTH $content_length; + +uwsgi_param REQUEST_URI $request_uri; +uwsgi_param PATH_INFO $document_uri; +uwsgi_param DOCUMENT_ROOT $document_root; +uwsgi_param SERVER_PROTOCOL $server_protocol; +uwsgi_param REQUEST_SCHEME $scheme; +uwsgi_param HTTPS $https if_not_empty; + +uwsgi_param REMOTE_ADDR $remote_addr; +uwsgi_param REMOTE_PORT $remote_port; +uwsgi_param SERVER_PORT $server_port; +uwsgi_param SERVER_NAME $server_name; diff --git a/etc/nginx/uwsgi_params.default b/etc/nginx/uwsgi_params.default new file mode 100644 index 0000000..09c732c --- /dev/null +++ b/etc/nginx/uwsgi_params.default @@ -0,0 +1,17 @@ + +uwsgi_param QUERY_STRING $query_string; +uwsgi_param REQUEST_METHOD $request_method; +uwsgi_param CONTENT_TYPE $content_type; +uwsgi_param CONTENT_LENGTH $content_length; + +uwsgi_param REQUEST_URI $request_uri; +uwsgi_param PATH_INFO $document_uri; +uwsgi_param DOCUMENT_ROOT $document_root; +uwsgi_param SERVER_PROTOCOL $server_protocol; +uwsgi_param REQUEST_SCHEME $scheme; +uwsgi_param HTTPS $https if_not_empty; + +uwsgi_param REMOTE_ADDR $remote_addr; +uwsgi_param REMOTE_PORT $remote_port; +uwsgi_param SERVER_PORT $server_port; +uwsgi_param SERVER_NAME $server_name; diff --git a/etc/nginx/win-utf b/etc/nginx/win-utf new file mode 100644 index 0000000..ed8bc00 --- /dev/null +++ b/etc/nginx/win-utf @@ -0,0 +1,126 @@ + +# This map is not a full windows-1251 <> utf8 map: it does not +# contain Serbian and Macedonian letters. If you need a full map, +# use contrib/unicode2nginx/win-utf map instead. + +charset_map windows-1251 utf-8 { + + 82 E2809A ; # single low-9 quotation mark + + 84 E2809E ; # double low-9 quotation mark + 85 E280A6 ; # ellipsis + 86 E280A0 ; # dagger + 87 E280A1 ; # double dagger + 88 E282AC ; # euro + 89 E280B0 ; # per mille + + 91 E28098 ; # left single quotation mark + 92 E28099 ; # right single quotation mark + 93 E2809C ; # left double quotation mark + 94 E2809D ; # right double quotation mark + 95 E280A2 ; # bullet + 96 E28093 ; # en dash + 97 E28094 ; # em dash + + 99 E284A2 ; # trade mark sign + + A0 C2A0 ; #   + A1 D18E ; # capital Byelorussian short U + A2 D19E ; # small Byelorussian short u + + A4 C2A4 ; # currency sign + A5 D290 ; # capital Ukrainian soft G + A6 C2A6 ; # borken bar + A7 C2A7 ; # section sign + A8 D081 ; # capital YO + A9 C2A9 ; # (C) + AA D084 ; # capital Ukrainian YE + AB C2AB ; # left-pointing double angle quotation mark + AC C2AC ; # not sign + AD C2AD ; # soft hypen + AE C2AE ; # (R) + AF D087 ; # capital Ukrainian YI + + B0 C2B0 ; # ° + B1 C2B1 ; # plus-minus sign + B2 D086 ; # capital Ukrainian I + B3 D196 ; # small Ukrainian i + B4 D291 ; # small Ukrainian soft g + B5 C2B5 ; # micro sign + B6 C2B6 ; # pilcrow sign + B7 C2B7 ; # · + B8 D191 ; # small yo + B9 E28496 ; # numero sign + BA D194 ; # small Ukrainian ye + BB C2BB ; # right-pointing double angle quotation mark + + BF D197 ; # small Ukrainian yi + + C0 D090 ; # capital A + C1 D091 ; # capital B + C2 D092 ; # capital V + C3 D093 ; # capital G + C4 D094 ; # capital D + C5 D095 ; # capital YE + C6 D096 ; # capital ZH + C7 D097 ; # capital Z + C8 D098 ; # capital I + C9 D099 ; # capital J + CA D09A ; # capital K + CB D09B ; # capital L + CC D09C ; # capital M + CD D09D ; # capital N + CE D09E ; # capital O + CF D09F ; # capital P + + D0 D0A0 ; # capital R + D1 D0A1 ; # capital S + D2 D0A2 ; # capital T + D3 D0A3 ; # capital U + D4 D0A4 ; # capital F + D5 D0A5 ; # capital KH + D6 D0A6 ; # capital TS + D7 D0A7 ; # capital CH + D8 D0A8 ; # capital SH + D9 D0A9 ; # capital SHCH + DA D0AA ; # capital hard sign + DB D0AB ; # capital Y + DC D0AC ; # capital soft sign + DD D0AD ; # capital E + DE D0AE ; # capital YU + DF D0AF ; # capital YA + + E0 D0B0 ; # small a + E1 D0B1 ; # small b + E2 D0B2 ; # small v + E3 D0B3 ; # small g + E4 D0B4 ; # small d + E5 D0B5 ; # small ye + E6 D0B6 ; # small zh + E7 D0B7 ; # small z + E8 D0B8 ; # small i + E9 D0B9 ; # small j + EA D0BA ; # small k + EB D0BB ; # small l + EC D0BC ; # small m + ED D0BD ; # small n + EE D0BE ; # small o + EF D0BF ; # small p + + F0 D180 ; # small r + F1 D181 ; # small s + F2 D182 ; # small t + F3 D183 ; # small u + F4 D184 ; # small f + F5 D185 ; # small kh + F6 D186 ; # small ts + F7 D187 ; # small ch + F8 D188 ; # small sh + F9 D189 ; # small shch + FA D18A ; # small hard sign + FB D18B ; # small y + FC D18C ; # small soft sign + FD D18D ; # small e + FE D18E ; # small yu + FF D18F ; # small ya +}