Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
881858a
Roll back to Soto 6.x until the Cognito libraries support 7.x.
daveverwer Sep 18, 2024
a11de78
first commit
rahafjrw Sep 18, 2024
a5b301b
remove redundant dependancy
rahafjrw Sep 20, 2024
8d4508f
flesh out auth components + minor css
rahafjrw Nov 13, 2024
2bae5b7
display error message in forgot password
rahafjrw Nov 15, 2024
b56cb1a
Added more error information when we get a non-AWS cognito error.
daveverwer Nov 20, 2024
9a8532a
Added some additional error logging.
daveverwer Nov 20, 2024
feeea95
Merge branch 'main' into portal
rahafjrw Nov 26, 2024
9e75369
Current -> environment
rahafjrw Nov 27, 2024
dfc3ebd
seperate Cognito auth logic
rahafjrw Nov 27, 2024
803cd82
basic login tests
rahafjrw Nov 30, 2024
608a520
separate cognito sign up function
rahafjrw Dec 2, 2024
98ae917
add support for testing sign up
rahafjrw Dec 2, 2024
78ef4d4
error handling
rahafjrw Dec 3, 2024
f06be9a
descriptive env variables + entirely move cognito config
rahafjrw Dec 3, 2024
21631c0
support displaying error message in portal view
rahafjrw Dec 3, 2024
0700d20
seperate more cognito functions
rahafjrw Dec 31, 2024
b6b2bc6
support separated cognito functionality in forgot, login, and reset
rahafjrw Dec 31, 2024
205ee2d
seperate verify and delete cognito functions
rahafjrw Jan 1, 2025
0f524e9
add cognito functions as dependencies for testing
rahafjrw Jan 4, 2025
1f0a0d7
test suite for authentication
rahafjrw Jan 5, 2025
79339fa
simplification of authenticateToken + address refresh
rahafjrw Jan 20, 2025
b293252
Merge branch 'main' into portal-merged
daveverwer Feb 5, 2025
4771e08
Fixed up SiteURL after merge.
daveverwer Feb 5, 2025
d626dac
Fix a warning around the unimplemented refresh token exception.
daveverwer Feb 5, 2025
ce91401
Merge inconsistency.
daveverwer Feb 5, 2025
d19763e
Remove a couple of warrnings and a better comment.
daveverwer Feb 5, 2025
ac5231d
Indentation to match project standards.
daveverwer Feb 5, 2025
82f6b93
rename manage to portal
rahafjrw Feb 9, 2025
0ddb07f
renaming
rahafjrw Feb 11, 2025
cb8ce7d
reduce scope of controllers under portal
rahafjrw Feb 11, 2025
7b47889
implement dbId dependency in portal tests
rahafjrw Feb 11, 2025
22a9e10
minor front-end styling and renaming
rahafjrw Feb 12, 2025
36090b8
organize plot extensions
rahafjrw Feb 13, 2025
d71e01f
error handling
rahafjrw Mar 5, 2025
d8c4834
await shutdown
rahafjrw Mar 5, 2025
c6dd19c
update reset route in tests
rahafjrw Mar 9, 2025
0555f65
Merge branch 'main' of https://github.com/SwiftPackageIndex/SwiftPack…
rahafjrw Mar 11, 2025
c40a9ba
remove excludeFromOpenAPI
rahafjrw Mar 12, 2025
ff6d120
Merge branch 'main' into portal
daveverwer Mar 25, 2025
7c2b08f
Converted tests to Swift Testing.
daveverwer Mar 25, 2025
1f75cdd
Snapshots.
daveverwer Mar 25, 2025
61b99cb
Merge branch 'main' into portal
daveverwer Apr 9, 2025
7d40939
Sorted CSS imports.
daveverwer Apr 10, 2025
31e7f1b
Added an account image to the CSS.
daveverwer Apr 10, 2025
c87d083
Replaced the “Portal” link with an icon.
daveverwer Apr 10, 2025
cc08f45
Setup for styling the login form a little.
daveverwer Apr 10, 2025
42c38c8
Better naming for the form container div.
daveverwer Apr 10, 2025
66387d4
WIP.
daveverwer Apr 14, 2025
a30defa
Merge branch 'main' into portal
daveverwer May 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions FrontEnd/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ $mobile-breakpoint: 740px;
@import 'styles/package_list';
@import 'styles/package';
@import 'styles/panel_button';
@import 'styles/portal';
@import 'styles/readme';
@import 'styles/search_results';
@import 'styles/search';
Expand Down
8 changes: 8 additions & 0 deletions FrontEnd/styles/header_footer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ footer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
Expand Down Expand Up @@ -91,6 +92,13 @@ header {
border-color: var(--header-link-highlight);
}
}

