Files
freeCodeCamp/client/src/utils/ajax.ts
Nicholas Carrigan (he/him) 8614db7a32 feat: enable new curriculum (#44183)
* feat: use legacy flag

chore: reorder challenges

fix: linter

revert: server change

feat: unblock new editor

fix: proper order

fix: 0-based order

fix: broke the order

feat: move tribute certification to its own block

feat: split the old projects block into 4

fix: put all blocks in order

chore: add intro text

refactor: use block, not blockName in query

fix: project progress indicator

* fix: reorder new challenges/certs

* fix: reorder legacy challenges

* fix: reintroduce legacy certs

* feat: add showNewCurriculum flag to env

* chore: forgot sample.env

* feat: use feature flag for display

* fix: rename meta + dirs to match new blocks

* fix: add new blocks to help-category-map

* fix: update completion-modal for new GQL schema

* test: duplicate title/id errors ->  warnings

* fix: update completion-modal to new GQL schema Mk2

* chore: re-order metas (again)

* fix: revert super-block-intro changes

The intro needs to show both legacy and new content.  We need to decide
which pages are created, rather than than what a page shows when
rendered.

* feat: move upcoming curriculum into own superblock

* fix: handle one certification with two superBlocks

* fix: remove duplicated intros

* fix: remove duplicate projects from /settings

* fix: drop 'two' from Responsive Web Design Two

* chore: rename slug suffix from two to v2

* feat: control display of new curriculum

* feat: control project paths shown on /settings

* fix: use new project order for /settings

This does mean that /settings will change before the release, but I
don't think it's serious.  All the projects are there, just not in the
legacy order.

* fix: claim/show cert button

* chore: remove isLegacy

Since we have legacy superblocks, we don't currently need individual
blocks to be legacy

* test: fix utils.test

* fix: verifyCanClaim needs certification

If Shaun removes the cert claim cards, maybe we can remove this entirely

* fix: add hasEditableBoundaries flags where needed

* chore: remove isUpcomingChange

* chore: v2 -> 22

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
2021-12-20 12:36:31 -06:00

282 lines
7.1 KiB
TypeScript

import cookies from 'browser-cookies';
import envData from '../../../config/env.json';
import { FlashMessageArg } from '../components/Flash/redux';
import type {
ChallengeFile,
ClaimedCertifications,
CompletedChallenge,
User
} from '../redux/prop-types';
const { apiLocation } = envData;
const base = apiLocation;
const defaultOptions: RequestInit = {
credentials: 'include'
};
// csrf_token is passed to the client as a cookie. The client must send
// this back as a header.
function getCSRFToken() {
const token =
typeof window !== 'undefined' ? cookies.get('csrf_token') : null;
return token ?? '';
}
async function get<T>(path: string): Promise<T> {
return fetch(`${base}${path}`, defaultOptions).then<T>(res => res.json());
}
export function post<T = void>(path: string, body: unknown): Promise<T> {
return request('POST', path, body);
}
function put<T = void>(path: string, body: unknown): Promise<T> {
return request('PUT', path, body);
}
function deleteRequest<T = void>(path: string, body: unknown): Promise<T> {
return request('DELETE', path, body);
}
async function request<T>(
method: 'POST' | 'PUT' | 'DELETE',
path: string,
body: unknown
): Promise<T> {
const options: RequestInit = {
...defaultOptions,
method,
headers: {
'CSRF-Token': getCSRFToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
};
return fetch(`${base}${path}`, options).then<T>(res => res.json());
}
/** GET **/
interface SessionUser {
user?: { [username: string]: User };
sessionMeta: { activeDonations: number };
}
type ChallengeFilesForFiles = {
files: Array<Omit<ChallengeFile, 'fileKey'> & { key: string }>;
} & Omit<CompletedChallenge, 'challengeFiles'>;
type ApiSessionResponse = Omit<SessionUser, 'user'>;
type ApiUser = {
user: {
[username: string]: Omit<User, 'completedChallenges'> & {
completedChallenges?: ChallengeFilesForFiles[];
};
};
result?: string;
};
type UserResponse = {
user: { [username: string]: User } | Record<string, never>;
result: string | undefined;
};
function parseApiResponseToClientUser(data: ApiUser): UserResponse {
const userData = data.user?.[data?.result ?? ''];
let completedChallenges: CompletedChallenge[] = [];
if (userData) {
completedChallenges =
userData.completedChallenges?.reduce(
(acc: CompletedChallenge[], curr: ChallengeFilesForFiles) => {
return [
...acc,
{
...curr,
challengeFiles: curr.files.map(({ key: fileKey, ...file }) => ({
...file,
fileKey
}))
}
];
},
[]
) ?? [];
}
return {
user: { [data.result ?? '']: { ...userData, completedChallenges } },
result: data.result
};
}
export function getSessionUser(): Promise<SessionUser> {
const response: Promise<ApiUser & ApiSessionResponse> = get(
'/user/get-session-user'
);
// TODO: Once DB is migrated, no longer need to parse `files` -> `challengeFiles` etc.
return response.then(data => {
const { result, user } = parseApiResponseToClientUser(data);
return {
sessionMeta: data.sessionMeta,
result,
user
};
});
}
type UserProfileResponse = {
entities: Omit<UserResponse, 'result'>;
result: string | undefined;
};
export function getUserProfile(username: string): Promise<UserProfileResponse> {
const response: Promise<{ entities?: ApiUser; result?: string }> = get(
`/api/users/get-public-profile?username=${username}`
);
return response.then(data => {
const { result, user } = parseApiResponseToClientUser({
user: data.entities?.user ?? {},
result: data.result
});
return {
entities: { user },
result
};
});
}
interface Cert {
certTitle: string;
username: string;
date: Date;
completionTime: string;
}
export function getShowCert(username: string, certSlug: string): Promise<Cert> {
return get(`/certificate/showCert/${username}/${certSlug}`);
}
export function getUsernameExists(username: string): Promise<boolean> {
return get(`/api/users/exists?username=${username}`);
}
export interface GetVerifyCanClaimCert {
response: {
type: string;
message: {
status: boolean;
result: string;
};
variables: {
name: string;
};
};
isCertMap: ClaimedCertifications;
completedChallenges: CompletedChallenge[];
message?: FlashMessageArg;
}
export function getVerifyCanClaimCert(
username: string,
certification: string
): Promise<GetVerifyCanClaimCert> {
return get(
`/certificate/verify-can-claim-cert?username=${username}&superBlock=${certification}`
);
}
/** POST **/
interface Donation {
email: string;
amount: number;
duration: string;
provider: string;
subscriptionId: string;
customerId: string;
startDate: Date;
}
// TODO: Verify if the body has and needs this Donation type. The api seems to
// just need the body to exist, but doesn't seem to use the properties.
export function addDonation(body: Donation): Promise<void> {
return post('/donate/add-donation', body);
}
export function postChargeStripe(body: Donation): Promise<void> {
return post('/donate/charge-stripe', body);
}
export function postChargeStripeCard(body: Donation): Promise<void> {
return post('/donate/charge-stripe-card', body);
}
interface Report {
username: string;
reportDescription: string;
}
export function postReportUser(body: Report): Promise<void> {
return post('/user/report-user', body);
}
// Both are called without a payload in danger-zone-saga,
// which suggests both are sent without any body
// TODO: Convert to DELETE
export function postDeleteAccount(): Promise<void> {
return post('/account/delete', {});
}
export function postResetProgress(): Promise<void> {
return post('/account/reset-progress', {});
}
export function postWebhookToken(): Promise<void> {
return post('/user/webhook-token', {});
}
/** PUT **/
interface MyAbout {
name: string;
location: string;
about: string;
picture: string;
}
export function putUpdateMyAbout(values: MyAbout): Promise<void> {
return put('/update-my-about', { ...values });
}
export function putUpdateMyUsername(username: string): Promise<void> {
return put('/update-my-username', { username });
}
export function putUpdateMyProfileUI(
profileUI: User['profileUI']
): Promise<void> {
return put('/update-my-profileui', { profileUI });
}
// Update should contain only one flag and one new value
// It's possible to constrain to only one key with TS, but is overkill for this
// https://stackoverflow.com/a/60807986
export function putUpdateUserFlag(
update: Record<string, string>
): Promise<void> {
return put('/update-user-flag', update);
}
export function putUserAcceptsTerms(quincyEmails: boolean): Promise<void> {
return put('/update-privacy-terms', { quincyEmails });
}
export function putUserUpdateEmail(email: string): Promise<void> {
return put('/update-my-email', { email });
}
export function putVerifyCert(certSlug: string): Promise<void> {
return put('/certificate/verify', { certSlug });
}
/** DELETE **/
export function deleteWebhookToken(): Promise<void> {
return deleteRequest('/user/webhook-token', {});
}