diff --git a/.github/workflows/master-job.yml b/.github/workflows/master-job.yml new file mode 100644 index 0000000..971f9a3 --- /dev/null +++ b/.github/workflows/master-job.yml @@ -0,0 +1,72 @@ +name: MasterAction + +on: + push: + branches: + - master + +jobs: + check-doc-coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1.2.0 + - name: Calculate Documentation Coverage + uses: MatsMoll/swift-doc@master + with: + inputs: "Sources" + output: "dcov.json" + - name: Check Documentation Percent + run: sudo bash CI/check-percentage.sh dcov.json 1 + + bionic-tests: + needs: check-doc-coverage + services: + postgres: + image: postgres + ports: + - 5432:5432 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1.2.0 + - name: Run unit tests master + run: swift test --enable-test-discovery + env: + DATABASE_USER: postgres + DATABASE_PASSWORD: postgres + DATABASE_DB: postgres + DATABASE_HOSTNAME: localhost + + release-docs: + needs: bionic-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1.2.0 + - name: Generate Documentation + uses: SwiftDocOrg/swift-doc@master + with: + inputs: "Sources" + module-name: KognitaWeb + output: "Documentation" + - name: Upload Documentation to Wiki + uses: SwiftDocOrg/github-wiki-publish-action@v1 + with: + path: "Documentation" + env: + GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} + + release-version: + needs: release-docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1.2.0 + - name: Release new version + uses: MatsMoll/action-finch@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} + GITHUB_SHA: ${{ secrets.GITHUB_SHA }} + FINCH_CONFIG: CI/finch-config.yml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 59e2b03..959fa0f 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -1,21 +1,30 @@ -name: Swift +name: DevelopAction on: pull_request: branches: - - master - develop push: branches: - - master - develop - - rc/* - - 1.* jobs: - bionic-job: + check-doc-coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1.2.0 + - name: Calculate Documentation Coverage + uses: MatsMoll/swift-doc@master + with: + inputs: "Sources" + output: "dcov.json" + - name: Check Documentation Percent + run: sudo bash CI/check-percentage.sh dcov.json 1 + + bionic-tests: + needs: check-doc-coverage services: - psql: + postgres: image: postgres ports: - 5432:5432 @@ -26,33 +35,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1.2.0 - - name: GitHub Action for SwiftLint - uses: norio-nomura/action-swiftlint@3.1.0 - name: Run unit tests - if: github.ref != 'refs/heads/master' + run: swift test --enable-test-discovery env: BUILD_TYPE: DEV DATABASE_USER: postgres DATABASE_PASSWORD: postgres DATABASE_DB: postgres - DATABASE_HOSTNAME: localhost - run: swift test - - name: Run unit tests master - if: github.ref == 'refs/heads/master' - env: - DATABASE_USER: postgres - DATABASE_PASSWORD: postgres - DATABASE_DB: postgres - DATABASE_HOSTNAME: localhost - run: swift test - - name: Build library in release mode - if: github.ref == 'refs/heads/master' - run: swift build -c release - - name: Release new version - uses: MatsMoll/action-finch@master - if: github.ref == 'refs/heads/master' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} - GITHUB_SHA: ${{ secrets.GITHUB_SHA }} - FINCH_CONFIG: CI/finch-config.yml + DATABASE_HOSTNAME: localhost \ No newline at end of file diff --git a/CI/check-percentage.sh b/CI/check-percentage.sh new file mode 100644 index 0000000..e359f40 --- /dev/null +++ b/CI/check-percentage.sh @@ -0,0 +1,11 @@ +#!/bin/sh +PERCENT="$(cat "$1" | jq ".data.totals.percent")" +THRESHOLD="$2" +if [ "${PERCENT%.*}" -ge "$THRESHOLD" ] +then + echo "Looking good with $PERCENT% coverage" + exit 0 +else + echo "Only $PERCENT% coverage. Needs $THRESHOLD% or more" + exit 1 +fi diff --git a/Package.swift b/Package.swift index 5c52d75..ead88dc 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,8 @@ var dependencies: [Package.Dependency] = [ .package(name: "vapor", url: "https://github.com/vapor/vapor.git", from: "4.29.0"), .package(name: "HTMLKitVaporProvider", url: "https://github.com/MatsMoll/htmlkit-vapor-provider.git", from: "1.0.1"), + + .package(url: "https://github.com/vapor-community/HTMLKit.git", from: "2.1.0"), ] // Kognita Core @@ -27,6 +29,19 @@ case "DEV": ] ) default: + #if os(macOS) + if ProcessInfo.processInfo.environment["CUSTOM_SETTINGS"] == nil { + dependencies.append(contentsOf: [ + .package(path: "../KognitaAPI"), + .package(path: "../KognitaCore"), + .package(path: "../KognitaViews"), + .package(path: "../KognitaModels") + ] + ) + break + } + + #endif let coreVersion = ProcessInfo.processInfo.environment["KOGNITA_CORE"] ?? "2.0.0" let pagesVersion = ProcessInfo.processInfo.environment["KOGNITA_PAGES"] ?? "2.0.0" let apiVersion = ProcessInfo.processInfo.environment["KOGNITA_API"] ?? "2.0.0" diff --git a/Public/robots.txt b/Public/robots.txt new file mode 100644 index 0000000..4f9540b --- /dev/null +++ b/Public/robots.txt @@ -0,0 +1 @@ +User-agent: * \ No newline at end of file diff --git a/Sources/App/HTMLKitErrorMiddleware.swift b/Sources/App/HTMLKitErrorMiddleware.swift index 4746cd7..96df2ca 100644 --- a/Sources/App/HTMLKitErrorMiddleware.swift +++ b/Sources/App/HTMLKitErrorMiddleware.swift @@ -7,11 +7,177 @@ import Vapor import HTMLKit - - +//import Combine +// +///// Can represent a path e.g a `String` or a `Identifiable` object +//protocol EndpointPathItem { +// var pathDescription: String { get } +// var pathIdentifier: String { get } +//} +// +//extension String: EndpointPathItem { +// var pathDescription: String { self } +// var pathIdentifier: String { self } +//} +// +//struct SimpleGet: Endpoint { +// let method: Method = .get +// var pathComponents: [String] +//} +// +//struct SimplePost: Endpoint { +// let method: Method = .post +// var pathComponents: [String] +// +// func with(content: C.Type) -> PostWithContent { .init(pathComponents: pathComponents) } +//} +// +//struct PostWithContent: Endpoint { +// let method: Method = .get +// var pathComponents: [String] +//} +// +//enum Method { +// case post +// case get +// case put +// case delete +//} +// +//protocol Endpoint { +// var method: Method { get } +// var pathComponents: [String] { get } +//} +// +//struct ParameterPost: Endpoint { +// let method: Method = .post +// var pathComponents: [String] +// +// func request(body: C.Type) -> ParameterContentPost { .init(pathComponents: pathComponents) } +//} +// +//struct ParameterContentPost: Endpoint { +// let method: Method = .post +// var pathComponents: [String] +// +// func respone(body: R.Type) -> ParameterContentPostWithResponse { +// .init(pathComponents: pathComponents) +// } +//} +// +//struct ParameterContentPostWithResponse: Endpoint { +// let method: Method = .post +// var pathComponents: [String] +//} +// +//enum EndpointBuilder { +// static func post(to paths: String, _ parameter: P.Type) -> ParameterPost

{ +// ParameterPost(pathComponents: [paths, ":\(String(reflecting: P.self))"]) +// } +//} +// +//// Some extensions only available in the Vapor code +//extension Endpoint { +// var vaporPathComponents: [PathComponent] { +// pathComponents.map { path in +// if path.hasPrefix(":") { +// return .parameter(String(path.dropFirst())) +// } else { +// return .constant(path) +// } +// } +// } +//} +// +//extension ParameterContentPostWithResponse { +// func resolve(req: Request, implementation: (T.ID, Content, Request) -> EventLoopFuture) -> EventLoopFuture { +// let id = req.parameters.get(String(reflecting: T.self)) as! T.ID // Needs some String literal init for the ID +// let content = try! req.content.decode(Content.self) +// return implementation(id, content, req) +// } +//} +// +//extension ParameterContentPostWithResponse where Response: Vapor.Response { +// +// func register(in route: RoutesBuilder, _ implementation: (T.ID, Content, Request) -> EventLoopFuture) { +// route.on(.POST, vaporPathComponents) { (req) -> EventLoopFuture in +// resolve(req: req, implementation: implementation) +// } +// } +//} +// +//// Some extensions only available in the iOS code +//extension ParameterContentPostWithResponse { +// func request(with id: T.ID, content: Content, baseURL: URL) -> AnyPublisher { +// let url = baseURL.appendingPathComponent(pathComponents.first! + "/\(id)") +// var urlRequest = URLRequest(url: url) +// urlRequest.httpMethod = "POST" +// urlRequest.httpBody = try! JSONEncoder().encode(content) +// return URLSession.shared.dataTaskPublisher(for: urlRequest) +// .map(\.data) +// .decode(type: Response.self, decoder: JSONDecoder()) +// .eraseToAnyPublisher() +// } +//} +// +//extension ParameterPost { +// func resolve(req: Request, function: (T.ID, Request) -> EventLoopFuture) -> EventLoopFuture { +// let id = req.parameters.get(String(reflecting: T.self)) +// return function(id as! T.ID, req) +// } +//} +// +//extension ParameterContentPost { +// func resolve(req: Request, implementation: (T.ID, Content, Request) -> EventLoopFuture) -> EventLoopFuture { +// let id = req.parameters.get(String(reflecting: T.self)) as! T.ID // Needs some string literal init for the ID +// let content = try! req.content.decode(Content.self) +// return implementation(id, content, req) +// } +// +// func register(in route: RoutesBuilder, _ implementation: (T.ID, Content, Request) -> EventLoopFuture) { +// route.on(.POST, vaporPathComponents) { (req) -> EventLoopFuture in +// self.resolve(req: req, implementation: implementation) +// .transform(to: .ok) +// } +// } +//} +// +//extension PostWithContent { +// func resolve(req: Request, function: (Content, Request) -> EventLoopFuture) -> EventLoopFuture { +// let content = try! req.content.decode(Content.self) +// return function(content, req) +// } +//} +// +// +//extension ParameterPost { +// func request(with id: T.ID, baseURL: URL) -> URLSession.DataTaskPublisher { +// let url = baseURL.appendingPathComponent(pathComponents.first! + "/\(id)") +// return URLSession.shared.dataTaskPublisher(for: url) +// } +//} +// +//extension ParameterContentPost { +// func request(with id: T.ID, content: Content, baseURL: URL) -> URLSession.DataTaskPublisher { +// let url = baseURL.appendingPathComponent(pathComponents.first! + "/\(id)") +// var urlRequest = URLRequest(url: url) +// urlRequest.httpBody = try! JSONEncoder().encode(content) +// return URLSession.shared.dataTaskPublisher(for: urlRequest) +// } +//} +// +//extension PostWithContent { +// func makeRequest(with content: Content, baseURL: URL) -> URLSession.DataTaskPublisher { +// let url = baseURL.appendingPathComponent(pathComponents.first!) +// var urlRequest = URLRequest(url: url) +// urlRequest.httpMethod = "POST" +// urlRequest.httpBody = try! JSONEncoder().encode(content) +// return URLSession.shared.dataTaskPublisher(for: urlRequest) +// } +//} /// Captures all errors and transforms them into an internal server error. public struct HTMLKitErrorMiddleware: Middleware { - + /// A path to ignore let ignorePath: String diff --git a/Sources/App/Subject/SubjectWebController.swift b/Sources/App/Subject/SubjectWebController.swift index 1703cc5..ce71a6d 100644 --- a/Sources/App/Subject/SubjectWebController.swift +++ b/Sources/App/Subject/SubjectWebController.swift @@ -21,72 +21,92 @@ final class SubjectWebController: RouteCollection { routes.get("subjects", use: listAll) routes.get("subjects", "search", use: search(on:)) - routes.get("subjects", "create", use: createSubject) let subject = routes.grouped("subjects", Subject.parameter) subject.get(use: details) - subject.get("edit", use: editSubject) - subject.get("compendium", use: compendium) + + let authRoutes = routes.grouped(RedirectMiddleware(path: "/login")) + authRoutes.get("edit", use: editSubject) + authRoutes.get("compendium", use: compendium) + authRoutes.get("subjects", "create", use: createSubject) } func search(on req: Request) throws -> EventLoopFuture { let query = try req.query.decode(Subject.ListOverview.SearchQuery.self) - return req.repositories { repositories in - try repositories.subjectRepository - .allSubjects(for: req.auth.require(), searchQuery: query) - .map(Subject.Templates.ListComponent.Context.init(subjects: )) - .flatMap { context in - Subject.Templates.ListComponent().render(with: context, for: req) + return req.eventLoop.future() + .flatMap { + if let user = req.auth.get(User.self) { + return req.repositories { repo in + repo.subjectRepository + .allSubjects(for: user.id, searchQuery: query) + + } + } else { + return req.repositories { repo in + repo.subjectRepository + .allSubjects(for: nil, searchQuery: query) + } } - } + } + .map(Subject.Templates.ListComponent.Context.init(subjects: )) + .flatMap { context in + Subject.Templates.ListComponent().render(with: context, for: req) + } + } - func listAll(_ req: Request) throws -> EventLoopFuture { + func listAll(_ req: Request) throws -> EventLoopFuture { - let user = try req.auth.require(User.self) let query = try? req.query.decode(ListAllQuery.self) - - return try req.controllers.taskDiscussionResponseController - .setRecentlyVisited(on: req) // FIXME: Rename - .failableFlatMap { activeDiscussion in - - try req.controllers.subjectController - .getListContent(req) - .flatMapThrowing { listContent in - - try req.htmlkit - .render( - Subject.Templates.ListOverview.self, - with: .init( - user: user, - list: listContent, - wasIncorrectPassword: query?.incorrectPassword ?? false, - recentlyActiveDiscussions: activeDiscussion - ) - ) - } + + if let user = req.auth.get(User.self) { + return try req.controllers.taskDiscussionResponseController + .setRecentlyVisited(on: req) // FIXME: Rename + .failableFlatMap { activeDiscussion in + + try req.controllers.subjectController + .getListContent(req) + .flatMap { listContent in + + Pages.AuthenticatedDashboard() + .render( + with: .init( + user: user, + list: listContent, + wasIncorrectPassword: query?.incorrectPassword ?? false, + recentlyActiveDiscussions: activeDiscussion + ), + for: req + ) + } + } + } else { + return req.repositories { repo in + repo.subjectRepository.allSubjects(for: nil, searchQuery: .init()) + } + .flatMap { subject in + Pages.UnauthenticatedDashboard() + .render(with: .init(subjects: subject, showCoockieMessage: !req.cookies.isAccepted), for: req) + } } } - func details(_ req: Request) throws -> EventLoopFuture { - - let user = try req.auth.require(User.self) + func details(_ req: Request) throws -> EventLoopFuture { return try req.controllers.subjectController .getDetails(req) - .flatMapThrowing { details in - - try req.htmlkit - .render( - Subject.Templates.Details.self, - with: .init( - user: user, - details: details - ) - ) + .flatMap { details in + + if let user = req.auth.get(User.self) { + return Subject.Templates.Details() + .render(with: .init(user: user, details: details), for: req) + } else { + return Subject.Templates.Details.Unauthenticated() + .render(with: .init(details: details, showCookieMessage: !req.cookies.isAccepted), for: req) + } } } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 58b5ffb..939c9a6 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -56,7 +56,7 @@ private func setupUserWeb(for app: Application) throws { } try sessionMiddle.register(collection: UserWebController()) - try redirectMiddle.register(collection: SubjectWebController()) + try sessionMiddle.register(collection: SubjectWebController()) try redirectMiddle.register(collection: TopicWebController()) try redirectMiddle.register(collection: MultipleChoiseTaskWebController()) try redirectMiddle.register(collection: CreatorWebController())