li.portal {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
}

@media screen and (max-width: $mobile-breakpoint) {
Expand Down
4 changes: 4 additions & 0 deletions FrontEnd/styles/images.scss

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions FrontEnd/styles/portal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// -------------------------------------------------------------------------
// Styles for authentication pages (login, signup, etc.)
// -------------------------------------------------------------------------

.portal-form-container {
height: 55vh;
padding: 10%;
}

.portal-form-inputs {
display: flex;
flex-direction: column;
width: 50%;
margin-bottom: 15px;
}
2 changes: 1 addition & 1 deletion FrontEnd/styles/search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ section.search {

nav > ul > li.search > form {
grid-template-columns: auto 30px;
max-width: 160px;
max-width: 140px;

input[type='search'] {
padding: 5px;
Expand Down
1 change: 1 addition & 0 deletions Public/images/portal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Resources/SVGs/account~dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Resources/SVGs/account~light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions Sources/App/Controllers/Portal/DeleteAccountController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation
import Dependencies
import Fluent
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity

Comment on lines +1 to +9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing Apache License preamble

According to the coding guidelines, all Swift files should include the Apache 2.0 License preamble at the top, commented to suit the language. Please add the standard license header.

+// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import Foundation
 import Dependencies
 import Fluent
 import Plot
 import Vapor
 import SotoCognitoAuthentication
 import SotoCognitoIdentityProvider
 import SotoCognitoIdentity
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import Foundation
import Dependencies
import Fluent
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
import Dependencies
import Fluent
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/DeleteAccountController.swift at the top of
the file (lines 1 to 9), add the standard Apache 2.0 License preamble as a Swift
comment block before any import statements. This involves inserting the full
license header text formatted with comment markers appropriate for Swift files
to comply with the project's coding guidelines.

extension Portal {

enum DeleteAccountController {
@Sendable
static func deleteAccount(req: Request) async throws -> Response {
@Dependency(\.cognito) var cognito
do {
try await cognito.deleteUser(req: req)
req.auth.logout(AuthenticatedUser.self)
req.session.unauthenticate(AuthenticatedUser.self)
req.session.destroy()
return req.redirect(to: SiteURL.home.relativeURL())
} catch {
return PortalPage.View(path: SiteURL.portal.relativeURL(), model: PortalPage.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription)")).document().encodeResponse(status: .internalServerError)
}
}
}
}
32 changes: 32 additions & 0 deletions Sources/App/Controllers/Portal/ForgotPasswordController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Fluent
import Dependencies
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity

Comment on lines +1 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing Apache 2.0 License preamble.

The file is missing the required Apache 2.0 License preamble that should be included at the top of all Swift files according to the coding guidelines.

Add the Apache 2.0 License preamble at the top of the file:

+//
+// Copyright © 2023 Swift Package Index. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
 import Fluent
 import Dependencies
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/ForgotPasswordController.swift at the top of
the file (lines 1 to 8), add the Apache 2.0 License preamble comment before any
import statements. This preamble should include the standard Apache 2.0 license
text as required by the project's coding guidelines to ensure proper licensing
information is present.

extension Portal {

enum ForgotPasswordController {
@Sendable
static func show(req: Request) async throws -> HTML {
return ForgotPassword.View(path: req.url.path, model: ForgotPassword.Model()).document()
}

@Sendable
static func forgotPasswordEmail(req: Request) async throws -> HTML {
@Dependency(\.cognito) var cognito
struct Credentials: Content {
var email: String
}
do {
let user = try req.content.decode(Credentials.self)
try await cognito.forgotPassword(req: req, username: user.email)
return Reset.View(path: SiteURL.resetPassword.relativeURL(), model: Reset.Model(email: user.email)).document()
} catch {
return ForgotPassword.View(path: req.url.path, model: ForgotPassword.Model(errorMessage: "An error occurred: \(error.localizedDescription)")).document()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using more user-friendly error messages.

Directly exposing error.localizedDescription to the user might reveal technical details that aren't helpful for end users. Consider using more user-friendly error messages, potentially mapping specific error types to more descriptive messages.

-                return ForgotPassword.View(path: req.url.path, model: ForgotPassword.Model(errorMessage: "An error occurred: \(error.localizedDescription)")).document()
+                let errorMessage = "We couldn't process your password reset request. Please try again later."
+                // Log the actual error for debugging
+                req.logger.error("Password reset error: \(error.localizedDescription)")
+                return ForgotPassword.View(path: req.url.path, model: ForgotPassword.Model(errorMessage: errorMessage)).document()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return ForgotPassword.View(path: req.url.path, model: ForgotPassword.Model(errorMessage: "An error occurred: \(error.localizedDescription)")).document()
let errorMessage = "We couldn't process your password reset request. Please try again later."
// Log the actual error for debugging
req.logger.error("Password reset error: \(error.localizedDescription)")
return ForgotPassword.View(
path: req.url.path,
model: ForgotPassword.Model(errorMessage: errorMessage)
).document()
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/ForgotPasswordController.swift at line 28,
replace the direct use of error.localizedDescription in the user-facing error
message with a more user-friendly message. Implement a mapping from specific
error types to descriptive, non-technical messages and use that mapped message
in the ForgotPassword.Model instead of exposing raw error details.

}
}
}
}
56 changes: 56 additions & 0 deletions Sources/App/Controllers/Portal/LoginController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation
import Dependencies
import Fluent
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity

enum Portal {

enum LoginController {
@Sendable
static func show(req: Request) async throws -> HTML {
return Login.View(path: req.url.path, model: Login.Model(errorMessage: "")).document()
}

@Sendable
static func login(req: Request) async throws -> Response {
@Dependency(\.cognito) var cognito
struct UserCreds: Content {
var email: String
var password: String
}
do {
let user = try req.content.decode(UserCreds.self)
let response = try await cognito.authenticate(req: req, username: user.email, password: user.password)
switch response {
case .authenticated(let authenticatedResponse):
let user = AuthenticatedUser(accessToken: authenticatedResponse.accessToken!)
req.auth.login(user)
case .challenged(_): // Cognito is not configured to send challenges, so we should never receive this response.
break
}
Comment on lines +28 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid force-unwrap of accessToken & handle unexpected challenge

authenticatedResponse.accessToken! will crash the server if Cognito ever omits the field (e.g. config drift).
Additionally, silently ignoring the .challenged case hides configuration issues.

-case .authenticated(let authenticatedResponse):
-    let user = AuthenticatedUser(accessToken: authenticatedResponse.accessToken!)
-    req.auth.login(user)
-case .challenged(_):
-    break
+case .authenticated(let authenticatedResponse):
+    guard let token = authenticatedResponse.accessToken else {
+        throw Abort(.internalServerError, reason: "Missing access token in Cognito response")
+    }
+    req.auth.login(AuthenticatedUser(accessToken: token))
+case .challenged(let challenge):
+    throw Abort(.unauthorized, reason: "Unexpected Cognito challenge: \(challenge)")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch response {
case .authenticated(let authenticatedResponse):
let user = AuthenticatedUser(accessToken: authenticatedResponse.accessToken!)
req.auth.login(user)
case .challenged(_): // Cognito is not configured to send challenges, so we should never receive this response.
break
}
switch response {
case .authenticated(let authenticatedResponse):
guard let token = authenticatedResponse.accessToken else {
throw Abort(.internalServerError, reason: "Missing access token in Cognito response")
}
req.auth.login(AuthenticatedUser(accessToken: token))
case .challenged(let challenge):
throw Abort(.unauthorized, reason: "Unexpected Cognito challenge: \(challenge)")
}
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/LoginController.swift around lines 28 to 34,
avoid force-unwrapping authenticatedResponse.accessToken to prevent server
crashes if the token is missing. Instead, safely unwrap the accessToken using
optional binding and handle the nil case appropriately, such as returning an
error or logging it. Also, do not silently ignore the .challenged case; add
explicit handling to log a warning or return an error to surface potential
configuration issues with Cognito.

return req.redirect(to: SiteURL.portal.relativeURL(), redirectType: .normal)
} catch let error as SotoCognitoError {
var model = Login.Model(errorMessage: "There was an error. Please try again.")
switch error {
case .unauthorized(let reason):
model = Login.Model(errorMessage: reason ?? "There was an error. Please try again.")
case .unexpectedResult(let reason):
model = Login.Model(errorMessage: reason ?? "There was an error. Please try again.")
case .invalidPublicKey:
break
}
return Login.View(path: req.url.path, model: model).document().encodeResponse(status: .unauthorized)
} catch let error as AWSClientError {
return Login.View(path: SiteURL.login.relativeURL(), model: Login.Model(errorMessage: "An AWS client error occurred: \(error.errorCode)")).document().encodeResponse(status: .unauthorized)
} catch {
return Login.View(path: SiteURL.login.relativeURL(), model: Login.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription)")).document().encodeResponse(status: .unauthorized)
}

}
}
}

21 changes: 21 additions & 0 deletions Sources/App/Controllers/Portal/LogoutController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation
import Fluent
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity

Comment on lines +1 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing Apache 2.0 License preamble.

According to the coding guidelines, every Swift file should have the Apache 2.0 License preamble at the top, commented to suit the language.

Add the license preamble at the top of the file:

+// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import Foundation
 import Fluent
 import Plot
 import Vapor
 import SotoCognitoAuthentication
 import SotoCognitoIdentityProvider
 import SotoCognitoIdentity
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import Foundation
import Fluent
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
import Fluent
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/LogoutController.swift at the top of the file
(lines 1 to 8), add the Apache 2.0 License preamble as a comment before any
import statements. This involves inserting the standard Apache 2.0 license
header formatted for Swift files, ensuring it is properly commented out and
placed as the very first content in the file.

extension Portal {

enum LogoutController {
@Sendable
static func logout(req: Request) async throws -> Response {
req.auth.logout(AuthenticatedUser.self)
req.session.unauthenticate(AuthenticatedUser.self)
req.session.destroy()
return req.redirect(to: SiteURL.home.relativeURL())
}
}
}

14 changes: 14 additions & 0 deletions Sources/App/Controllers/Portal/PortalController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Fluent
import Plot
import Vapor
import SotoCognitoAuthenticationKit

extension Portal {

enum PortalController {
@Sendable
static func show(req: Request) async throws -> HTML {
return PortalPage.View(path: req.url.path, model: PortalPage.Model()).document()
}
}
}
Comment on lines +1 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing Apache License preamble

According to the coding guidelines, all Swift files should include the Apache 2.0 License preamble at the top, commented to suit the language. Please add the standard license header.

+// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import Fluent
 import Plot
 import Vapor
 import SotoCognitoAuthenticationKit
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import Fluent
import Plot
import Vapor
import SotoCognitoAuthenticationKit
extension Portal {
enum PortalController {
@Sendable
static func show(req: Request) async throws -> HTML {
return PortalPage.View(path: req.url.path, model: PortalPage.Model()).document()
}
}
}
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Fluent
import Plot
import Vapor
import SotoCognitoAuthenticationKit
extension Portal {
enum PortalController {
@Sendable
static func show(req: Request) async throws -> HTML {
return PortalPage.View(path: req.url.path, model: PortalPage.Model()).document()
}
}
}
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/PortalController.swift at the top of the file
before any imports, add the standard Apache 2.0 License preamble as a Swift
comment block. This should include the copyright notice and license terms
formatted appropriately for Swift source files.

40 changes: 40 additions & 0 deletions Sources/App/Controllers/Portal/ResetController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Fluent
import Dependencies
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity

Comment on lines +1 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing Apache 2.0 License preamble.

According to the coding guidelines, every Swift file should have the Apache 2.0 License preamble at the top, commented to suit the language.

Add the license preamble at the top of the file:

+// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import Fluent
 import Dependencies
 import Plot
 import Vapor
 import SotoCognitoAuthentication
 import SotoCognitoIdentityProvider
 import SotoCognitoIdentity
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import Fluent
import Dependencies
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Fluent
import Dependencies
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/ResetController.swift at the top of the file,
add the Apache 2.0 License preamble as a comment according to Swift syntax. This
should be placed before any import statements to comply with coding guidelines.

extension Portal {

enum ResetController {
@Sendable
static func show(req: Request) async throws -> HTML {
return Reset.View(path: req.url.path, model: Reset.Model()).document()
}

@Sendable
static func resetPassword(req: Request) async throws -> HTML {
@Dependency(\.cognito) var cognito
struct UserInfo: Content {
var email: String
var password: String
var confirmationCode: String
}
do {
let user = try req.content.decode(UserInfo.self)
try await cognito.resetPassword(req: req, username: user.email, password: user.password, confirmationCode: user.confirmationCode)
let model = SuccessfulChange.Model(successMessage: "Successfully changed password")
return SuccessfulChange.View(path: req.url.path, model: model).document()
} catch let error as AWSErrorType {
let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
let model = Reset.Model(errorMessage: errorMessage)
return Reset.View(path: req.url.path, model: model).document()
} catch {
let model = Reset.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription)")
return Reset.View(path: req.url.path, model: model).document()
}
Comment on lines +19 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation and escape HTML in error messages.

The method handles the password reset flow well, but there are two potential improvements:

  1. No validation is performed on the input before sending to Cognito. Consider validating email format, password strength, and confirmation code presence.
  2. Error messages are concatenated directly with error descriptions, which could lead to HTML injection if the error contains special characters.

Consider applying these improvements:

        @Sendable
        static func resetPassword(req: Request) async throws -> HTML {
            @Dependency(\.cognito) var cognito
            struct UserInfo: Content {
                var email: String
                var password: String
                var confirmationCode: String
            }
            do {
                let user = try req.content.decode(UserInfo.self)
+               // Validate input
+               guard !user.email.isEmpty, !user.password.isEmpty, !user.confirmationCode.isEmpty else {
+                   throw Abort(.badRequest, reason: "All fields are required")
+               }
+               
                try await cognito.resetPassword(req: req, username: user.email, password: user.password, confirmationCode: user.confirmationCode)
                let model = SuccessfulChange.Model(successMessage: "Successfully changed password")
                return SuccessfulChange.View(path: req.url.path, model: model).document()
            } catch let error as AWSErrorType {
-               let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
+               let errorMessage = (error.message != nil) ? "There was an error: \(error.message?.description.htmlEscaped ?? "")" : "There was an error: \(error.localizedDescription.htmlEscaped)"
                let model = Reset.Model(errorMessage: errorMessage)
                return Reset.View(path: req.url.path, model: model).document()
            } catch {
-               let model = Reset.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription)")
+               let model = Reset.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription.htmlEscaped)")
                return Reset.View(path: req.url.path, model: model).document()
            }
        }

Note: You'll need to ensure that an htmlEscaped extension is available for String, or use an appropriate HTML escaping method.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Dependency(\.cognito) var cognito
struct UserInfo: Content {
var email: String
var password: String
var confirmationCode: String
}
do {
let user = try req.content.decode(UserInfo.self)
try await cognito.resetPassword(req: req, username: user.email, password: user.password, confirmationCode: user.confirmationCode)
let model = SuccessfulChange.Model(successMessage: "Successfully changed password")
return SuccessfulChange.View(path: req.url.path, model: model).document()
} catch let error as AWSErrorType {
let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
let model = Reset.Model(errorMessage: errorMessage)
return Reset.View(path: req.url.path, model: model).document()
} catch {
let model = Reset.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription)")
return Reset.View(path: req.url.path, model: model).document()
}
@Sendable
static func resetPassword(req: Request) async throws -> HTML {
@Dependency(\.cognito) var cognito
struct UserInfo: Content {
var email: String
var password: String
var confirmationCode: String
}
do {
let user = try req.content.decode(UserInfo.self)
// Validate input
guard !user.email.isEmpty, !user.password.isEmpty, !user.confirmationCode.isEmpty else {
throw Abort(.badRequest, reason: "All fields are required")
}
try await cognito.resetPassword(req: req, username: user.email, password: user.password, confirmationCode: user.confirmationCode)
let model = SuccessfulChange.Model(successMessage: "Successfully changed password")
return SuccessfulChange.View(path: req.url.path, model: model).document()
} catch let error as AWSErrorType {
let errorMessage = (error.message != nil)
? "There was an error: \(error.message?.description.htmlEscaped ?? "")"
: "There was an error: \(error.localizedDescription.htmlEscaped)"
let model = Reset.Model(errorMessage: errorMessage)
return Reset.View(path: req.url.path, model: model).document()
} catch {
let model = Reset.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription.htmlEscaped)")
return Reset.View(path: req.url.path, model: model).document()
}
}
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/ResetController.swift around lines 19 to 37,
add validation for the UserInfo input fields before calling
cognito.resetPassword, checking that the email is in a valid format, the
password meets strength requirements, and the confirmation code is present.
Additionally, sanitize all error messages by escaping HTML special characters
before including them in the response to prevent HTML injection vulnerabilities.
Implement or use an existing htmlEscaped method on String to perform this
escaping.

}
}
}
38 changes: 38 additions & 0 deletions Sources/App/Controllers/Portal/SignupController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Fluent
import Dependencies
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity

