Skip to content

Commit 6537f97

Browse files
authored
Auth & Downloadable Images (#69)
* update lock file * add basic guard to archive * authentication v1 * move containers into components dir * make own modals dir * better modals and downloading images * show footer when src * previous and next image buttons * fix theme rendering * update api url
1 parent b4dba22 commit 6537f97

43 files changed

Lines changed: 609 additions & 214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 0 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import { Image, Size } from "./model";
2-
3-
// CONSTANTS
4-
// const API_URL = "http://127.0.0.1:8000";
5-
const API_URL = "https://rex-api-505972842640.europe-west2.run.app";
6-
7-
const THUMBNAIL_BUCKET = "rex-thumbnails";
1+
import { API_URL, Image, Size, THUMBNAIL_BUCKET, TokenResponse } from "./model";
82

93
// UTILITY FUNCS
104
export const getThumbnailURL = (image_name: string): string => {
@@ -17,17 +11,36 @@ export const getThumbnailURL = (image_name: string): string => {
1711
* Wake's up the API.
1812
*/
1913
export async function wakeUp() {
20-
try {
21-
const response = await fetch(`${API_URL}/`);
22-
if (!response.ok) {
23-
throw new Error(`api did not wake up!`);
24-
}
25-
} catch (error) {
26-
console.log(error);
27-
throw error;
14+
const response = await fetch(`${API_URL}/`);
15+
if (!response.ok) {
16+
console.error(`Rex API did not wake up... ${response.status}`);
2817
}
2918
}
3019

20+
// - AUTH
21+
22+
/**
23+
* Authenticates and fetches Bearer Token.
24+
*
25+
* @param {FormData} authForm
26+
*
27+
* @returns {TokenResponse} Token Response.
28+
*/
29+
export async function getToken(authForm: FormData): Promise<TokenResponse> {
30+
const response = await fetch(`${API_URL}/token`, {
31+
method: "POST",
32+
body: authForm,
33+
});
34+
35+
if (!response.ok) {
36+
throw new Error(`Fetching Token failed | status: ${response.status}`);
37+
}
38+
39+
return await response.json();
40+
}
41+
42+
// - PHOTOGRAPHY
43+
3144
/**
3245
* Fetches a photo from a given album.
3346
*
@@ -37,22 +50,63 @@ export async function wakeUp() {
3750
* @returns {Image} The desired photo.
3851
*/
3952
export async function getPhoto(
53+
token: string,
4054
image_name: string,
41-
size: Size = Size.LARGE
55+
size: Size = Size.MEDIUM,
4256
): Promise<Image> {
4357
const encoded_path = encodeURI(`${image_name}?size=${size}`);
4458
const url = `${API_URL}/photography/${encoded_path}`;
4559

46-
try {
47-
const response = await fetch(url);
48-
if (!response.ok) {
49-
throw new Error(
50-
`failed to fetch photo '${image_name}' | status: ${response.status}`
51-
);
52-
}
53-
return await response.json();
54-
} catch (error) {
55-
console.log(error);
56-
throw error;
60+
const response = await fetch(url, {
61+
headers: { Authorization: `Bearer ${token}` },
62+
});
63+
64+
if (response.status == 401) {
65+
throw new Error(`Fetching Photo failed | unauthenticated`);
66+
} else if (!response.ok) {
67+
throw new Error(
68+
`failed to fetch photo '${image_name}' | status: ${response.status}`,
69+
);
5770
}
71+
72+
return await response.json();
73+
}
74+
75+
/**
76+
* Downloads a photo.
77+
*
78+
* @param {string} image_name
79+
* @param {Size} [size=Size.MEDIUM] Optional
80+
*
81+
* @returns {Image} The desired photo.
82+
*/
83+
export async function downloadPhoto(
84+
token: string,
85+
image_name: string,
86+
size: Size = Size.MEDIUM,
87+
): Promise<void> {
88+
const encoded_path = encodeURI(`${image_name}/download?size=${size}`);
89+
const url = `${API_URL}/photography/${encoded_path}`;
90+
91+
const response = await fetch(url, {
92+
headers: { Authorization: `Bearer ${token}` },
93+
});
94+
95+
if (response.status == 401) {
96+
throw new Error(`Fetching Photo failed | unauthenticated`);
97+
} else if (!response.ok) {
98+
throw new Error(
99+
`failed to download photo '${image_name}' | status: ${response.status}`,
100+
);
101+
}
102+
103+
const blob = await response.blob();
104+
const blobUrl = URL.createObjectURL(blob);
105+
const link = document.createElement("a");
106+
link.href = blobUrl;
107+
link.download = image_name;
108+
document.body.appendChild(link);
109+
link.click();
110+
document.body.removeChild(link);
111+
URL.revokeObjectURL(blobUrl);
58112
}

src/api/rex-api/model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
// CONSTANTS
2+
// export const API_URL = "http://127.0.0.1:8080";
3+
export const API_URL = "https://rex-api-505972842640.europe-west2.run.app";
4+
5+
export const THUMBNAIL_BUCKET = "rex-thumbnails";
6+
17
interface PhotoInfo {
28
alt?: string; // alt text for the photo
39
}
@@ -11,6 +17,11 @@ export interface Album {
1117

1218
// RESPONSE TYPES
1319

20+
export interface TokenResponse {
21+
access_token: string;
22+
token_type: string;
23+
}
24+
1425
export interface Image {
1526
url: string;
1627
name?: string;
@@ -19,5 +30,6 @@ export interface Image {
1930

2031
export enum Size {
2132
THUMBNAIL = "thumbnail",
33+
MEDIUM = "medium",
2234
LARGE = "large",
2335
}

src/components/About.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import PlaceIcon from "@mui/icons-material/Place";
22

3-
import Container from "../containers/Container";
3+
import Container from "./containers/Container";
44
import { PDFLink } from "./CustomLinks";
55
import { Tags } from "./Tags";
66

src/components/Auth.module.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.Wrapper {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 20px;
5+
}
6+
7+
.PasswordForm {
8+
display: flex;
9+
flex-direction: column;
10+
align-items: center;
11+
background-color: var(--background);
12+
gap: 5px;
13+
padding: 20px;
14+
border-radius: 20px;
15+
}
16+
17+
.Back {
18+
background-color: var(--background);
19+
gap: 5px;
20+
padding: 20px;
21+
border-radius: 20px;
22+
cursor: pointer;
23+
text-align: center;
24+
}

src/components/Auth.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useState } from "react";
2+
import { useNavigate } from "react-router-dom";
3+
4+
import { useAuth } from "@contexts";
5+
6+
import { getToken } from "../api/rex-api/fetchPhotography";
7+
8+
import styles from "./Auth.module.css";
9+
10+
export const PasswordForm = ({
11+
onAuthenticated,
12+
}: {
13+
onAuthenticated: () => void;
14+
}) => {
15+
const { storeToken } = useAuth();
16+
17+
const navigate = useNavigate();
18+
19+
const [wrongPwEntered, setWrongPwEntered] = useState<boolean>(false);
20+
21+
const enterPassword = (authForm: FormData) => {
22+
authForm.set("username", "rexgreenway");
23+
getToken(authForm)
24+
.then((resp) => {
25+
storeToken(resp.access_token);
26+
onAuthenticated();
27+
})
28+
.catch((error) => {
29+
console.error("Authentication Error: ", error);
30+
setWrongPwEntered(true);
31+
});
32+
};
33+
34+
return (
35+
<div className={styles.Wrapper}>
36+
<form
37+
className={styles.PasswordForm}
38+
onSubmit={(e) => {
39+
e.preventDefault();
40+
const formData = new FormData(e.currentTarget);
41+
enterPassword(formData);
42+
}}
43+
>
44+
<h2>Enter Password:</h2>
45+
<div>
46+
<input type="password" name="password" />
47+
</div>
48+
{wrongPwEntered && <h3 style={{ color: "red" }}>WRONG PASSWORD</h3>}
49+
</form>
50+
<div className={styles.Back} onClick={() => navigate("..")}>
51+
<p>{"<-"} Back to Home</p>
52+
</div>
53+
</div>
54+
);
55+
};

src/components/Footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GitHub, Instagram, LinkedIn, ViewStream } from "@mui/icons-material";
22

3-
import Container from "../containers/Container";
3+
import Container from "./containers/Container";
44
import { ToggleThemeButton } from "./elements";
55

66
import styles from "./Footer.module.css";

src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import RexLogo from "../assets/R-Logo.svg?react";
55

66
import { CustomRouteObject } from "../router/routes";
77

8-
import Container from "../containers/Container";
8+
import Container from "./containers/Container";
99

1010
import styles from "./Header.module.css";
1111

0 commit comments

Comments
 (0)