Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vapor 4 #43

Open
wants to merge 72 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
5c1d063
Update manifest to 5.2
0xTim Mar 26, 2020
2dbd465
Upgrade to Vapor 4
0xTim Mar 26, 2020
64d75c6
Update RouteCollection protocol conformances
0xTim Mar 26, 2020
e2871ac
Rename syncDecode to decode
0xTim Mar 26, 2020
a9f0f70
Update URL accessors
0xTim Mar 26, 2020
07900ed
Start migrating the repositories
0xTim Mar 26, 2020
9ba81fb
Migrate more repository stuff over
0xTim Mar 26, 2020
2ce9659
Migrate last make repository calls
0xTim Mar 26, 2020
99a3825
Get the feed generators converted
0xTim Mar 26, 2020
5230462
Start migrating the presenter services over
0xTim Mar 26, 2020
1072d69
Keep on going
0xTim Mar 26, 2020
0855821
Port over password stuff to new request extensions
0xTim Mar 27, 2020
567c450
More fixes, including password stuff which needs to be PRed into Vapor
0xTim Mar 27, 2020
3f87589
Migrate factories over to storage
0xTim Mar 27, 2020
84be307
Fix errors in PostAdminController
0xTim Mar 27, 2020
425c8f0
More code compiling
0xTim Mar 27, 2020
a7eedd2
Migrate password hasher stuff to storage
0xTim Mar 27, 2020
af0c386
Migrate over PaginatorTag
0xTim Mar 27, 2020
d311de6
Keep on trucking
0xTim Mar 27, 2020
f8c3ad0
Migrate the random number generator over to storage
0xTim Mar 27, 2020
4f8b40d
Compiling without the provider
0xTim Mar 27, 2020
d9e44ab
Bring part main provider stuff
0xTim Mar 27, 2020
1b4be71
Rename for new protocol
0xTim Mar 27, 2020
84a1513
Migrate RNG to real service type
0xTim Mar 27, 2020
76d6667
Port the rest of the services over
0xTim Mar 27, 2020
373fe7e
Fix the path creator issue
0xTim Mar 27, 2020
994d0ac
Start porting the tests
0xTim Apr 3, 2020
6185c40
Migrate MediaType to HTTPMediaType and all calls to request.http to r…
0xTim Apr 3, 2020
b266b40
Most services compiling
0xTim Apr 3, 2020
53f1df3
Bring back the CapturingViewRenderer
0xTim Apr 3, 2020
e8d75af
Get BlogPresenterTests compiling
0xTim Apr 3, 2020
9a57453
Get BlogViewTests compiling
0xTim Apr 3, 2020
d9220a5
Get BlogAdminPresenterTests compiling
0xTim Apr 3, 2020
7cb9b0d
Get the login tests working
0xTim Apr 3, 2020
5c485c8
Migrate some tests to new throwing setup
0xTim Apr 3, 2020
9fc5e6a
Get provider tests working
0xTim Apr 3, 2020
5522968
Get the ReversedPasswordHasher working
0xTim Apr 3, 2020
ce9682e
Get the TestWorld compiling
0xTim Apr 3, 2020
0731734
modify and register CapturingViewRenderer to test the Context which w…
tuchangwei Apr 4, 2020
7f28f48
Merge pull request #44 from tuchangwei/vapor4
0xTim Apr 4, 2020
dd0cafa
Merge branch 'vapor4' of github.com:brokenhandsio/SteamPress into vapor4
0xTim Apr 5, 2020
365e2bc
Port the password stuff over in tests
0xTim Apr 5, 2020
55a9973
Get password and presenter configuration compiling
0xTim Apr 5, 2020
5513566
Get repository configuration working in test
0xTim Apr 5, 2020
ba9537c
Remove some warnings
0xTim Apr 5, 2020
6c3a4b5
It compiles!
0xTim Apr 5, 2020
c65ec93
Stop some tests crashing
0xTim Apr 5, 2020
c3abc6a
Actually register routes
0xTim Apr 5, 2020
7432e27
The setup methods don't need to throw anymore
0xTim Apr 6, 2020
211e138
Stop the tests from crashing
0xTim Apr 6, 2020
80c3034
Get some more of the tests passing
0xTim Apr 6, 2020
a0ac133
Set WEBSITE_URL in setup as it's now required
0xTim Apr 6, 2020
ebbaa4c
Return the same instance of the presenter in test
0xTim Apr 6, 2020
00837e4
Fix tag and user paths
0xTim Apr 6, 2020
0ce517e
Fix the last tag tests
0xTim Apr 6, 2020
0c9a678
Migrate the Atom tests
0xTim Apr 6, 2020
3da52da
Migrate the RSS feed tests
0xTim Apr 6, 2020
be402b0
Fix code coverage
0xTim Apr 6, 2020
a3abe2d
Start working out application and config
0xTim Apr 14, 2020
3905542
Migrate source code to Vapor 4 release and use async password functions
0xTim Apr 14, 2020
e411cfd
Make the tests compile with Vapor 4 release
0xTim Apr 14, 2020
830855e
Tmp fix to get the tests working again
0xTim Apr 14, 2020
af3d3f4
Get SteamPress working with weird extension stuff
0xTim Apr 14, 2020
6038e9b
Migrate to throwing setup and teardown functions
0xTim Apr 14, 2020
416365a
Remove commented out code
0xTim Apr 14, 2020
f7aed38
Namespace all the steampress stuff on application
0xTim Apr 14, 2020
4ca3b12
Update README for Vapor 4
0xTim Apr 14, 2020
f79c677
Add new initialiser to help repositories
0xTim Apr 17, 2020
035bbae
Make repositories public on application in case anyone needs to acces…
0xTim Apr 17, 2020
ee9d336
Expose reset password required in the user initialiser
0xTim Apr 17, 2020
514232a
Add new initialiser for adding individual repositories
0xTim Apr 17, 2020
376f57f
Make applicaiton in steampress extension public to make it easy to bu…
0xTim Apr 17, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ on:
jobs:
xenial:
container:
image: vapor/swift:5.1-xenial
image: vapor/swift:5.2-xenial
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: swift test --enable-test-discovery --enable-code-coverage
bionic:
container:
image: vapor/swift:5.1-bionic
image: vapor/swift:5.2-bionic
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
Expand All @@ -20,7 +20,7 @@ jobs:
- name: Setup container for codecov upload
run: apt-get update && apt-get install curl
- name: Process coverage file
run: llvm-cov show .build/x86_64-unknown-linux/debug/SteamPressPackageTests.xctest -instr-profile=.build/x86_64-unknown-linux/debug/codecov/default.profdata > coverage.txt
run: llvm-cov show .build/x86_64-unknown-linux-gnu/debug/SteamPressPackageTests.xctest -instr-profile=.build/debug/codecov/default.profdata > coverage.txt
- name: Upload code coverage
uses: codecov/codecov-action@v1
with:
Expand Down
18 changes: 13 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
// swift-tools-version:5.1
// swift-tools-version:5.2