Comment on lines +1 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing Apache 2.0 License preamble.

According to the coding guidelines, every Swift file should have the Apache 2.0 License preamble at the top, commented to suit the language.

Add the license preamble at the top of the file:

+// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import Fluent
 import Dependencies
 import Plot
 import Vapor
 import SotoCognitoAuthentication
 import SotoCognitoIdentityProvider
 import SotoCognitoIdentity
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/SignupController.swift at the top of the file
(lines 1 to 8), add the Apache 2.0 License preamble as a Swift comment block
before any import statements. This involves inserting the standard license text
formatted with Swift comment syntax (/* ... */ or // for each line) to comply
with the project's coding guidelines.

extension Portal {

enum SignupController {
@Sendable
static func show(req: Request) async throws -> HTML {
return Signup.View(path: req.url.path, model: Signup.Model(errorMessage: "")).document()
}

@Sendable
static func signup(req: Request) async throws -> HTML {
@Dependency(\.cognito) var cognito
struct UserCreds: Content {
var email: String
var password: String
}
do {
let user = try req.content.decode(UserCreds.self)
try await cognito.signup(req: req, username: user.email, password: user.password)
return Verify.View(path: SiteURL.verify.relativeURL(), model: Verify.Model(email: user.email)).document()
} catch let error as AWSErrorType {
let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
let model = Signup.Model(errorMessage: errorMessage)
return Signup.View(path: req.url.path, model: model).document()
} catch {
return Signup.View(path: SiteURL.signup.relativeURL(), model: Signup.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription)")).document()
}
Comment on lines +18 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation, escape HTML in error messages, and use consistent paths.

The method handles the signup flow well, but there are three potential improvements:

  1. No validation is performed on the input before sending to Cognito. Consider validating email format and password strength.
  2. Error messages are concatenated directly with error descriptions, which could lead to HTML injection.
  3. Path handling is inconsistent between AWS errors and generic errors.

Consider applying these improvements:

        @Sendable
        static func signup(req: Request) async throws -> HTML {
            @Dependency(\.cognito) var cognito
            struct UserCreds: Content {
                var email: String
                var password: String
            }
            do {
                let user = try req.content.decode(UserCreds.self)
+               // Validate input
+               guard !user.email.isEmpty, !user.password.isEmpty else {
+                   throw Abort(.badRequest, reason: "Email and password are required")
+               }
+               
                try await cognito.signup(req: req, username: user.email, password: user.password)
                return Verify.View(path: SiteURL.verify.relativeURL(), model: Verify.Model(email: user.email)).document()
            } catch let error as AWSErrorType {
-               let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
+               let errorMessage = (error.message != nil) ? "There was an error: \(error.message?.description.htmlEscaped ?? "")" : "There was an error: \(error.localizedDescription.htmlEscaped)"
                let model = Signup.Model(errorMessage: errorMessage)
                return Signup.View(path: req.url.path, model: model).document()
            } catch {
-               return Signup.View(path: SiteURL.signup.relativeURL(), model: Signup.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription)")).document()
+               return Signup.View(path: req.url.path, model: Signup.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription.htmlEscaped)")).document()
            }
            
        }

Note: You'll need to ensure that an htmlEscaped extension is available for String, or use an appropriate HTML escaping method.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static func signup(req: Request) async throws -> HTML {
@Dependency(\.cognito) var cognito
struct UserCreds: Content {
var email: String
var password: String
}
do {
let user = try req.content.decode(UserCreds.self)
try await cognito.signup(req: req, username: user.email, password: user.password)
return Verify.View(path: SiteURL.verify.relativeURL(), model: Verify.Model(email: user.email)).document()
} catch let error as AWSErrorType {
let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
let model = Signup.Model(errorMessage: errorMessage)
return Signup.View(path: req.url.path, model: model).document()
} catch {
return Signup.View(path: SiteURL.signup.relativeURL(), model: Signup.Model(errorMessage: "An unknown error occurred: \(error.localizedDescription)")).document()
}
@Sendable
static func signup(req: Request) async throws -> HTML {
@Dependency(\.cognito) var cognito
struct UserCreds: Content {
var email: String
var password: String
}
do {
let user = try req.content.decode(UserCreds.self)
// Validate input
guard !user.email.isEmpty, !user.password.isEmpty else {
throw Abort(.badRequest, reason: "Email and password are required")
}
try await cognito.signup(req: req, username: user.email, password: user.password)
return Verify.View(
path: SiteURL.verify.relativeURL(),
model: Verify.Model(email: user.email)
).document()
} catch let error as AWSErrorType {
let errorMessage = (error.message != nil)
? "There was an error: \(error.message?.description.htmlEscaped ?? "")"
: "There was an error: \(error.localizedDescription.htmlEscaped)"
let model = Signup.Model(errorMessage: errorMessage)
return Signup.View(path: req.url.path, model: model).document()
} catch {
return Signup.View(
path: req.url.path,
model: Signup.Model(
errorMessage: "An unknown error occurred: \(error.localizedDescription.htmlEscaped)"
)
).document()
}
}
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/SignupController.swift around lines 18 to 34,
add validation for the email format and password strength before calling Cognito
signup to prevent invalid inputs. Escape all error message strings using an
htmlEscaped method or equivalent to avoid HTML injection vulnerabilities. Also,
unify the path handling by consistently using SiteURL.signup.relativeURL() for
error views in both AWS error and generic error catch blocks to maintain
consistent user experience.


}
}
}
42 changes: 42 additions & 0 deletions Sources/App/Controllers/Portal/VerifyController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

import Fluent
import Plot
import Vapor
import SotoCognitoAuthentication
import SotoCognitoIdentityProvider
import SotoCognitoIdentity
import Dependencies

extension Portal {

enum VerifyController {
@Sendable
static func show(req: Request) async throws -> HTML {
return Verify.View(path: req.url.path, model: Verify.Model(email: "")).document()
}

@Sendable
static func verify(req: Request) async throws -> HTML {
@Dependency(\.cognito) var cognito
struct VerifyInformation: Content {
var email: String
var confirmationCode: String
}
do {
let info = try req.content.decode(VerifyInformation.self)
try await cognito.confirmSignUp(req: req, username: info.email, confirmationCode: info.confirmationCode)
let model = SuccessfulChange.Model(successMessage: "Successfully confirmed signup")
return SuccessfulChange.View(path: req.url.path, model: model).document()
} catch let error as AWSErrorType {
let info = try req.content.decode(VerifyInformation.self)
let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
let model = Verify.Model(email: info.email, errorMessage: errorMessage)
return Verify.View(path: req.url.path, model: model).document()
Comment on lines +30 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Interpolating optional produces Optional(...) in the UI

"\(error.message)" will render as Optional("…"). Build the message safely:

-let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
+let details = error.message ?? error.localizedDescription
+let errorMessage = "There was an error: \(details)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch let error as AWSErrorType {
let info = try req.content.decode(VerifyInformation.self)
let errorMessage = (error.message != nil) ? "There was an error: \(error.message)" : "There was an error: \(error.localizedDescription)"
let model = Verify.Model(email: info.email, errorMessage: errorMessage)
return Verify.View(path: req.url.path, model: model).document()
} catch let error as AWSErrorType {
let info = try req.content.decode(VerifyInformation.self)
let details = error.message ?? error.localizedDescription
let errorMessage = "There was an error: \(details)"
let model = Verify.Model(email: info.email, errorMessage: errorMessage)
return Verify.View(path: req.url.path, model: model).document()
🤖 Prompt for AI Agents
In Sources/App/Controllers/Portal/VerifyController.swift around lines 30 to 34,
the error message interpolation uses an optional value directly, causing the UI
to display "Optional(...)" text. To fix this, unwrap the optional safely before
interpolation, for example by using optional binding or the nil-coalescing
operator, so the message string contains only the actual error text without the
Optional wrapper.

} catch {
let info = try req.content.decode(VerifyInformation.self)
let model = Verify.Model(email: info.email, errorMessage: "An unknown error occurred: \(error.localizedDescription)")
return Verify.View(path: req.url.path, model: model).document()
}
}
}
}
Loading
Loading