-
Notifications
You must be signed in to change notification settings - Fork 0
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
SCC-4236: Username for My Account 2.0 #397
Changes from all commits
766b558
7e67c6e
47219eb
cd54635
f116cf8
a6f7684
e52407e
673d296
bc995e2
f1a87f8
ee979fc
28e698d
303cf48
d240835
8ec5985
4a5e036
d78b943
013d510
b17b5a1
6570b9f
0bcf57d
bc4f37e
63aa14e
f1e65a6
2863e93
5e720be
3b317c3
e7bf689
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import sierraClient from "../../../src/server/sierraClient" | ||
import type { HTTPResponse } from "../../../src/types/appTypes" | ||
import nyplApiClient from "../../../src/server/nyplApiClient" | ||
|
||
/** | ||
* PUT request to Sierra to update patron PIN, first validating with previous PIN. | ||
|
@@ -27,6 +28,52 @@ export async function updatePin( | |
} | ||
} | ||
|
||
/** | ||
* PUT request to Sierra to update patron username, first validating that it's available. | ||
* Returns status and message about request. | ||
*/ | ||
export async function updateUsername( | ||
patronId: string, | ||
newUsername: string | ||
): Promise<HTTPResponse> { | ||
try { | ||
// If the new username is an empty string, skips validation and directly updates in Sierra. | ||
const client = await sierraClient() | ||
if (newUsername === "") { | ||
const client = await sierraClient() | ||
await client.put(`patrons/${patronId}`, { | ||
varFields: [{ fieldTag: "u", content: newUsername }], | ||
}) | ||
return { status: 200, message: "Username removed successfully" } | ||
} else { | ||
const platformClient = await nyplApiClient({ version: "v0.3" }) | ||
const response = await platformClient.post("/validations/username", { | ||
username: newUsername, | ||
}) | ||
|
||
if (response?.type === "available-username") { | ||
await client.put(`patrons/${patronId}`, { | ||
varFields: [{ fieldTag: "u", content: newUsername }], | ||
}) | ||
Comment on lines
+55
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a general problem but I don't think it's a problem we'll encounter. This risks having race conditions if the username is available and then a separate request is made to update the patron with that username. If another patron is in the process of creating an account or updating their own username to the same one, then we'll have this race condition and error. This will be caught regardless on line 66 and also, there's really not much we can do anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay good to note |
||
return { status: 200, message: `Username updated to ${newUsername}` } | ||
} else if (response?.type === "unavailable-username") { | ||
// Username taken but not an error, returns a message. | ||
return { status: 200, message: "Username taken" } | ||
} else { | ||
throw new Error("Username update failed") | ||
} | ||
} | ||
} catch (error) { | ||
return { | ||
status: error?.status || 500, | ||
message: | ||
error?.message || | ||
error.response?.data?.description || | ||
"An error occurred", | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* PUT request to Sierra to update patron settings. Returns status and message about request. | ||
*/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { NextApiResponse, NextApiRequest } from "next" | ||
import initializePatronTokenAuth from "../../../../src/server/auth" | ||
import { updateUsername } from "../helpers" | ||
|
||
/** | ||
* API route handler for /api/account/username/{patronId} | ||
*/ | ||
export default async function handler( | ||
req: NextApiRequest, | ||
res: NextApiResponse | ||
) { | ||
let responseMessage = "Request error" | ||
let responseStatus = 400 | ||
const patronTokenResponse = await initializePatronTokenAuth(req.cookies) | ||
const cookiePatronId = patronTokenResponse.decodedPatron?.sub | ||
if (!cookiePatronId) { | ||
responseStatus = 403 | ||
responseMessage = "No authenticated patron" | ||
return res.status(responseStatus).json(responseMessage) | ||
} | ||
if (req.method == "GET") { | ||
responseMessage = "Please make a PUT request to this endpoint." | ||
} | ||
if (req.method == "PUT") { | ||
/** We get the patron id from the request: */ | ||
const patronId = req.query.id as string | ||
const { username } = req.body | ||
/** We check that the patron cookie matches the patron id in the request, | ||
* i.e.,the logged in user is updating their own username. */ | ||
if (patronId == cookiePatronId) { | ||
const response = await updateUsername(patronId, username) | ||
responseStatus = response.status | ||
responseMessage = response.message | ||
} else { | ||
responseStatus = 403 | ||
responseMessage = "Authenticated patron does not match request" | ||
} | ||
} | ||
res.status(responseStatus).json(responseMessage) | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this extraction 🌝 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { Banner, Text, Link } from "@nypl/design-system-react-components" | ||
|
||
export type StatusType = "" | "failure" | "usernameFailure" | "success" | ||
|
||
type StatusBannerProps = { | ||
status: StatusType | ||
statusMessage: string | ||
} | ||
|
||
const successContent = ( | ||
<Text marginBottom={0} color="ui.black !important"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to verify, the design token with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
Your changes were saved. | ||
</Text> | ||
) | ||
|
||
const generalFailureContent = ( | ||
<Text marginBottom={0} color="ui.black !important"> | ||
Your changes were not saved. | ||
</Text> | ||
) | ||
|
||
const specificFailureContent = (statusMessage: string) => { | ||
return ( | ||
<Text marginBottom={0} color={"ui.black !important"}> | ||
{statusMessage} Please try again or{" "} | ||
<Link href="https://www.nypl.org/get-help/contact-us">contact us</Link>{" "} | ||
for assistance. | ||
</Text> | ||
) | ||
} | ||
|
||
const statusContent = (status, statusMessage) => { | ||
if (status === "success") { | ||
return successContent | ||
} | ||
if (status === "failure" && statusMessage !== "") { | ||
return specificFailureContent(statusMessage) | ||
} else { | ||
return generalFailureContent | ||
} | ||
} | ||
|
||
export const StatusBanner = ({ status, statusMessage }: StatusBannerProps) => { | ||
return ( | ||
<Banner | ||
sx={{ marginTop: "m" }} | ||
isDismissible | ||
content={ | ||
<div style={{ alignItems: "center" }}> | ||
{statusContent(status, statusMessage)} | ||
</div> | ||
} | ||
type={status === "failure" ? "negative" : "positive"} | ||
/> | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this return a response to verify it was successful? Or is it a quiet "there was no error so it's good" validation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes just 200 if successful, no body