import PackageDescription

let package = Package(
name: "SteamPress",
platforms: [
.macOS(.v10_15)
],
products: [
.library(name: "SteamPress", targets: ["SteamPress"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-rc"),
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.0.0"),
.package(url: "https://github.com/vapor-community/markdown.git", from: "0.4.0"),
.package(url: "https://github.com/vapor/auth.git", from: "2.0.0"),
.package(url: "https://github.com/vapor/leaf-kit.git", from: "1.0.0-rc.1"),
.package(name: "SwiftMarkdown", url: "https://github.com/vapor-community/markdown.git", from: "0.6.1"),
],
targets: [
.target(name: "SteamPress", dependencies: ["Vapor", "SwiftSoup", "SwiftMarkdown", "Authentication"]),
.target(name: "SteamPress", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "LeafKit", package: "leaf-kit"),
"SwiftSoup",
"SwiftMarkdown"
]),
.testTarget(name: "SteamPressTests", dependencies: ["SteamPress"]),
]
)
39 changes: 14 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<br>
<br>
<a href="https://swift.org">
<img src="http://img.shields.io/badge/Swift-5.1-brightgreen.svg" alt="Language">
<img src="http://img.shields.io/badge/Swift-5.2-brightgreen.svg" alt="Language">
</a>
<a href="https://github.com/brokenhandsio/SteamPress/actions">
<img src="https://github.com/brokenhandsio/SteamPress/workflows/CI/badge.svg?branch=master" alt="Build Status">
Expand All @@ -16,7 +16,7 @@
</a>
</p>

