Skip to content

Portal Prototype #3732

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 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
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

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

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()
}
}
}
}
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
}
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

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()
}
}
}
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

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()
}
}
}
}
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

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()
}

}
}
}
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()
} 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