From 1d5102721b38db7c395f6ad9d65bf8b732c434b3 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Wed, 18 Apr 2018 17:07:14 +0200 Subject: [PATCH 01/11] Introduce AdminPanelUserType protocol --- Package.swift | 11 +-- .../AdminPanelProvider/Commands/Seeder.swift | 6 +- .../AdminPanelUserController.swift | 63 ++++++++-------- .../Controllers/LoginController.swift | 12 +-- Sources/AdminPanelProvider/Helpers/Gate.swift | 4 +- Sources/AdminPanelProvider/Middlewares.swift | 16 ---- .../Middlewares/ActivityMiddleware.swift | 8 +- .../Middlewares/ProtectMiddleware.swift | 6 +- .../AdminPanelProvider/Models/Action.swift | 16 ++-- .../Models/AdminPanelUser.swift | 73 +++++++++++++++---- .../Models/AdminPanelUserForm.swift | 6 +- Sources/AdminPanelProvider/Provider.swift | 37 ++++++---- .../Routes/AdminPanelUserRoutes.swift | 14 ++-- 13 files changed, 162 insertions(+), 110 deletions(-) diff --git a/Package.swift b/Package.swift index 3df3f5b5..9ab0f8a6 100644 --- a/Package.swift +++ b/Package.swift @@ -4,15 +4,16 @@ let package = Package( name: "AdminPanelProvider", dependencies: [ // Vapor - .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2), - .Package(url: "https://github.com/vapor/leaf-provider.git", majorVersion: 1), .Package(url: "https://github.com/vapor/auth-provider.git", majorVersion: 1), + .Package(url: "https://github.com/vapor/leaf-provider.git", majorVersion: 1), + .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2), // Nodes .Package(url: "https://github.com/nodes-vapor/flash.git", majorVersion: 1), - .Package(url: "https://github.com/nodes-vapor/slugify.git", majorVersion: 1), - .Package(url: "https://github.com/nodes-vapor/storage.git", majorVersion: 0, minor: 4), - .Package(url: "https://github.com/nodes-vapor/paginator.git", majorVersion: 2, minor: 0), .Package(url: "https://github.com/nodes-vapor/audit-provider.git", majorVersion: 0, minor: 1), + .Package(url: "https://github.com/nodes-vapor/forms.git", majorVersion: 0, minor: 7), + .Package(url: "https://github.com/nodes-vapor/paginator.git", majorVersion: 2, minor: 0), + .Package(url: "https://github.com/nodes-vapor/slugify.git", majorVersion: 1), + .Package(url: "https://github.com/nodes-vapor/storage.git", majorVersion: 0, minor: 4) ] ) diff --git a/Sources/AdminPanelProvider/Commands/Seeder.swift b/Sources/AdminPanelProvider/Commands/Seeder.swift index f1bc999f..4519739b 100644 --- a/Sources/AdminPanelProvider/Commands/Seeder.swift +++ b/Sources/AdminPanelProvider/Commands/Seeder.swift @@ -1,8 +1,10 @@ import Vapor import Console +typealias Seeder = CustomUserSeeder + /// Seeds the admin panel with a default user -public final class Seeder: Command, ConfigInitializable { +public final class CustomUserSeeder: Command, ConfigInitializable { public let id = "admin-panel:seeder" public let help: [String] = [ @@ -18,7 +20,7 @@ public final class Seeder: Command, ConfigInitializable { public func run(arguments: [String]) throws { console.info("Started the seeder") - let user = try AdminPanelUser( + let user = try U( name: "Admin", title: "Default admin account", email: "admin@admin.com", diff --git a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift index 9efcc07e..9908f0d0 100644 --- a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift +++ b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift @@ -1,10 +1,13 @@ +import Flash +import Forms import Leaf import SMTP -import Flash -import Vapor import Storage +import Vapor + +public typealias AdminPanelUserController = CustomAdminPanelUserController -public final class AdminPanelUserController { +public final class CustomAdminPanelUserController { public let renderer: ViewRenderer public let env: Environment public let mailer: MailProtocol? @@ -23,11 +26,11 @@ public final class AdminPanelUserController { } public func index(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(AdminPanelUser.self) + let requestingUser = try req.auth.assertAuthenticated(U.self) try Gate.assertAllowed(requestingUser, requiredRole: .admin) - let superAdmins = try AdminPanelUser.makeQuery().filter("role", "Super Admin").all() - let admins = try AdminPanelUser.makeQuery().filter("role", "Admin").all() - let users = try AdminPanelUser.makeQuery().filter("role", "User").all() + let superAdmins = try U.makeQuery().filter(U.roleKey, "Super Admin").all() + let admins = try U.makeQuery().filter(U.roleKey, "Admin").all() + let users = try U.makeQuery().filter(U.roleKey, "User").all() return try renderer.make( "AdminPanel/BackendUser/index", @@ -41,19 +44,19 @@ public final class AdminPanelUserController { } public func create(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(AdminPanelUser.self) + let requestingUser = try req.auth.assertAuthenticated(U.self) try Gate.assertAllowed(requestingUser, requiredRole: .admin) - let fieldset = try req.storage["_fieldset"] as? Node ?? AdminPanelUserForm().makeNode(in: nil) + let fieldset = try req.fieldset ?? AdminPanelUserForm().makeNode(in: nil) return try renderer.make("AdminPanel/BackendUser/edit", ["fieldset": fieldset], for: req) } public func store(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(AdminPanelUser.self) + let requestingUser = try req.auth.assertAuthenticated(U.self) try Gate.assertAllowed(requestingUser, requiredRole: .admin) do { let (form, hasErrors) = AdminPanelUserForm.validating(req.data) - let isEmailUnique = try AdminPanelUser.makeQuery().filter("email", form.email).first() == nil + let isEmailUnique = try U.makeQuery().filter("email", form.email).first() == nil if hasErrors || !isEmailUnique { let response = redirect("/admin/backend/users/create") @@ -71,7 +74,7 @@ public final class AdminPanelUserController { ) } - response.storage["_fieldset"] = fieldset + response.fieldset = fieldset return response } @@ -87,7 +90,7 @@ public final class AdminPanelUserController { let randomPassword = form.password.isEmpty ? String.random(12) : form.password - let user = try AdminPanelUser( + let user = try U( name: form.name, title: form.title, email: form.email, @@ -139,35 +142,35 @@ public final class AdminPanelUserController { } public func edit(req: Request) throws -> ResponseRepresentable { - let user: AdminPanelUser + let user: U do { - user = try req.parameters.next(AdminPanelUser.self) + user = try req.parameters.next(U.self) } catch { return redirect("/admin/backend/users").flash(.error, "User not found") } - let requestingUser = try req.auth.assertAuthenticated(AdminPanelUser.self) + let requestingUser = try req.auth.assertAuthenticated(U.self) let allowed = Gate.allow(requestingUser, requiredRole: .admin) || requestingUser.id == user.id guard allowed else { throw Abort.notFound } - let fieldset = try req.storage["_fieldset"] as? Node ?? AdminPanelUserForm().makeNode(in: nil) + let fieldset = try req.fieldset ?? AdminPanelUserForm().makeNode(in: nil) return try renderer.make("AdminPanel/BackendUser/edit", ["user": user, "fieldset": fieldset], for: req) } public func update(req: Request) throws -> ResponseRepresentable { do { - var user: AdminPanelUser + var user: U do { - user = try req.parameters.next(AdminPanelUser.self) + user = try req.parameters.next(U.self) } catch { return redirect("/admin/backend/users").flash(.error, "User not found") } - let requestingUser = try req.auth.assertAuthenticated(AdminPanelUser.self) + let requestingUser = try req.auth.assertAuthenticated(U.self) let allowed = Gate.allow(requestingUser, requiredRole: .admin) || requestingUser.id == user.id guard allowed else { @@ -178,7 +181,7 @@ public final class AdminPanelUserController { let (form, hasErrors) = AdminPanelUserForm.validating(req.data, ignoreRole: true) if - let userByEmail = try AdminPanelUser.makeQuery().filter("email", form.email).first(), + let userByEmail = try U.makeQuery().filter(U.emailKey, form.email).first(), userByEmail.id != user.id { let response = redirect("/admin/backend/users/\(user.id?.string ?? "0")/edit/") @@ -194,7 +197,7 @@ public final class AdminPanelUserController { ]) ) - response.storage["_fieldset"] = fieldset + response.fieldset = fieldset return response } @@ -202,7 +205,7 @@ public final class AdminPanelUserController { let response = redirect("/admin/backend/users/\(user.id?.string ?? "0")/edit/") .flash(.error, "Validation error") let fieldset = try form.makeNode(in: nil) - response.storage["_fieldset"] = fieldset + response.fieldset = fieldset return response } @@ -216,7 +219,7 @@ public final class AdminPanelUserController { let response = redirect("/admin/backend/users/\(user.id?.string ?? "0")/edit/") .flash(.error, "Please pick a new password") let fieldset = try form.makeNode(in: nil) - response.storage["_fieldset"] = fieldset + response.fieldset = fieldset return response } @@ -259,12 +262,12 @@ public final class AdminPanelUserController { } public func delete(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(AdminPanelUser.self) + let requestingUser = try req.auth.assertAuthenticated(U.self) try Gate.assertAllowed(requestingUser, requiredRole: .admin) - let user: AdminPanelUser + let user: U do { - user = try req.parameters.next(AdminPanelUser.self) + user = try req.parameters.next(U.self) } catch { return redirect("/admin/backend/users").flash(.error, "User not found") } @@ -279,13 +282,13 @@ public final class AdminPanelUserController { } public func restore(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(AdminPanelUser.self) + let requestingUser = try req.auth.assertAuthenticated(U.self) try Gate.assertAllowed(requestingUser, requiredRole: .admin) - let user: AdminPanelUser + let user: U do { let id = try req.parameters.next(Int.self) - guard let u = try AdminPanelUser.makeQuery().filter("id", id).withSoftDeleted().first() else { + guard let u = try U.makeQuery().filter(U.idKey, id).withSoftDeleted().first() else { throw Abort.notFound } diff --git a/Sources/AdminPanelProvider/Controllers/LoginController.swift b/Sources/AdminPanelProvider/Controllers/LoginController.swift index 3cb0ae12..9ca03137 100644 --- a/Sources/AdminPanelProvider/Controllers/LoginController.swift +++ b/Sources/AdminPanelProvider/Controllers/LoginController.swift @@ -2,7 +2,9 @@ import Vapor import Cookies import AuthProvider -public final class LoginController { +public typealias LoginController = CustomUserLoginController + +public final class CustomUserLoginController { public let renderer: ViewRenderer public let mailer: MailProtocol? public let panelConfig: PanelConfig @@ -27,7 +29,7 @@ public final class LoginController { } let credentials = Password(username: username, password: password) - let user = try AdminPanelUser.authenticate(credentials) + let user = try U.authenticate(credentials) try req.auth.authenticate(user, persist: true) var redir = "/admin/dashboard" @@ -42,7 +44,7 @@ public final class LoginController { } public func landing(req: Request) throws -> ResponseRepresentable { - guard !req.auth.isAuthenticated(AdminPanelUser.self) else { + guard !req.auth.isAuthenticated(U.self) else { return redirect("/admin/dashboard") } @@ -66,7 +68,7 @@ public final class LoginController { do { guard let email = req.data["email"]?.string, - let user = try AdminPanelUser.makeQuery().filter("email", email).first() + let user = try U.makeQuery().filter(U.emailKey, email).first() else { return redirect("/admin/login") .flash(.success, "E-mail with instructions sent if user exists") @@ -151,7 +153,7 @@ public final class LoginController { .flash(.error, "Passwords do not match") } - guard let user = try AdminPanelUser.makeQuery().filter("email", email).first() else { + guard let user = try U.makeQuery().filter(U.emailKey, email).first() else { return redirect("/admin/login").flash(.error, "User not found") } diff --git a/Sources/AdminPanelProvider/Helpers/Gate.swift b/Sources/AdminPanelProvider/Helpers/Gate.swift index 80b7cdfb..ff80161e 100644 --- a/Sources/AdminPanelProvider/Helpers/Gate.swift +++ b/Sources/AdminPanelProvider/Helpers/Gate.swift @@ -34,13 +34,13 @@ public class Gate { } /// Returns whether or not a given user has more than or equal permissions than required - public static func allow(_ user: AdminPanelUser, requiredRole: Role) -> Bool { + public static func allow(_ user: U, requiredRole: Role) -> Bool { guard let role = Role.init(from: user.role) else { return false } return allow(role, requiredRole: requiredRole) } /// Throws if a user doesn't have equal or more permissions than required - public static func assertAllowed(_ user: AdminPanelUser, requiredRole: Role) throws { + public static func assertAllowed(_ user: U, requiredRole: Role) throws { guard allow(user, requiredRole: requiredRole) else { // Don't show them this endpoint exists throw Abort.notFound diff --git a/Sources/AdminPanelProvider/Middlewares.swift b/Sources/AdminPanelProvider/Middlewares.swift index 9f20d1d1..16f4776d 100644 --- a/Sources/AdminPanelProvider/Middlewares.swift +++ b/Sources/AdminPanelProvider/Middlewares.swift @@ -1,22 +1,6 @@ -import HTTP import Vapor public enum Middlewares { public static var unsecured: [Middleware] = [] public static var secured: [Middleware] = [] } - -public class FieldsetMiddleware: Middleware { - let key = "_fieldset" - public init() {} - - public func respond(to request: Request, chainingTo next: Responder) throws -> Response { - // Add fieldset to next request - request.storage[key] = request.session?.data[key] - request.session?.data[key] = nil - - let respond = try next.respond(to: request) - request.session?.data[key] = respond.storage[key] as? Node ?? nil - return respond - } -} diff --git a/Sources/AdminPanelProvider/Middlewares/ActivityMiddleware.swift b/Sources/AdminPanelProvider/Middlewares/ActivityMiddleware.swift index e26fd377..306ccd57 100644 --- a/Sources/AdminPanelProvider/Middlewares/ActivityMiddleware.swift +++ b/Sources/AdminPanelProvider/Middlewares/ActivityMiddleware.swift @@ -3,9 +3,11 @@ import Storage import Paginator import AuditProvider -public final class ActivityMiddleware: Middleware { +public typealias ActivityMiddleware = CustomUserActivityMiddleware + +public final class CustomUserActivityMiddleware: Middleware { public func respond(to request: Request, chainingTo next: Responder) throws -> Response { - if request.auth.isAuthenticated(AdminPanelUser.self) { + if request.auth.isAuthenticated(U.self) { let node = try AuditEvent.makeQuery().limit(10).all().map { raw -> Node in var node = try raw.makeNode(in: nil) @@ -13,7 +15,7 @@ public final class ActivityMiddleware: Middleware { try node.set("createdAt", createdAt) } - if let author = try AdminPanelUser.find(raw.authorId) { + if let author = try U.find(raw.authorId) { try node.set("author", author.makeNode(in: nil)) } diff --git a/Sources/AdminPanelProvider/Middlewares/ProtectMiddleware.swift b/Sources/AdminPanelProvider/Middlewares/ProtectMiddleware.swift index 457ae3ea..4c623099 100644 --- a/Sources/AdminPanelProvider/Middlewares/ProtectMiddleware.swift +++ b/Sources/AdminPanelProvider/Middlewares/ProtectMiddleware.swift @@ -2,11 +2,13 @@ import HTTP import Flash import Authentication +public typealias ProtectMiddleware = CustomUserProtectMiddleware + /// Redirects unauthenticated requests to a supplied path. -public final class ProtectMiddleware: Middleware { +public final class CustomUserProtectMiddleware: Middleware { public func respond(to req: Request, chainingTo next: Responder) throws -> Response { do { - if let user = req.auth.authenticated(AdminPanelUser.self), user.shouldResetPassword { + if let user = req.auth.authenticated(U.self), user.shouldResetPassword { let redirectPath = "/admin/backend/users/\(user.id?.string ?? "0")/edit" if req.uri.path != redirectPath && req.uri.path.replacingOccurrences(of: "/", with: "") != redirectPath.replacingOccurrences(of: "/", with: "") { diff --git a/Sources/AdminPanelProvider/Models/Action.swift b/Sources/AdminPanelProvider/Models/Action.swift index 54fa7171..981927f8 100644 --- a/Sources/AdminPanelProvider/Models/Action.swift +++ b/Sources/AdminPanelProvider/Models/Action.swift @@ -1,7 +1,9 @@ import FluentProvider +public typealias Action = CustomUserAction + /// An event that occured in the admin panel -public final class Action: Model { +public final class CustomUserAction: Model { public let storage = Storage() public var name: String @@ -35,9 +37,9 @@ public final class Action: Model { } } -extension Action: Timestampable {} +extension CustomUserAction: Timestampable {} -extension Action: JSONRepresentable { +extension CustomUserAction: JSONRepresentable { public func makeJSON() throws -> JSON { var json = JSON() @@ -51,13 +53,13 @@ extension Action: JSONRepresentable { } } -extension Action: Preparation { +extension CustomUserAction: Preparation { public static func prepare(_ database: Database) throws { try database.create(self) { $0.id() $0.string("name") $0.string("message") - $0.foreignId(for: AdminPanelUser.self) + $0.foreignId(for: U.self) } } @@ -66,8 +68,8 @@ extension Action: Preparation { } } -extension Action { - public static func report(_ user: AdminPanelUser, _ message: String) { +extension CustomUserAction { + public static func report(_ user: U, _ message: String) { do { let action = Action(name: user.name, userId: user.id ?? "0", message: message) try action.save() diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift index 86f09872..2b8ab3fe 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift @@ -5,16 +5,63 @@ import AuthProvider import AuditProvider import FluentProvider +public protocol AdminPanelUserType: + NodeRepresentable, + Parameterizable, + PasswordAuthenticatable, + Persistable, + Preparation, + SoftDeletable, + ViewDataRepresentable +{ + associatedtype A: Preparation + + init( + name: String, + title: String, + email: String, + password: String, + role: String, + shouldResetPassword: Bool, + avatar: String? + ) throws + + var avatar: String? { get set } + var email: String { get set } + var name: String { get set } + var password: String { get set } + var role: String { get set } + var shouldResetPassword: Bool { get set } + var title: String { get set } + + /// database key name for `role` property + static var roleKey: String { get } + + /// database key name for `email` property + static var emailKey: String { get } + + func updatePassword(_ newPass: String) throws +} + +extension AdminPanelUserType { + public static var roleKey: String { return "role" } + public static var emailKey: String { return "email" } +} + +extension AdminPanelUser: AdminPanelUserType { + public typealias A = Action +} + public final class AdminPanelUser: Model { public let storage = Storage() - public var name: String - public var title: String + public var avatar: String? public var email: String + public var name: String public var password: String public var role: String public var shouldResetPassword: Bool - public var avatar: String? + public var title: String public var avatarUrl: String { return avatar ?? "https://api.adorable.io/avatars/150/\(email).png" @@ -41,9 +88,9 @@ public final class AdminPanelUser: Model { public init(row: Row) throws { name = try row.get("name") title = try row.get("title") - email = try row.get("email") + email = try row.get(AdminPanelUser.emailKey) password = try row.get("password") - role = try row.get("role") + role = try row.get(AdminPanelUser.roleKey) shouldResetPassword = try row.get(AdminPanelUser.shouldResetPasswordKey) avatar = row["avatar"]?.string } @@ -53,9 +100,9 @@ public final class AdminPanelUser: Model { try row.set("name", name) try row.set("title", title) - try row.set("email", email) + try row.set(AdminPanelUser.emailKey, email) try row.set("password", password) - try row.set("role", role) + try row.set(AdminPanelUser.roleKey, role) try row.set(AdminPanelUser.shouldResetPasswordKey, shouldResetPassword) try row.set("avatar", avatar) @@ -76,8 +123,8 @@ extension AdminPanelUser: ViewDataRepresentable { "id": .string(id?.string ?? "0"), "name": .string(name), "title": .string(title), - "email": .string(email), - "role": .string(role), + AdminPanelUser.emailKey: .string(email), + AdminPanelUser.roleKey: .string(role), "avatarUrl": .string(Storage.getCDNPath(optional: avatar) ?? avatarUrl) ]) } @@ -90,7 +137,7 @@ extension AdminPanelUser: NodeRepresentable { "name": .string(name), "title": .string(title), "email": .string(email), - "role": .string(role), + AdminPanelUser.roleKey: .string(role), "avatarUrl": .string(Storage.getCDNPath(optional: avatar) ?? avatarUrl) ]) } @@ -106,9 +153,9 @@ extension AdminPanelUser: Preparation { $0.id() $0.string("name") $0.string("title") - $0.string("email") + $0.string(AdminPanelUser.emailKey) $0.string("password") - $0.string("role") + $0.string(AdminPanelUser.roleKey) $0.bool(AdminPanelUser.shouldResetPasswordKey) $0.string("avatar", optional: true) } @@ -121,7 +168,7 @@ extension AdminPanelUser: Preparation { extension AdminPanelUser: PasswordAuthenticatable { public static func authenticate(_ credentials: Password) throws -> AdminPanelUser { guard - let user = try AdminPanelUser.makeQuery().filter("email", credentials.username).first(), + let user = try makeQuery().filter(AdminPanelUser.emailKey, credentials.username).first(), try BCryptHasher().check(credentials.password, matchesHash: user.password) else { throw Abort.unauthorized diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift index 3afde2b6..9319b7c7 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift @@ -53,9 +53,9 @@ public struct AdminPanelUserForm { extension AdminPanelUserForm { public static func validating(_ data: Content, ignoreRole: Bool = false) -> (AdminPanelUserForm, Bool) { let name = data["name"]?.string - let email = data["email"]?.string + let email = data[AdminPanelUser.emailKey]?.string let title = data["title"]?.string - let role = data["role"]?.string + let role = data[AdminPanelUser.roleKey]?.string let shouldResetPassword = data["shouldResetPassword"]?.string != nil let sendEmail = data["sendEmail"]?.string != nil let password = data["password"]?.string @@ -220,7 +220,7 @@ extension AdminPanelUserForm: NodeRepresentable { try node.set("name", nameObj) try node.set("email", emailObj) try node.set("title", titleObj) - try node.set("role", roleObj) + try node.set(AdminPanelUser.roleKey, roleObj) try node.set("shouldResetPassword", shouldResetPasswordObj) try node.set("sendEmail", sendEmailObj) try node.set("password", passwordObj) diff --git a/Sources/AdminPanelProvider/Provider.swift b/Sources/AdminPanelProvider/Provider.swift index 8227b6ce..f4cc51a1 100644 --- a/Sources/AdminPanelProvider/Provider.swift +++ b/Sources/AdminPanelProvider/Provider.swift @@ -1,15 +1,20 @@ -import Flash -import Vapor -import Storage -import Sessions +import AuditProvider import AuthProvider -import LeafProvider +import Flash +import Forms import Leaf -import AuditProvider +import LeafProvider import Paginator +import Sessions +import Storage +import Vapor + +public typealias Provider = CustomUserProvider -public final class Provider: Vapor.Provider { - public static let repositoryName = "nodes-vapor/admin-panel-provider" +public final class CustomUserProvider: Vapor.Provider { + public static var repositoryName: String { + return "nodes-vapor/admin-panel-provider" + } public var panelConfig: PanelConfig public init(panelConfig: PanelConfig) { @@ -85,20 +90,20 @@ public final class Provider: Vapor.Provider { public func boot(_ config: Config) throws { try Middlewares.unsecured.append(PanelConfigMiddleware(panelConfig)) - Middlewares.unsecured.append(PersistMiddleware(AdminPanelUser.self)) + Middlewares.unsecured.append(PersistMiddleware(U.self)) Middlewares.unsecured.append(FlashMiddleware()) Middlewares.unsecured.append(FieldsetMiddleware()) - Middlewares.unsecured.append(ActivityMiddleware()) + Middlewares.unsecured.append(CustomUserActivityMiddleware()) Middlewares.secured = Middlewares.unsecured - Middlewares.secured.append(ProtectMiddleware()) - Middlewares.secured.append(PasswordAuthenticationMiddleware(AdminPanelUser.self)) + Middlewares.secured.append(CustomUserProtectMiddleware()) + Middlewares.secured.append(PasswordAuthenticationMiddleware(U.self)) - config.preparations.append(AdminPanelUser.self) + config.preparations.append(U.self) config.preparations.append(AdminPanelUserResetToken.self) - config.preparations.append(Action.self) + config.preparations.append(U.A.self) - config.addConfigurable(command: Seeder.init, name: "admin-panel:seeder") + config.addConfigurable(command: CustomUserSeeder.init, name: "admin-panel:seeder") try config.addProvider(AuditProvider.Provider.self) try config.addProvider(PaginatorProvider.self) } @@ -146,7 +151,7 @@ public final class Provider: Vapor.Provider { public func beforeRun(_ droplet: Droplet) throws {} } -extension Provider { +extension CustomUserProvider { public func registerLeafTags(_ renderer: LeafRenderer) { let stem = renderer.stem stem.register(Box()) diff --git a/Sources/AdminPanelProvider/Routes/AdminPanelUserRoutes.swift b/Sources/AdminPanelProvider/Routes/AdminPanelUserRoutes.swift index 2c3ffe27..5a3347a9 100644 --- a/Sources/AdminPanelProvider/Routes/AdminPanelUserRoutes.swift +++ b/Sources/AdminPanelProvider/Routes/AdminPanelUserRoutes.swift @@ -1,8 +1,10 @@ import HTTP import Vapor -public final class AdminPanelUserRoutes: RouteCollection { - public let controller: AdminPanelUserController +public typealias AdminPanelUserRoutes = CustomAdminPanelUserRoutes + +public final class CustomAdminPanelUserRoutes: RouteCollection { + public let controller: CustomAdminPanelUserController public init( renderer: ViewRenderer, @@ -10,7 +12,7 @@ public final class AdminPanelUserRoutes: RouteCollection { mailer: MailProtocol?, panelConfig: PanelConfig ) { - controller = AdminPanelUserController( + controller = CustomAdminPanelUserController( renderer: renderer, env: env, mailer: mailer, @@ -26,10 +28,10 @@ public final class AdminPanelUserRoutes: RouteCollection { admin.get("backend/users/create", handler: controller.create) admin.post("backend/users/store", handler: controller.store) - admin.get("backend/users/", AdminPanelUser.parameter, "edit", handler: controller.edit) - admin.post("backend/users/", AdminPanelUser.parameter, "edit", handler: controller.update) + admin.get("backend/users/", U.parameter, "edit", handler: controller.edit) + admin.post("backend/users/", U.parameter, "edit", handler: controller.update) - admin.post("backend/users/", AdminPanelUser.parameter, "delete", handler: controller.delete) + admin.post("backend/users/", U.parameter, "delete", handler: controller.delete) admin.get("backend/users/", Int.parameter, "restore", handler: controller.restore) admin.get("backend/users/logout", handler: controller.logout) From cf8d3b7d99d0dae8ae4c3147499c1a1c2be71590 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Thu, 19 Apr 2018 13:51:50 +0200 Subject: [PATCH 02/11] Fix routes and move functionality to user protocol --- .../AdminPanelUserController.swift | 14 ++--- .../Middlewares/ProtectMiddleware.swift | 2 +- .../Models/AdminPanelUser.swift | 58 +++++++++---------- Sources/AdminPanelProvider/Provider.swift | 12 ++-- .../Routes/LoginRoutes.swift | 8 ++- 5 files changed, 45 insertions(+), 49 deletions(-) diff --git a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift index 9908f0d0..fd2d39c2 100644 --- a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift +++ b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift @@ -26,7 +26,7 @@ public final class CustomAdminPanelUserController { } public func index(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(U.self) + let requestingUser: U = try req.auth.assertAuthenticated() try Gate.assertAllowed(requestingUser, requiredRole: .admin) let superAdmins = try U.makeQuery().filter(U.roleKey, "Super Admin").all() let admins = try U.makeQuery().filter(U.roleKey, "Admin").all() @@ -44,14 +44,14 @@ public final class CustomAdminPanelUserController { } public func create(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(U.self) + let requestingUser: U = try req.auth.assertAuthenticated() try Gate.assertAllowed(requestingUser, requiredRole: .admin) let fieldset = try req.fieldset ?? AdminPanelUserForm().makeNode(in: nil) return try renderer.make("AdminPanel/BackendUser/edit", ["fieldset": fieldset], for: req) } public func store(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(U.self) + let requestingUser: U = try req.auth.assertAuthenticated() try Gate.assertAllowed(requestingUser, requiredRole: .admin) do { @@ -149,7 +149,7 @@ public final class CustomAdminPanelUserController { return redirect("/admin/backend/users").flash(.error, "User not found") } - let requestingUser = try req.auth.assertAuthenticated(U.self) + let requestingUser: U = try req.auth.assertAuthenticated() let allowed = Gate.allow(requestingUser, requiredRole: .admin) || requestingUser.id == user.id guard allowed else { @@ -170,7 +170,7 @@ public final class CustomAdminPanelUserController { return redirect("/admin/backend/users").flash(.error, "User not found") } - let requestingUser = try req.auth.assertAuthenticated(U.self) + let requestingUser: U = try req.auth.assertAuthenticated() let allowed = Gate.allow(requestingUser, requiredRole: .admin) || requestingUser.id == user.id guard allowed else { @@ -262,7 +262,7 @@ public final class CustomAdminPanelUserController { } public func delete(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(U.self) + let requestingUser: U = try req.auth.assertAuthenticated() try Gate.assertAllowed(requestingUser, requiredRole: .admin) let user: U @@ -282,7 +282,7 @@ public final class CustomAdminPanelUserController { } public func restore(req: Request) throws -> ResponseRepresentable { - let requestingUser = try req.auth.assertAuthenticated(U.self) + let requestingUser: U = try req.auth.assertAuthenticated() try Gate.assertAllowed(requestingUser, requiredRole: .admin) let user: U diff --git a/Sources/AdminPanelProvider/Middlewares/ProtectMiddleware.swift b/Sources/AdminPanelProvider/Middlewares/ProtectMiddleware.swift index 4c623099..3815add3 100644 --- a/Sources/AdminPanelProvider/Middlewares/ProtectMiddleware.swift +++ b/Sources/AdminPanelProvider/Middlewares/ProtectMiddleware.swift @@ -8,7 +8,7 @@ public typealias ProtectMiddleware = CustomUserProtectMiddleware public final class CustomUserProtectMiddleware: Middleware { public func respond(to req: Request, chainingTo next: Responder) throws -> Response { do { - if let user = req.auth.authenticated(U.self), user.shouldResetPassword { + if let user: U = req.auth.authenticated(), user.shouldResetPassword { let redirectPath = "/admin/backend/users/\(user.id?.string ?? "0")/edit" if req.uri.path != redirectPath && req.uri.path.replacingOccurrences(of: "/", with: "") != redirectPath.replacingOccurrences(of: "/", with: "") { diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift index 2b8ab3fe..b2a85847 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift @@ -6,16 +6,15 @@ import AuditProvider import FluentProvider public protocol AdminPanelUserType: + AuditCustomDescribable, NodeRepresentable, Parameterizable, PasswordAuthenticatable, - Persistable, Preparation, + SessionPersistable, SoftDeletable, ViewDataRepresentable { - associatedtype A: Preparation - init( name: String, title: String, @@ -27,6 +26,7 @@ public protocol AdminPanelUserType: ) throws var avatar: String? { get set } + var avatarUrl: String { get } var email: String { get set } var name: String { get set } var password: String { get set } @@ -39,8 +39,12 @@ public protocol AdminPanelUserType: /// database key name for `email` property static var emailKey: String { get } +} - func updatePassword(_ newPass: String) throws +extension AdminPanelUserType { + public var avatarUrl: String { + return avatar ?? "https://api.adorable.io/avatars/150/\(email).png" + } } extension AdminPanelUserType { @@ -48,10 +52,26 @@ extension AdminPanelUserType { public static var emailKey: String { return "email" } } -extension AdminPanelUser: AdminPanelUserType { - public typealias A = Action +extension AdminPanelUserType { + public func updatePassword(_ newPass: String) throws { + password = try BCryptHasher().make(newPass.makeBytes()).makeString() + try save() + } + + public static func authenticate(_ credentials: Password) throws -> Self { + guard + let user = try makeQuery().filter(emailKey, credentials.username).first(), + try BCryptHasher().check(credentials.password, matchesHash: user.password) + else { + throw Abort.unauthorized + } + + return user + } } +extension AdminPanelUser: AdminPanelUserType {} + public final class AdminPanelUser: Model { public let storage = Storage() @@ -63,10 +83,6 @@ public final class AdminPanelUser: Model { public var shouldResetPassword: Bool public var title: String - public var avatarUrl: String { - return avatar ?? "https://api.adorable.io/avatars/150/\(email).png" - } - public init( name: String, title: String, @@ -110,13 +126,6 @@ public final class AdminPanelUser: Model { } } -extension AdminPanelUser { - public func updatePassword(_ newPass: String) throws { - password = try BCryptHasher().make(newPass.makeBytes()).makeString() - try save() - } -} - extension AdminPanelUser: ViewDataRepresentable { public func makeViewData() throws -> ViewData { return try ViewData(viewData: [ @@ -145,8 +154,6 @@ extension AdminPanelUser: NodeRepresentable { extension AdminPanelUser: Author {} extension AdminPanelUser: Timestampable {} -extension AdminPanelUser: SoftDeletable {} -extension AdminPanelUser: SessionPersistable {} extension AdminPanelUser: Preparation { public static func prepare(_ database: Database) throws { try database.create(self) { @@ -165,18 +172,7 @@ extension AdminPanelUser: Preparation { try database.delete(self) } } -extension AdminPanelUser: PasswordAuthenticatable { - public static func authenticate(_ credentials: Password) throws -> AdminPanelUser { - guard - let user = try makeQuery().filter(AdminPanelUser.emailKey, credentials.username).first(), - try BCryptHasher().check(credentials.password, matchesHash: user.password) - else { - throw Abort.unauthorized - } - return user - } -} extension AdminPanelUser: AuditCustomDescribable { public static var auditDescription: String { return "User" @@ -195,6 +191,4 @@ extension AdminPanelUser { return "should_reset_password" } } - } - diff --git a/Sources/AdminPanelProvider/Provider.swift b/Sources/AdminPanelProvider/Provider.swift index f4cc51a1..ca09e03f 100644 --- a/Sources/AdminPanelProvider/Provider.swift +++ b/Sources/AdminPanelProvider/Provider.swift @@ -90,18 +90,18 @@ public final class CustomUserProvider: Vapor.Provider { public func boot(_ config: Config) throws { try Middlewares.unsecured.append(PanelConfigMiddleware(panelConfig)) - Middlewares.unsecured.append(PersistMiddleware(U.self)) + Middlewares.unsecured.append(PersistMiddleware()) Middlewares.unsecured.append(FlashMiddleware()) Middlewares.unsecured.append(FieldsetMiddleware()) Middlewares.unsecured.append(CustomUserActivityMiddleware()) Middlewares.secured = Middlewares.unsecured Middlewares.secured.append(CustomUserProtectMiddleware()) - Middlewares.secured.append(PasswordAuthenticationMiddleware(U.self)) + Middlewares.secured.append(PasswordAuthenticationMiddleware()) config.preparations.append(U.self) config.preparations.append(AdminPanelUserResetToken.self) - config.preparations.append(U.A.self) + config.preparations.append(CustomUserAction.self) config.addConfigurable(command: CustomUserSeeder.init, name: "admin-panel:seeder") try config.addProvider(AuditProvider.Provider.self) @@ -122,13 +122,13 @@ public final class CustomUserProvider: Vapor.Provider { mailer = nil } - let loginController = LoginController( + let loginController = CustomUserLoginController( renderer: renderer, mailer: mailer, panelConfig: panelConfig ) - let loginCollection = LoginRoutes(controller: loginController) + let loginCollection = CustomUserLoginRoutes(controller: loginController) try droplet.collection(loginCollection) let panelRoutes = PanelRoutes( @@ -138,7 +138,7 @@ public final class CustomUserProvider: Vapor.Provider { ) try droplet.collection(panelRoutes) - let bUserRoutes = AdminPanelUserRoutes( + let bUserRoutes = CustomAdminPanelUserRoutes( renderer: renderer, env: droplet.config.environment, mailer: mailer, diff --git a/Sources/AdminPanelProvider/Routes/LoginRoutes.swift b/Sources/AdminPanelProvider/Routes/LoginRoutes.swift index 9de73a21..d80b2a77 100644 --- a/Sources/AdminPanelProvider/Routes/LoginRoutes.swift +++ b/Sources/AdminPanelProvider/Routes/LoginRoutes.swift @@ -2,10 +2,12 @@ import HTTP import Vapor import AuthProvider -public final class LoginRoutes: RouteCollection { - public let controller: LoginController +public typealias LoginRoutes = CustomUserLoginRoutes - public init(controller: LoginController) { +public final class CustomUserLoginRoutes: RouteCollection { + public let controller: CustomUserLoginController + + public init(controller: CustomUserLoginController) { self.controller = controller } From b472d9c494c5c8a4eca26f7976c7d60a10b37907 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Thu, 19 Apr 2018 14:18:42 +0200 Subject: [PATCH 03/11] Small cleanups --- .../Controllers/AdminPanelUserController.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift index fd2d39c2..f68bb33d 100644 --- a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift +++ b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift @@ -88,13 +88,13 @@ public final class CustomAdminPanelUserController { avatar = path } - let randomPassword = form.password.isEmpty ? String.random(12) : form.password + let password = form.password.isEmpty ? String.random(12) : form.password let user = try U( name: form.name, title: form.title, email: form.email, - password: randomPassword, + password: password, role: form.role, shouldResetPassword: form.shouldResetPassword, avatar: avatar @@ -114,8 +114,8 @@ public final class CustomAdminPanelUserController { "url": .string(panelConfig.baseUrl) ] - if !randomPassword.isEmpty { - context["password"] = .string(randomPassword) + if !password.isEmpty { + context["password"] = .string(password) } mailer?.sendEmail( @@ -144,7 +144,7 @@ public final class CustomAdminPanelUserController { public func edit(req: Request) throws -> ResponseRepresentable { let user: U do { - user = try req.parameters.next(U.self) + user = try req.parameters.next() } catch { return redirect("/admin/backend/users").flash(.error, "User not found") } @@ -165,7 +165,7 @@ public final class CustomAdminPanelUserController { do { var user: U do { - user = try req.parameters.next(U.self) + user = try req.parameters.next() } catch { return redirect("/admin/backend/users").flash(.error, "User not found") } @@ -267,7 +267,7 @@ public final class CustomAdminPanelUserController { let user: U do { - user = try req.parameters.next(U.self) + user = try req.parameters.next() } catch { return redirect("/admin/backend/users").flash(.error, "User not found") } From af3c6f9a81deb5901fe75d6c65e3deea4b67afe5 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Fri, 20 Apr 2018 16:42:33 +0200 Subject: [PATCH 04/11] Use Forms --- .../AdminPanelProvider/Commands/Seeder.swift | 10 +- .../AdminPanelUserController.swift | 151 +++----- .../Controllers/LoginController.swift | 4 +- .../Helpers/UniqueEntityValidator.swift | 47 +++ .../Models/AdminPanelUser.swift | 168 +++++++-- .../Models/AdminPanelUserForm.swift | 342 +++++++----------- .../Tags/Leaf/Request+Active.swift | 1 - 7 files changed, 372 insertions(+), 351 deletions(-) create mode 100644 Sources/AdminPanelProvider/Helpers/UniqueEntityValidator.swift diff --git a/Sources/AdminPanelProvider/Commands/Seeder.swift b/Sources/AdminPanelProvider/Commands/Seeder.swift index 4519739b..600e6041 100644 --- a/Sources/AdminPanelProvider/Commands/Seeder.swift +++ b/Sources/AdminPanelProvider/Commands/Seeder.swift @@ -20,15 +20,7 @@ public final class CustomUserSeeder: Command, ConfigIniti public func run(arguments: [String]) throws { console.info("Started the seeder") - let user = try U( - name: "Admin", - title: "Default admin account", - email: "admin@admin.com", - password: "admin", - role: "Super Admin", - shouldResetPassword: false, - avatar: nil - ) + let user = try U.makeSeededUser() try user.save() diff --git a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift index f68bb33d..ae40921e 100644 --- a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift +++ b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift @@ -46,8 +46,12 @@ public final class CustomAdminPanelUserController { public func create(req: Request) throws -> ResponseRepresentable { let requestingUser: U = try req.auth.assertAuthenticated() try Gate.assertAllowed(requestingUser, requiredRole: .admin) - let fieldset = try req.fieldset ?? AdminPanelUserForm().makeNode(in: nil) - return try renderer.make("AdminPanel/BackendUser/edit", ["fieldset": fieldset], for: req) + let fieldset = try req.fieldset ?? AdminPanelUserForm().makeFieldset(inValidationMode: .none) + + return try renderer.make( + "AdminPanel/BackendUser/edit", + ViewData([.fieldset: fieldset, .request: req]) + ) } public func store(req: Request) throws -> ResponseRepresentable { @@ -55,55 +59,30 @@ public final class CustomAdminPanelUserController { try Gate.assertAllowed(requestingUser, requiredRole: .admin) do { - let (form, hasErrors) = AdminPanelUserForm.validating(req.data) - let isEmailUnique = try U.makeQuery().filter("email", form.email).first() == nil + let form = try U.Form.init(request: req) - if hasErrors || !isEmailUnique { - let response = redirect("/admin/backend/users/create") + guard form.isValid(inValidationMode: .all) else { + return try redirect("/admin/backend/users/create") .flash(.error, "Validation error") - var fieldset = try form.makeNode(in: nil) - - if (!isEmailUnique) { - try fieldset.set( - "email", - try Node(node: [ - "label": "Email", - "value": .string(form.email), - "errors": Node(node: ["Provided email already exists."]) - ]) - ) - } - - response.fieldset = fieldset - return response - } - - var avatar: String? = nil - if - let profileImage = req.data["profileImage"]?.string, - profileImage.hasPrefix("data:"), - panelConfig.isStorageEnabled - { - let path = try Storage.upload(dataURI: profileImage, folder: "profile") - avatar = path + .setFieldset(form.makeFieldset(inValidationMode: .all)) } - let password = form.password.isEmpty ? String.random(12) : form.password - - let user = try U( - name: form.name, - title: form.title, - email: form.email, - password: password, - role: form.role, - shouldResetPassword: form.shouldResetPassword, - avatar: avatar + let user = try U.init( + form: form, + panelConfig: panelConfig, + req: req ) + var randomPassword: String? + if form.password == nil || form.password?.isEmpty == true { + randomPassword = String.random(12) + user.password = try U.hashPassword(randomPassword!) // safe to force unwrap here + } + try user.save() if - form.sendEmail, + try req.data.get("shouldSendEmail") ?? false, panelConfig.isEmailEnabled, let name = panelConfig.fromName, let email = panelConfig.fromEmail @@ -114,8 +93,8 @@ public final class CustomAdminPanelUserController { "url": .string(panelConfig.baseUrl) ] - if !password.isEmpty { - context["password"] = .string(password) + if let randomPassword = randomPassword { + context["password"] = .string(randomPassword) } mailer?.sendEmail( @@ -156,8 +135,11 @@ public final class CustomAdminPanelUserController { throw Abort.notFound } - let fieldset = try req.fieldset ?? AdminPanelUserForm().makeNode(in: nil) - return try renderer.make("AdminPanel/BackendUser/edit", ["user": user, "fieldset": fieldset], for: req) + let fieldset = try req.fieldset ?? user.makeForm().makeFieldset(inValidationMode: .none) + return try renderer.make( + "AdminPanel/BackendUser/edit", + ViewData(["user": user, .fieldset: fieldset, .request: req]) + ) } public func update(req: Request) throws -> ResponseRepresentable { @@ -177,74 +159,35 @@ public final class CustomAdminPanelUserController { throw Abort.notFound } - // users already have a role, so we don't care if they don't/can't update it - let (form, hasErrors) = AdminPanelUserForm.validating(req.data, ignoreRole: true) + let form = try U.Form.init(request: req) - if - let userByEmail = try U.makeQuery().filter(U.emailKey, form.email).first(), - userByEmail.id != user.id - { - let response = redirect("/admin/backend/users/\(user.id?.string ?? "0")/edit/") + if !form.isValid(inValidationMode: .nonNil) { + return redirect("/admin/backend/users/\(user.id?.string ?? "0")/edit/") .flash(.error, "Validation error") - var fieldset = try form.makeNode(in: nil) - - try fieldset.set( - "email", - try Node(node: [ - "label": "Email", - "value": .string(form.email), - "errors": Node(node: ["Provided email already exists."]) - ]) - ) - - response.fieldset = fieldset - return response - } - - if hasErrors { - let response = redirect("/admin/backend/users/\(user.id?.string ?? "0")/edit/") - .flash(.error, "Validation error") - let fieldset = try form.makeNode(in: nil) - response.fieldset = fieldset - return response + .setFieldset(try form.makeFieldset(inValidationMode: .nonNil)) } - user.name = form.name - user.title = form.title - user.email = form.email - - let formPasswordHash = try BCryptHasher().make(form.password.makeBytes()).makeString() - if user.shouldResetPassword { - guard formPasswordHash != user.password else { - let response = redirect("/admin/backend/users/\(user.id?.string ?? "0")/edit/") - .flash(.error, "Please pick a new password") - let fieldset = try form.makeNode(in: nil) - response.fieldset = fieldset - return response + // Users aren't allowed to change their own role + if let role = form.role, requestingUser.id != user.id { + // is the requesting user allowed to select this role? + if Gate.allow(requestingUser.role, requiredRole: role) { + user.role = role } - - user.shouldResetPassword = false } - if !form.password.isEmpty { - user.password = formPasswordHash - } + try user.updateNonPasswordValues(form: form, panelConfig: panelConfig, req: req) - // Users aren't allowed to change their own role - if requestingUser.id != user.id { - // is the requesting user allowed to select this role? - if Gate.allow(requestingUser.role, requiredRole: form.role) { - user.role = form.role - } + let passwordHash = try form.password.map(U.hashPassword) + + if user.shouldResetPassword, passwordHash == user.password || passwordHash == nil { + return try redirect("/admin/backend/users/\(user.id?.string ?? "0")/edit/") + .flash(.error, "Please pick a new password") + .setFieldset(form.makeFieldset(inValidationMode: .nonNil)) } - if - let profileImage = req.data["profileImage"]?.string, - profileImage.hasPrefix("data:"), - panelConfig.isStorageEnabled - { - let path = try Storage.upload(dataURI: profileImage, folder: "profile") - user.avatar = path + if let passwordHash = passwordHash { + user.password = passwordHash + user.shouldResetPassword = false } try user.save() diff --git a/Sources/AdminPanelProvider/Controllers/LoginController.swift b/Sources/AdminPanelProvider/Controllers/LoginController.swift index 9ca03137..70288ced 100644 --- a/Sources/AdminPanelProvider/Controllers/LoginController.swift +++ b/Sources/AdminPanelProvider/Controllers/LoginController.swift @@ -157,7 +157,9 @@ public final class CustomUserLoginController { return redirect("/admin/login").flash(.error, "User not found") } - try user.updatePassword(password) + let hashedPassword = try U.hashPassword(password) + user.password = hashedPassword + try user.save() try token.use() return redirect("/admin/login").flash(.success, "Password reset") } diff --git a/Sources/AdminPanelProvider/Helpers/UniqueEntityValidator.swift b/Sources/AdminPanelProvider/Helpers/UniqueEntityValidator.swift new file mode 100644 index 00000000..d68cf7ab --- /dev/null +++ b/Sources/AdminPanelProvider/Helpers/UniqueEntityValidator.swift @@ -0,0 +1,47 @@ +import Fluent +import Forms +import Validation + +internal final class UniqueEntityValidator: Validator { + typealias CountEntities = ( + _ fieldName: String, + _ value: String, + _ exceptId: Identifier? + ) throws -> Int + + private let fieldName: String + private let exceptId: Identifier? + private let countOfEntities: CountEntities + private let errorOnExist: FormFieldValidationError + + internal init( + fieldName: String, + exceptId: Identifier?, + countOfEntities: @escaping CountEntities, + errorOnExist: FormFieldValidationError + ) { + self.countOfEntities = countOfEntities + self.errorOnExist = errorOnExist + self.exceptId = exceptId + self.fieldName = fieldName + } + + internal func validate(_ input: String) throws { + guard try countOfEntities(fieldName, input, exceptId) == 0 else { + throw errorOnExist + } + } +} + +extension Entity { + static func countOfEntities( + where fieldName: String, + equals input: String, + exceptId: Identifier? + ) throws -> Int { + return try makeQuery() + .filter(fieldName, input) + .filter(idKey, .notEquals, exceptId) + .count() + } +} diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift index b2a85847..f755d562 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift @@ -1,9 +1,15 @@ -import Vapor -import BCrypt -import Storage -import AuthProvider import AuditProvider +import AuthProvider +import BCrypt import FluentProvider +import Forms +import Storage +import Vapor + +public protocol AdminPanelUserFormType: Form { + var role: String? { get } + var password: String? { get } +} public protocol AdminPanelUserType: AuditCustomDescribable, @@ -15,24 +21,36 @@ public protocol AdminPanelUserType: SoftDeletable, ViewDataRepresentable { - init( - name: String, - title: String, - email: String, - password: String, - role: String, - shouldResetPassword: Bool, - avatar: String? - ) throws + static func makeSeededUser() throws -> Self + static func makeSSOUser(withEmail: String) throws -> Self + + associatedtype Form: AdminPanelUserFormType, RequestInitializable + + /// Create a new user with the values from the form + /// + /// - Parameters: + /// - form: form with user values + /// - panelConfig: panel configuration + /// - req: the request + init(form: Form, panelConfig: PanelConfig?, req: Request?) throws + + /// Should update any fields which have corresponding values in the form except password. + /// + /// - Parameters: + /// - form: form with updated values + /// - panelConfig: panel configuration + /// - req: the request + func updateNonPasswordValues(form: Form, panelConfig: PanelConfig?, req: Request?) throws + func makeForm() -> Form + + static func hashPassword(_: String) throws -> String - var avatar: String? { get set } - var avatarUrl: String { get } var email: String { get set } var name: String { get set } - var password: String { get set } var role: String { get set } + + var password: String { get set } var shouldResetPassword: Bool { get set } - var title: String { get set } /// database key name for `role` property static var roleKey: String { get } @@ -41,21 +59,12 @@ public protocol AdminPanelUserType: static var emailKey: String { get } } -extension AdminPanelUserType { - public var avatarUrl: String { - return avatar ?? "https://api.adorable.io/avatars/150/\(email).png" - } -} - extension AdminPanelUserType { public static var roleKey: String { return "role" } public static var emailKey: String { return "email" } -} -extension AdminPanelUserType { - public func updatePassword(_ newPass: String) throws { - password = try BCryptHasher().make(newPass.makeBytes()).makeString() - try save() + public static func hashPassword(_ password: String) throws -> String { + return try BCryptHasher().make(password.makeBytes()).makeString() } public static func authenticate(_ credentials: Password) throws -> Self { @@ -70,7 +79,104 @@ extension AdminPanelUserType { } } -extension AdminPanelUser: AdminPanelUserType {} +extension AdminPanelUser: AdminPanelUserType { + public static func makeSeededUser() throws -> AdminPanelUser { + return try .init( + name: "Admin", + title: "Default admin account", + email: "admin@admin.com", + password: "admin", + role: "Super Admin", + shouldResetPassword: false, + avatar: nil + ) + } + + public static func makeSSOUser(withEmail email: String) throws -> AdminPanelUser { + return try .init( + name: "Admin", + title: "Nodes Admin", + email: email, + password: String.random(16), + role: "Super Admin", + shouldResetPassword: false, + avatar: nil + ) + } + + public convenience init( + form: AdminPanelUserForm, + panelConfig: PanelConfig?, + req: Request? + ) throws { + let values = try form.assertValues() + + // extract avatar from request + var avatar: String? = nil + if + let req = req, + let panelConfig = panelConfig, + let profileImage = req.data["profileImage"]?.string, + profileImage.hasPrefix("data:"), + panelConfig.isStorageEnabled + { + let path = try Storage.upload(dataURI: profileImage, folder: "profile") + avatar = path + } + + let newPassword: String + let shouldResetPassword: Bool + + if let password = form.password, !password.isEmpty { + newPassword = password + shouldResetPassword = form.shouldResetPassword + } else { + newPassword = "" // this will be overwritten by the controller! + shouldResetPassword = true + } + + try self.init( + name: values.name, + title: values.title, + email: values.email, + password: AdminPanelUser.hashPassword(newPassword), + role: values.role, + shouldResetPassword: shouldResetPassword, + avatar: avatar + ) + } + + public func updateNonPasswordValues( + form: AdminPanelUserForm, + panelConfig: PanelConfig?, + req: Request? + ) throws { + if let name = form.name { + self.name = name + } + if let title = form.title { + self.title = title + } + if let email = form.email { + self.email = email + } + + if + let req = req, + let panelConfig = panelConfig, + let profileImage = req.data["profileImage"]?.string, + profileImage.hasPrefix("data:"), + panelConfig.isStorageEnabled + { + let path = try Storage.upload(dataURI: profileImage, folder: "profile") + avatar = path + } + } + + public func makeForm() -> AdminPanelUserForm { + return AdminPanelUserForm(user: self) + } +} public final class AdminPanelUser: Model { public let storage = Storage() @@ -127,6 +233,10 @@ public final class AdminPanelUser: Model { } extension AdminPanelUser: ViewDataRepresentable { + public var avatarUrl: String { + return avatar ?? "https://api.adorable.io/avatars/150/\(email).png" + } + public func makeViewData() throws -> ViewData { return try ViewData(viewData: [ "id": .string(id?.string ?? "0"), diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift index 9319b7c7..32361edb 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift @@ -1,231 +1,159 @@ +import Forms +import Validation import Vapor -public struct AdminPanelUserForm { - public let name: String - public let nameErrors: [String] - public let email: String - public let emailErrors: [String] - public let title: String - public let titleErrors: [String] - public let role: String - public let roleErrors: [String] - public let password: String - public let passwordErrors: [String] - public let passwordRepeat: String - public let passwordRepeatErrors: [String] - - public let shouldResetPassword: Bool - public let sendEmail: Bool - - public init( +// sourcery: form +public struct AdminPanelUserForm: AdminPanelUserFormType { + public let nameField: FormField + public let emailField: FormField + public let passwordField: FormField + public let titleField: FormField + public let roleField: FormField + public let shouldResetPasswordField: FormField + + init( + userId: Identifier? = nil, name: String? = nil, - nameErrors: [String] = [], email: String? = nil, - emailErrors: [String] = [], + password: String? = nil, title: String? = nil, - titleErrors: [String] = [], role: String? = nil, - roleErrors: [String] = [], - password: String? = nil, - passwordErrors: [String] = [], - passwordRepeat: String? = nil, - passwordRepeatErrors: [String] = [], - shouldResetPassword: Bool? = nil, - sendEmail: Bool? = nil + avatar: String? = nil, + shouldResetPassword: Bool = false ) { - self.name = name ?? "" - self.nameErrors = nameErrors - self.email = email ?? "" - self.emailErrors = emailErrors - self.title = title ?? "" - self.titleErrors = titleErrors - self.role = role ?? "" - self.roleErrors = roleErrors - self.password = password ?? "" - self.passwordErrors = passwordErrors - self.passwordRepeat = passwordRepeat ?? "" - self.passwordRepeatErrors = passwordRepeatErrors - self.shouldResetPassword = shouldResetPassword ?? false - self.sendEmail = sendEmail ?? false + let stringLengthValidator = Count.containedIn(low: 1, high: 191) + + let emailValidator = EmailValidator() && UniqueEntityValidator( + fieldName: "email", + exceptId: userId, + countOfEntities: AdminPanelUser.countOfEntities, + errorOnExist: ValidatorError.failure( + type: "User email", + reason: "Provided email already exists." + ) + ) + + nameField = FormField( + key: "name", + label: "Name", + value: name, + validator: stringLengthValidator.allowingNil(false) + ) + emailField = FormField( + key: "email", + label: "Email", + value: email, + validator: emailValidator.allowingNil(false) + ) + // TODO: add more password restrictions + let passwordValidator = Count.containedIn(low: 6, high: 191) + passwordField = FormField( + key: "password", + label: "Password", + value: password, + validator: passwordValidator.allowingNil(true) + ) + titleField = FormField( + key: "title", + label: "Title", + value: title, + validator: stringLengthValidator.allowingNil(false) + ) + roleField = FormField( + key: "role", + label: "Role", + value: role, + validator: stringLengthValidator.allowingNil(false) + ) + shouldResetPasswordField = FormField( + key: "shouldResetPassword", + label: "Should Reset Password", + value: shouldResetPassword + ) } } extension AdminPanelUserForm { - public static func validating(_ data: Content, ignoreRole: Bool = false) -> (AdminPanelUserForm, Bool) { - let name = data["name"]?.string - let email = data[AdminPanelUser.emailKey]?.string - let title = data["title"]?.string - let role = data[AdminPanelUser.roleKey]?.string - let shouldResetPassword = data["shouldResetPassword"]?.string != nil - let sendEmail = data["sendEmail"]?.string != nil - let password = data["password"]?.string - let passwordRepeat = data["passwordRepeat"]?.string - - return validate( - name: name, - email: email, - title: title, - role: role, - shouldResetPassword: shouldResetPassword, - sendEmail: sendEmail, - password: password, - passwordRepeat: passwordRepeat, - ignoreRole: ignoreRole - ) + public var fields: [FieldType] { + return [ + nameField, + emailField, + passwordField, + titleField, + roleField, + shouldResetPasswordField + ] } +} - public static func validate( - name: String?, - email: String?, - title: String?, - role: String?, - shouldResetPassword: Bool?, - sendEmail: Bool?, - password: String?, - passwordRepeat: String?, - ignoreRole: Bool - ) -> (AdminPanelUserForm, Bool) { - var hasErrors = false - - var nameErrors: [String] = [] - var emailErrors: [String] = [] - var titleErrors: [String] = [] - var passwordErrors: [String] = [] - var passwordRepeatErrors: [String] = [] - var roleErrors: [String] = [] - - let requiredFieldError = "Field is required" - if name == nil { - nameErrors.append(requiredFieldError) - hasErrors = true - } - - if email == nil { - emailErrors.append(requiredFieldError) - hasErrors = true - } - - if title == nil { - titleErrors.append(requiredFieldError) - hasErrors = true - } - - if role == nil && !ignoreRole { - roleErrors.append(requiredFieldError) - hasErrors = true - } - - let nameCharactercount = name?.utf8.count ?? 0 - if nameCharactercount < 1 || nameCharactercount > 191 { - nameErrors.append("Must be between 1 and 191 characters long") - hasErrors = true - } - - let emailCharactercount = email?.utf8.count ?? 0 - if emailCharactercount < 1 || emailCharactercount > 191 { - emailErrors.append("Must be between 1 and 191 characters long") - hasErrors = true - } - - if let password = password, !password.isEmpty { - let passwordCharactercount = password.utf8.count - if passwordCharactercount < 8 || passwordCharactercount > 191 { - passwordErrors.append("Must be between 8 and 191 characters long") - hasErrors = true - } - } - - if let passwordRepeat = passwordRepeat, !passwordRepeat.isEmpty { - let passwordRepeatCharacterCount = passwordRepeat.utf8.count - if passwordRepeatCharacterCount < 8 || passwordRepeatCharacterCount > 191 { - passwordRepeatErrors.append("Must be between 8 and 191 characters long") - hasErrors = true - } - } +extension AdminPanelUserForm { + public var name: String? { + return nameField.value + } + public var email: String? { + return emailField.value + } + public var password: String? { + return passwordField.value + } + public var title: String? { + return titleField.value + } + public var role: String? { + return roleField.value + } + public var shouldResetPassword: Bool { + return shouldResetPasswordField.value ?? false + } +} - if password != passwordRepeat { - passwordRepeatErrors.append("Passwords do not match") - hasErrors = true +extension AdminPanelUserForm { + public func assertValues(errorOnNil: Error = Abort(.internalServerError)) throws -> ( + name: String, + email: String, + title: String, + role: String + ) { + guard + let name = name, + let email = email, + let title = title, + let role = role + else { + throw errorOnNil } return ( - AdminPanelUserForm( - name: name, - nameErrors: nameErrors, - email: email, - emailErrors: emailErrors, - title: title, - titleErrors: titleErrors, - role: role, - roleErrors: roleErrors, - password: password, - passwordErrors: passwordErrors, - passwordRepeat: passwordRepeat, - passwordRepeatErrors: passwordRepeatErrors, - shouldResetPassword: shouldResetPassword, - sendEmail: sendEmail - ), - hasErrors + name: name, + email: email, + title: title, + role: role ) } } -extension AdminPanelUserForm: NodeRepresentable { - public func makeNode(in context: Context?) throws -> Node { - let nameObj = try Node(node: [ - "label": "Name", - "value": .string(name), - "errors": Node(node: nameErrors) - ]) - - let emailObj = try Node(node: [ - "label": "Email", - "value": .string(email), - "errors": Node(node: emailErrors) - ]) - - let titleObj = try Node(node: [ - "label": "Title", - "value": .string(title), - "errors": Node(node: titleErrors) - ]) - - let roleObj = try Node(node: [ - "label": "Role", - "value": .string(role), - "errors": Node(node: roleErrors) - ]) - - let shouldResetPasswordObj = Node(node: [ - "label": "Should reset password", - "value": .bool(shouldResetPassword) - ]) - - let sendEmailObj = Node(node: [ - "label": "Send email with information", - "value": .bool(sendEmail) - ]) - - let passwordObj = try Node(node: [ - "label": "Password", - "errors": Node(node: passwordErrors) - ]) - - let passwordRepeatObj = try Node(node: [ - "label": "Repeat password", - "errors": Node(node: passwordRepeatErrors) - ]) - - var node = Node.object([:]) - try node.set("name", nameObj) - try node.set("email", emailObj) - try node.set("title", titleObj) - try node.set(AdminPanelUser.roleKey, roleObj) - try node.set("shouldResetPassword", shouldResetPasswordObj) - try node.set("sendEmail", sendEmailObj) - try node.set("password", passwordObj) - try node.set("passwordRepeat", passwordRepeatObj) +extension AdminPanelUserForm: RequestInitializable { + public init(request: Request) throws { + let content = request.data + try self.init( + name: content.get("name"), + email: content.get("email"), + password: content.get("password"), + title: content.get("title"), + role: content.get("role"), + shouldResetPassword: content.get("shouldResetPassword") + ) + } +} - return node +extension AdminPanelUserForm { + public init(user: AdminPanelUser) { + self.init( + name: user.name, + email: user.email, + title: user.title, + role: user.role, + shouldResetPassword: user.shouldResetPassword + ) } } diff --git a/Sources/AdminPanelProvider/Tags/Leaf/Request+Active.swift b/Sources/AdminPanelProvider/Tags/Leaf/Request+Active.swift index 28a37b56..ae1e0977 100644 --- a/Sources/AdminPanelProvider/Tags/Leaf/Request+Active.swift +++ b/Sources/AdminPanelProvider/Tags/Leaf/Request+Active.swift @@ -25,7 +25,6 @@ extension Request { } } - return false } } From 513b15c2fda99337a0e884c69c6a7b8115525b02 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Fri, 20 Apr 2018 17:16:15 +0200 Subject: [PATCH 05/11] Add send email to form --- .../AdminPanelUserController.swift | 2 +- .../Models/AdminPanelUser.swift | 3 ++- .../Models/AdminPanelUserForm.swift | 20 +++++++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift index ae40921e..d83f0354 100644 --- a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift +++ b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift @@ -82,7 +82,7 @@ public final class CustomAdminPanelUserController { try user.save() if - try req.data.get("shouldSendEmail") ?? false, + form.shouldSendEmail, panelConfig.isEmailEnabled, let name = panelConfig.fromName, let email = panelConfig.fromEmail diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift index f755d562..65a78e34 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift @@ -7,8 +7,9 @@ import Storage import Vapor public protocol AdminPanelUserFormType: Form { - var role: String? { get } var password: String? { get } + var role: String? { get } + var shouldSendEmail: Bool { get } } public protocol AdminPanelUserType: diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift index 32361edb..3f368564 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift @@ -10,6 +10,7 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { public let titleField: FormField public let roleField: FormField public let shouldResetPasswordField: FormField + public let shouldSendEmailField: FormField init( userId: Identifier? = nil, @@ -19,7 +20,8 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { title: String? = nil, role: String? = nil, avatar: String? = nil, - shouldResetPassword: Bool = false + shouldResetPassword: Bool = false, + shouldSendEmail: Bool = false ) { let stringLengthValidator = Count.containedIn(low: 1, high: 191) @@ -46,7 +48,7 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { validator: emailValidator.allowingNil(false) ) // TODO: add more password restrictions - let passwordValidator = Count.containedIn(low: 6, high: 191) + let passwordValidator = Count.containedIn(low: 8, high: 191) passwordField = FormField( key: "password", label: "Password", @@ -70,6 +72,11 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { label: "Should Reset Password", value: shouldResetPassword ) + shouldSendEmailField = FormField( + key: "shouldSendEmail", + label: "Send Email with Info", + value: shouldSendEmail + ) } } @@ -81,7 +88,8 @@ extension AdminPanelUserForm { passwordField, titleField, roleField, - shouldResetPasswordField + shouldResetPasswordField, + shouldSendEmailField ] } } @@ -105,6 +113,9 @@ extension AdminPanelUserForm { public var shouldResetPassword: Bool { return shouldResetPasswordField.value ?? false } + public var shouldSendEmail: Bool { + return shouldSendEmailField.value ?? false + } } extension AdminPanelUserForm { @@ -141,7 +152,8 @@ extension AdminPanelUserForm: RequestInitializable { password: content.get("password"), title: content.get("title"), role: content.get("role"), - shouldResetPassword: content.get("shouldResetPassword") + shouldResetPassword: content.get("shouldResetPassword"), + shouldSendEmail: content.get("shouldSendEmail") ) } } From 309f7b0c2e5dc8185cfc7b1894ebb844bd369984 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Mon, 23 Apr 2018 11:28:37 +0200 Subject: [PATCH 06/11] Allow 0 length passwords (random will be created) --- Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift index 3f368564..1f87ddb4 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift @@ -48,7 +48,7 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { validator: emailValidator.allowingNil(false) ) // TODO: add more password restrictions - let passwordValidator = Count.containedIn(low: 8, high: 191) + let passwordValidator = Count.equals(0) || Count.containedIn(low: 8, high: 191) passwordField = FormField( key: "password", label: "Password", From 041787111c1ea8d17d627312f048749fdabf4b4e Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Mon, 23 Apr 2018 11:33:09 +0200 Subject: [PATCH 07/11] Fix permissions on create/edit user actions --- .../AdminPanelUserController.swift | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift index d83f0354..fa8bf262 100644 --- a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift +++ b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift @@ -67,6 +67,12 @@ public final class CustomAdminPanelUserController { .setFieldset(form.makeFieldset(inValidationMode: .all)) } + guard let role = form.role, Gate.allow(requestingUser.role, requiredRole: role) else { + return try redirect("/admin/backend/users/create") + .flash(.error, "Cannot create user with higher role than yourself.") + .setFieldset(form.makeFieldset(inValidationMode: .all)) + } + let user = try U.init( form: form, panelConfig: panelConfig, @@ -129,7 +135,10 @@ public final class CustomAdminPanelUserController { } let requestingUser: U = try req.auth.assertAuthenticated() - let allowed = Gate.allow(requestingUser, requiredRole: .admin) || requestingUser.id == user.id + let allowed = requestingUser.id == user.id || + Gate.allow(requestingUser.role, requiredRole: user.role) && + Gate.allow(requestingUser, requiredRole: .admin) + guard allowed else { throw Abort.notFound @@ -143,7 +152,6 @@ public final class CustomAdminPanelUserController { } public func update(req: Request) throws -> ResponseRepresentable { - do { var user: U do { @@ -153,7 +161,9 @@ public final class CustomAdminPanelUserController { } let requestingUser: U = try req.auth.assertAuthenticated() - let allowed = Gate.allow(requestingUser, requiredRole: .admin) || requestingUser.id == user.id + let allowed = requestingUser.id == user.id || + Gate.allow(requestingUser.role, requiredRole: user.role) && + Gate.allow(requestingUser, requiredRole: .admin) guard allowed else { throw Abort.notFound @@ -219,6 +229,14 @@ public final class CustomAdminPanelUserController { return redirect("/admin/backend/users").flash(.error, "Cannot delete yourself") } + let allowed = + Gate.allow(requestingUser.role, requiredRole: user.role) && + Gate.allow(requestingUser, requiredRole: .admin) + guard allowed else { + return redirect("/admin/backend/users") + .flash(.error, "Cannot delete user with a higher role.") + } + try user.delete() return redirect("/admin/backend/users").flash(.warning, "User has been deleted. Undo") From be46240a6b84f796b48ca3b69fe1bcce84aeaa22 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Mon, 23 Apr 2018 13:08:49 +0200 Subject: [PATCH 08/11] improve AdminPanelUserForm --- .../AdminPanelUserController.swift | 2 +- .../Models/AdminPanelUser.swift | 4 +-- .../Models/AdminPanelUserForm.swift | 34 +++++++++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift index fa8bf262..53984619 100644 --- a/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift +++ b/Sources/AdminPanelProvider/Controllers/AdminPanelUserController.swift @@ -88,7 +88,7 @@ public final class CustomAdminPanelUserController { try user.save() if - form.shouldSendEmail, + form.shouldSendEmail ?? false, panelConfig.isEmailEnabled, let name = panelConfig.fromName, let email = panelConfig.fromEmail diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift index 65a78e34..63d0ec2f 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift @@ -9,7 +9,7 @@ import Vapor public protocol AdminPanelUserFormType: Form { var password: String? { get } var role: String? { get } - var shouldSendEmail: Bool { get } + var shouldSendEmail: Bool? { get } } public protocol AdminPanelUserType: @@ -130,7 +130,7 @@ extension AdminPanelUser: AdminPanelUserType { if let password = form.password, !password.isEmpty { newPassword = password - shouldResetPassword = form.shouldResetPassword + shouldResetPassword = form.shouldResetPassword ?? false } else { newPassword = "" // this will be overwritten by the controller! shouldResetPassword = true diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift index 1f87ddb4..648b80d8 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift @@ -7,6 +7,7 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { public let nameField: FormField public let emailField: FormField public let passwordField: FormField + public let passwordRepeatField: FormField public let titleField: FormField public let roleField: FormField public let shouldResetPasswordField: FormField @@ -17,6 +18,7 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { name: String? = nil, email: String? = nil, password: String? = nil, + passwordRepeat: String? = nil, title: String? = nil, role: String? = nil, avatar: String? = nil, @@ -54,6 +56,21 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { label: "Password", value: password, validator: passwordValidator.allowingNil(true) + .transformingErrors( + to: ValidatorError.failure( + type: "Password", + reason: "Password must be at least 8 characters." + ) + ) + ) + passwordRepeatField = FormField( + key: "passwordRepeat", + label: "Repeat password", + value: passwordRepeat, + validator: Equals(password ?? "") + .transformingErrors( + to: ValidatorError.failure(type: "Password", reason: "Passwords do not match") + ) ) titleField = FormField( key: "title", @@ -69,12 +86,12 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { ) shouldResetPasswordField = FormField( key: "shouldResetPassword", - label: "Should Reset Password", + label: "Should reset password", value: shouldResetPassword ) shouldSendEmailField = FormField( key: "shouldSendEmail", - label: "Send Email with Info", + label: "Send email with info", value: shouldSendEmail ) } @@ -86,6 +103,7 @@ extension AdminPanelUserForm { nameField, emailField, passwordField, + passwordRepeatField, titleField, roleField, shouldResetPasswordField, @@ -104,17 +122,20 @@ extension AdminPanelUserForm { public var password: String? { return passwordField.value } + public var passwordRepeat: String? { + return passwordRepeatField.value + } public var title: String? { return titleField.value } public var role: String? { return roleField.value } - public var shouldResetPassword: Bool { - return shouldResetPasswordField.value ?? false + public var shouldResetPassword: Bool? { + return shouldResetPasswordField.value } - public var shouldSendEmail: Bool { - return shouldSendEmailField.value ?? false + public var shouldSendEmail: Bool? { + return shouldSendEmailField.value } } @@ -150,6 +171,7 @@ extension AdminPanelUserForm: RequestInitializable { name: content.get("name"), email: content.get("email"), password: content.get("password"), + passwordRepeat: content.get("passwordRepeat"), title: content.get("title"), role: content.get("role"), shouldResetPassword: content.get("shouldResetPassword"), From 2523c1cfc650062a9a30a21b285e924d1275666d Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Mon, 23 Apr 2018 13:22:02 +0200 Subject: [PATCH 09/11] Attempt to compile in Swift 4.0 --- Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift index 648b80d8..6ae3ec54 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUserForm.swift @@ -67,7 +67,7 @@ public struct AdminPanelUserForm: AdminPanelUserFormType { key: "passwordRepeat", label: "Repeat password", value: passwordRepeat, - validator: Equals(password ?? "") + validator: Equals(password ?? "") .transformingErrors( to: ValidatorError.failure(type: "Password", reason: "Passwords do not match") ) From b0850da5de1c146571962fcf75eb2c3271ffd113 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Mon, 23 Apr 2018 13:55:42 +0200 Subject: [PATCH 10/11] Require Swift 4.1 --- .circleci/config.yml | 4 ++-- README.md | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2fe743d9..c1748924 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: MacOS: macos: - xcode: "9.0" + xcode: "9.3.0" steps: - checkout - restore_cache: @@ -30,7 +30,7 @@ jobs: - .build Linux: docker: - - image: brettrtoomey/vapor-ci:0.0.1 + - image: nodesvapor/vapor-ci:swift-4.1 steps: - checkout - restore_cache: diff --git a/README.md b/README.md index 59dde6a2..f0966488 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Admin Panel ✍️ -[![Swift Version](https://img.shields.io/badge/Swift-3-brightgreen.svg)](http://swift.org) +[![Swift Version](https://img.shields.io/badge/Swift-4.1-brightgreen.svg)](http://swift.org) [![Vapor Version](https://img.shields.io/badge/Vapor-2-F6CBCA.svg)](http://vapor.codes) [![Circle CI](https://circleci.com/gh/nodes-vapor/admin-panel-provider/tree/master.svg?style=shield)](https://circleci.com/gh/nodes-vapor/admin-panel-provider) [![codebeat badge](https://codebeat.co/badges/2aa06de9-5bb5-4c2e-ad1a-ef6e08273184)](https://codebeat.co/projects/github-com-nodes-vapor-admin-panel-provider-master) @@ -20,13 +20,6 @@ Admin Panel makes it easy to setup and maintain admin features for your Vapor pr Update your `Package.swift` file: -#### Swift 3 - -```swift -.Package(url: "https://github.com/nodes-vapor/admin-panel-provider.git", majorVersion: 0, minor: 6) -``` -#### Swift 4 - ```swift .package(url: "https://github.com/nodes-vapor/admin-panel-provider.git", .upToNextMinor(from: "0.6.0")), ``` From dc97190baf90e53fdc6a62c37b098d789b060ae8 Mon Sep 17 00:00:00 2001 From: Siemen Sikkema Date: Mon, 23 Apr 2018 15:42:29 +0200 Subject: [PATCH 11/11] Move makeSSOUser to SSO package --- .../AdminPanelProvider/Models/AdminPanelUser.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift index 63d0ec2f..d4b81505 100644 --- a/Sources/AdminPanelProvider/Models/AdminPanelUser.swift +++ b/Sources/AdminPanelProvider/Models/AdminPanelUser.swift @@ -23,7 +23,6 @@ public protocol AdminPanelUserType: ViewDataRepresentable { static func makeSeededUser() throws -> Self - static func makeSSOUser(withEmail: String) throws -> Self associatedtype Form: AdminPanelUserFormType, RequestInitializable @@ -93,18 +92,6 @@ extension AdminPanelUser: AdminPanelUserType { ) } - public static func makeSSOUser(withEmail email: String) throws -> AdminPanelUser { - return try .init( - name: "Admin", - title: "Nodes Admin", - email: email, - password: String.random(16), - role: "Super Admin", - shouldResetPassword: false, - avatar: nil - ) - } - public convenience init( form: AdminPanelUserForm, panelConfig: PanelConfig?,