SteamPress is a Swift blogging engine for use with the Vapor Framework to deploy blogs to sites that run on top of Vapor. It uses [Fluent](https://github.com/vapor/fluent) so will work with any database that has a Fluent Driver. It also incorporates a [Markdown Provider](https://github.com/vapor-community/markdown-provider) allowing you to write your posts in Markdown and then use Leaf to render the markdown.
SteamPress is a Swift blogging engine for use with the Vapor Framework to deploy blogs to sites that run on top of Vapor. It uses protocols to define database storage, so will work with any database that has a `SteamPressRepository` implementation, or you can write your own! It also incorporates a [Markdown Provider](https://github.com/vapor-community/markdown-provider) allowing you to write your posts in Markdown and then use Leaf to render the markdown.

The blog can either be used as the root of your website (i.e. appearing at https://www.acme.org) or in a subpath (i.e. https://www.acme.org/blog/).

Expand All @@ -42,14 +42,16 @@ There is an example of how it can work in a site (and what it requires in terms

## Add as a dependency

**TODO** Update

SteamPress is easy to integrate with your application. There are two providers that provide implementations for [PostgreSQL](https://github.com/brokenhandsio/steampress-fluent-postgres) or [MySQL](https://github.com/brokenhandsio/steampress-fluent-mysql). You are also free to write your own integrations. Normally you'd choose one of the implementations as that provides repository integrations for the database. In this example, we're using Postgres.

First, add the provider to your `Package.swift` dependencies:

```swift
dependencies: [
// ...
.package(name: "SteampressFluentPostgres", url: "https://github.com/brokenhandsio/steampress-fluent-postgres.git", from: "1.0.0"),
.package(name: "SteampressFluentPostgres", url: "https://github.com/brokenhandsio/steampress-fluent-postgres.git", from: "2.0.0"),
],
```

Expand Down Expand Up @@ -98,7 +100,7 @@ First add SteamPress to your `Package.swift` dependencies:
```swift
dependencies: [
// ...,
.package(name: "SteamPress", url: "https://github.com/brokenhandsio/SteamPress", from: "1.0.0")
.package(name: "SteamPress", url: "https://github.com/brokenhandsio/SteamPress", from: "2.0.0")
]
```

Expand All @@ -115,42 +117,29 @@ And then as a dependency to your target:
This will register the routes for you. You must provide implementations for the different repository types to your services:

```swift
services.register(MyTagRepository(), as: BlogTagRepository.self)
services.register(MyUserRepository(), as: BlogUserRepository.self)
services.register(MyPostRepository(), as: BlogPostRepository.self)
app.steampress.blogRepositories.use { application in
MyRepository(application: application)
}
```

You can then register the SteamPress provider with your services:

```swift
let steampressProvider = SteamPress.Provider()
try services.register(steampressProvider)
```
SteamPress will be automatically registered, depending on the configuration provided (see below).

## Integration

SteamPress offers a 'Remember Me' functionality when logging in to extend the duration of the session. In order for this to work, you must register the middleware:

```swift
var middlewares = MiddlewareConfig()
// ...
middlewares.use(BlogRememberMeMiddleware.self)
middlewares.use(SessionsMiddleware.self)
services.register(middlewares)
application.middlewares.use(BlogRememberMeMiddleware())
```

**Note:** This must be registered before you register the `SessionsMiddleware`.

SteamPress uses a `PasswordVerifier` protocol to check passwords. Vapor doesn't provide a default BCrypt implementation for this, so you must register this yourself:

```swift
config.prefer(BCryptDigest.self, for: PasswordVerifier.self)
```
**TODO: Update**

Finally, if you wish to use the `#markdown()` tag with your blog Leaf templates, you must register this. There's also a paginator tag, to make pagination easy:

```swift
var tags = LeafTagConfig.default()
var tags = LeafTagConfig.default()
tags.use(Markdown(), as: "markdown")
let paginatorTag = PaginatorTag(paginationLabel: "Blog Posts")
tags.use(paginatorTag, as: PaginatorTag.name)
Expand All @@ -175,7 +164,7 @@ let feedInformation = FeedInformation(
description: "SteamPress is an open-source blogging engine written for Vapor in Swift",
copyright: "Released under the MIT licence",
imageURL: "https://user-images.githubusercontent.com/9938337/29742058-ed41dcc0-8a6f-11e7-9cfc-680501cdfb97.png")
try services.register(SteamPressFluentPostgresProvider(blogPath: "blog", feedInformation: feedInformation, postsPerPage: 5))
application.steampress.configuration = SteamPressConfiguration(blogPath: "blog", feedInformation: feedInformation, postsPerPage: 5)
```

Additionally, you should set the `WEBSITE_URL` environment variable to the root address of your website, e.g. `https://www.steampress.io`. This is used to set various parameters throughout SteamPress.
Expand Down
4 changes: 2 additions & 2 deletions Sources/SteamPress/Controllers/API/APIController.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Vapor

struct APIController: RouteCollection {
func boot(router: Router) throws {
let apiRoutes = router.grouped("api")
func boot(routes: RoutesBuilder) throws {
let apiRoutes = routes.grouped("api")

let apiTagController = APITagController()
try apiRoutes.register(collection: apiTagController)
Expand Down
7 changes: 3 additions & 4 deletions Sources/SteamPress/Controllers/API/APITagController.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import Vapor

struct APITagController: RouteCollection {
func boot(router: Router) throws {
let tagsRoute = router.grouped("tags")
func boot(routes: RoutesBuilder) throws {
let tagsRoute = routes.grouped("tags")
tagsRoute.get(use: allTagsHandler)
}

func allTagsHandler(_ req: Request) throws -> EventLoopFuture<[BlogTag]> {
let repository = try req.make(BlogTagRepository.self)
return repository.getAllTags(on: req)
req.blogTagRepository.getAllTags()
}
}
129 changes: 67 additions & 62 deletions Sources/SteamPress/Controllers/Admin/LoginController.swift
Original file line number Diff line number Diff line change
@@ -1,136 +1,141 @@
import Vapor
import Authentication

struct LoginController: RouteCollection {

// MARK: - Properties
private let pathCreator: BlogPathCreator

// MARK: - Initialiser
init(pathCreator: BlogPathCreator) {
self.pathCreator = pathCreator
}

// MARK: - Route setup
func boot(router: Router) throws {
router.get("login", use: loginHandler)
router.post("login", use: loginPostHandler)

func boot(routes: RoutesBuilder) throws {
routes.get("login", use: loginHandler)
routes.post("login", use: loginPostHandler)
let redirectMiddleware = BlogLoginRedirectAuthMiddleware(pathCreator: pathCreator)
let protectedRoutes = router.grouped(redirectMiddleware)
let protectedRoutes = routes.grouped(redirectMiddleware)
protectedRoutes.post("logout", use: logoutHandler)
protectedRoutes.get("resetPassword", use: resetPasswordHandler)
protectedRoutes.post("resetPassword", use: resetPasswordPostHandler)
}

// MARK: - Route handlers
func loginHandler(_ req: Request) throws -> EventLoopFuture<View> {
let loginRequied = (try? req.query.get(Bool.self, at: "loginRequired")) != nil
let presenter = try req.make(BlogPresenter.self)
return try presenter.loginView(on: req, loginWarning: loginRequied, errors: nil, username: nil, usernameError: false, passwordError: false, rememberMe: false, pageInformation: req.pageInformation())
return try req.blogPresenter.loginView(loginWarning: loginRequied, errors: nil, username: nil, usernameError: false, passwordError: false, rememberMe: false, pageInformation: req.pageInformation())
}

func loginPostHandler(_ req: Request) throws -> EventLoopFuture<Response> {
let loginData = try req.content.syncDecode(LoginData.self)
let loginData = try req.content.decode(LoginData.self)
var loginErrors = [String]()
var usernameError = false
var passwordError = false

if loginData.username == nil {
loginErrors.append("You must supply your username")
usernameError = true
}

if loginData.password == nil {
loginErrors.append("You must supply your password")
passwordError = true
}

if !loginErrors.isEmpty {
let presenter = try req.make(BlogPresenter.self)
return try presenter.loginView(on: req, loginWarning: false, errors: loginErrors, username: loginData.username, usernameError: usernameError, passwordError: passwordError, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encode(for: req)
return try req.blogPresenter.loginView(loginWarning: false, errors: loginErrors, username: loginData.username, usernameError: usernameError, passwordError: passwordError, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encodeResponse(for: req)
}

guard let username = loginData.username, let password = loginData.password else {
throw Abort(.internalServerError)
}

if let rememberMe = loginData.rememberMe, rememberMe {
try req.session()["SteamPressRememberMe"] = "YES"
req.session.data["SteamPressRememberMe"] = "YES"
} else {
try req.session()["SteamPressRememberMe"] = nil
req.session.data["SteamPressRememberMe"] = nil
}

let userRepository = try req.make(BlogUserRepository.self)
return userRepository.getUser(username: username, on: req).flatMap { user in
let verifier = try req.make(PasswordVerifier.self)
guard let user = user, try verifier.verify(password, created: user.password) else {

return req.blogUserRepository.getUser(username: username).flatMap { user -> EventLoopFuture<Response> in
guard let user = user else {
let loginError = ["Your username or password is incorrect"]
let presenter = try req.make(BlogPresenter.self)
return try presenter.loginView(on: req, loginWarning: false, errors: loginError, username: loginData.username, usernameError: false, passwordError: false, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encode(for: req)
do {
return try req.blogPresenter.loginView(loginWarning: false, errors: loginError, username: loginData.username, usernameError: false, passwordError: false, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encodeResponse(for: req)
} catch {
return req.eventLoop.makeFailedFuture(error)

Check warning on line 67 in Sources/SteamPress/Controllers/Admin/LoginController.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SteamPress/Controllers/Admin/LoginController.swift#L64-L67

Added lines #L64 - L67 were not covered by tests
}
}
return req.password.async.verify(password, created: user.password).flatMap { userAuthenticated in
guard userAuthenticated else {
let loginError = ["Your username or password is incorrect"]
do {
return try req.blogPresenter.loginView(loginWarning: false, errors: loginError, username: loginData.username, usernameError: false, passwordError: false, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encodeResponse(for: req)
} catch {
return req.eventLoop.makeFailedFuture(error)

Check warning on line 76 in Sources/SteamPress/Controllers/Admin/LoginController.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SteamPress/Controllers/Admin/LoginController.swift#L76

Added line #L76 was not covered by tests
}
}
user.authenticateSession(on: req)
return req.eventLoop.future(req.redirect(to: self.pathCreator.createPath(for: "admin")))
}
try user.authenticateSession(on: req)
return req.future(req.redirect(to: self.pathCreator.createPath(for: "admin")))
}
}

func logoutHandler(_ request: Request) throws -> Response {
try request.unauthenticateBlogUserSession()
func logoutHandler(_ request: Request) -> Response {
request.unauthenticateBlogUserSession()
return request.redirect(to: pathCreator.createPath(for: pathCreator.blogPath))
}

func resetPasswordHandler(_ req: Request) throws -> EventLoopFuture<View> {
let presenter = try req.make(BlogAdminPresenter.self)
return try presenter.createResetPasswordView(on: req, errors: nil, passwordError: nil, confirmPasswordError: nil, pageInformation: req.adminPageInfomation())
try req.adminPresenter.createResetPasswordView(errors: nil, passwordError: nil, confirmPasswordError: nil, pageInformation: req.adminPageInfomation())
}

func resetPasswordPostHandler(_ req: Request) throws -> EventLoopFuture<Response> {
let data = try req.content.syncDecode(ResetPasswordData.self)

let data = try req.content.decode(ResetPasswordData.self)
var resetPasswordErrors = [String]()
var passwordError: Bool?
var confirmPasswordError: Bool?

guard let password = data.password, let confirmPassword = data.confirmPassword else {

if data.password == nil {
resetPasswordErrors.append("You must specify a password")
passwordError = true
}

if data.confirmPassword == nil {
resetPasswordErrors.append("You must confirm your password")
confirmPasswordError = true
}

let presenter = try req.make(BlogAdminPresenter.self)
let view = try presenter.createResetPasswordView(on: req, errors: resetPasswordErrors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: req.adminPageInfomation())
return try view.encode(for: req)

let view = try req.adminPresenter.createResetPasswordView(errors: resetPasswordErrors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: req.adminPageInfomation())
return view.encodeResponse(for: req)
}

if password != confirmPassword {
resetPasswordErrors.append("Your passwords must match!")
passwordError = true
confirmPasswordError = true
}

if password.count < 10 {
passwordError = true
resetPasswordErrors.append("Your password must be at least 10 characters long")
}

guard resetPasswordErrors.isEmpty else {
let presenter = try req.make(BlogAdminPresenter.self)
let view = try presenter.createResetPasswordView(on: req, errors: resetPasswordErrors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: req.adminPageInfomation())
return try view.encode(for: req)
let view = try req.adminPresenter.createResetPasswordView(errors: resetPasswordErrors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: req.adminPageInfomation())
return view.encodeResponse(for: req)
}

let user = try req.auth.require(BlogUser.self)
return req.password.async.hash(password).flatMap { hashedPassword in
user.password = hashedPassword
user.resetPasswordRequired = false
let redirect = req.redirect(to: self.pathCreator.createPath(for: "admin"))
return req.blogUserRepository.save(user).transform(to: redirect)
}

let user = try req.requireAuthenticated(BlogUser.self)
let hasher = try req.make(PasswordHasher.self)
user.password = try hasher.hash(password)
user.resetPasswordRequired = false
let userRespository = try req.make(BlogUserRepository.self)
let redirect = req.redirect(to: pathCreator.createPath(for: "admin"))
return userRespository.save(user, on: req).transform(to: redirect)
}
}
Loading