diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 17d475a7..c5bf2907 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -11,6 +11,8 @@ import EditRobotForm from "pages/EditRobotForm";
import Home from "pages/Home";
import Login from "pages/Login";
import Logout from "pages/Logout";
+import MyParts from "pages/MyParts";
+import MyRobots from "pages/MyRobots";
import NewPart from "pages/NewPart";
import NewRobot from "pages/NewRobot";
import NotFound from "pages/NotFound";
@@ -19,8 +21,6 @@ import Parts from "pages/Parts";
import RobotDetails from "pages/RobotDetails";
import Robots from "pages/Robots";
import TestImages from "pages/TestImages";
-import YourParts from "pages/YourParts";
-import YourRobots from "pages/YourRobots";
import { Container } from "react-bootstrap";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import "./App.css";
@@ -44,13 +44,13 @@ const App = () => {
} />
} />
} />
- } />
- } />
+ } />
+ } />
} />
} />
- } />
- } />
- } />
+ } />
+ } />
+ } />
} />
} />
diff --git a/frontend/src/components/nav/TopNavbar.tsx b/frontend/src/components/nav/TopNavbar.tsx
index 41b99033..2538a6a7 100644
--- a/frontend/src/components/nav/TopNavbar.tsx
+++ b/frontend/src/components/nav/TopNavbar.tsx
@@ -17,7 +17,7 @@ const TopNavbar = () => {
useEffect(() => {
(async () => {
try {
- // get code from query string to carry out oauth login
+ // Get the code from the query string to carry out OAuth login.
const search = window.location.search;
const params = new URLSearchParams(search);
const code = params.get("code");
@@ -25,8 +25,8 @@ const TopNavbar = () => {
const { email } = await auth_api.me();
auth.setEmail(email);
} else if (code) {
- const res = await auth_api.login_github(code as string);
- setLocalStorageAuth(res.username);
+ const res = await auth_api.loginGithub(code as string);
+ setLocalStorageAuth(res.api_key_id);
auth.setIsAuthenticated(true);
}
} catch (error) {
diff --git a/frontend/src/hooks/api.tsx b/frontend/src/hooks/api.tsx
index d63f5358..21a5f75c 100644
--- a/frontend/src/hooks/api.tsx
+++ b/frontend/src/hooks/api.tsx
@@ -37,13 +37,21 @@ export interface Robot {
packages: Package[];
}
+interface GithubAuthResponse {
+ api_key_id: string;
+}
+
interface MeResponse {
- id: string;
+ user_id: string;
email: string;
username: string;
admin: boolean;
}
+interface UploadImageResponse {
+ image_id: string;
+}
+
export class api {
public api: AxiosInstance;
@@ -51,7 +59,7 @@ export class api {
this.api = api;
}
- public async send_register_github(): Promise {
+ public async sendRegisterGithub(): Promise {
try {
const res = await this.api.get("/users/github/login");
return res.data;
@@ -68,9 +76,11 @@ export class api {
}
}
- public async login_github(code: string): Promise {
+ public async loginGithub(code: string): Promise {
try {
- const res = await this.api.get(`/users/github/code/${code}`);
+ const res = await this.api.get(
+ `/users/github/code/${code}`,
+ );
return res.data;
} catch (error) {
if (axios.isAxiosError(error)) {
@@ -87,7 +97,7 @@ export class api {
public async logout(): Promise {
try {
- await this.api.delete("/users/logout/");
+ await this.api.delete("/users/logout/");
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Error logging out:", error.response?.data);
@@ -101,7 +111,7 @@ export class api {
public async me(): Promise {
try {
- const res = await this.api.get("/users/me/");
+ const res = await this.api.get("/users/me/");
return res.data;
} catch (error) {
if (axios.isAxiosError(error)) {
@@ -156,9 +166,9 @@ export class api {
return map;
}
- public async getYourRobots(page: number): Promise<[Robot[], boolean]> {
+ public async getMyRobots(page: number): Promise<[Robot[], boolean]> {
try {
- const response = await this.api.get("/robots/your/", {
+ const response = await this.api.get("/robots/me/", {
params: { page },
});
return response.data;
@@ -174,6 +184,7 @@ export class api {
}
}
}
+
public async getRobotById(robotId: string | undefined): Promise {
try {
const response = await this.api.get(`/robots/${robotId}`);
@@ -188,6 +199,7 @@ export class api {
}
}
}
+
public async getPartById(partId: string | undefined): Promise {
try {
const response = await this.api.get(`/parts/${partId}`);
@@ -202,6 +214,7 @@ export class api {
}
}
}
+
public async currentUser(): Promise {
try {
const response = await this.api.get("/users/me");
@@ -218,6 +231,7 @@ export class api {
}
}
}
+
public async addRobot(robot: Robot): Promise {
const s = robot.name;
try {
@@ -234,6 +248,7 @@ export class api {
}
}
}
+
public async deleteRobot(id: string | undefined): Promise {
const s = id;
try {
@@ -250,10 +265,11 @@ export class api {
}
}
}
+
public async editRobot(robot: Robot): Promise {
const s = robot.name;
try {
- await this.api.post(`robots/edit-robot/${robot.id}/`, robot);
+ await this.api.post(`/robots/edit/${robot.id}/`, robot);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Error editing robot:", error.response?.data);
@@ -301,9 +317,10 @@ export class api {
}
}
}
- public async getYourParts(page: number): Promise<[Part[], boolean]> {
+
+ public async getMyParts(page: number): Promise<[Part[], boolean]> {
try {
- const response = await this.api.get("/parts/your/", { params: { page } });
+ const response = await this.api.get("/parts/me/", { params: { page } });
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
@@ -315,6 +332,7 @@ export class api {
}
}
}
+
public async addPart(part: Part): Promise {
const s = part.name;
try {
@@ -331,10 +349,11 @@ export class api {
}
}
}
+
public async deletePart(id: string | undefined): Promise {
const s = id;
try {
- await this.api.delete(`parts/delete/${id}/`);
+ await this.api.delete(`/parts/delete/${id}/`);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Error deleting part:", error.response?.data);
@@ -347,10 +366,11 @@ export class api {
}
}
}
+
public async editPart(part: Part): Promise {
const s = part.name;
try {
- await this.api.post(`parts/edit-part/${part.id}/`, part);
+ await this.api.post(`/parts/edit/${part.id}/`, part);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Error editing part:", error.response?.data);
@@ -363,32 +383,19 @@ export class api {
}
}
}
- public async getImage(imageId: string): Promise {
- try {
- const response = await this.api.get(`/image/${imageId}/`, {
- responseType: "blob",
- });
- return response.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- console.error("Error fetching image:", error.response?.data);
- throw new Error(
- error.response?.data?.detail || "Error fetching image " + imageId,
- );
- } else {
- console.error("Unexpected error:", error);
- throw new Error("Unexpected error");
- }
- }
- }
+
public async uploadImage(formData: FormData): Promise {
try {
- const res = await this.api.post("/image/upload/", formData, {
- headers: {
- "Content-Type": "multipart/form-data",
+ const res = await this.api.post(
+ "/image/upload/",
+ formData,
+ {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
},
- });
- return res.data.id;
+ );
+ return res.data.image_id;
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Error uploading image:", error.response?.data);
diff --git a/frontend/src/pages/EditPartForm.tsx b/frontend/src/pages/EditPartForm.tsx
index 30f8aab2..b050456d 100644
--- a/frontend/src/pages/EditPartForm.tsx
+++ b/frontend/src/pages/EditPartForm.tsx
@@ -56,7 +56,7 @@ const EditPartForm: React.FC = () => {
try {
await auth_api.editPart(newFormData);
setMessage(`Part edited successfully.`);
- navigate(`/parts/your/1`);
+ navigate(`/parts/me/1`);
} catch (error) {
setMessage("Error adding part ");
}
diff --git a/frontend/src/pages/EditRobotForm.tsx b/frontend/src/pages/EditRobotForm.tsx
index 339d1205..066ba5ee 100644
--- a/frontend/src/pages/EditRobotForm.tsx
+++ b/frontend/src/pages/EditRobotForm.tsx
@@ -76,7 +76,7 @@ const EditRobotForm: React.FC = () => {
try {
await auth_api.editRobot(newFormData);
setMessage(`Robot edited successfully.`);
- navigate(`/robots/your/1`);
+ navigate(`/robots/me/1`);
} catch (error) {
setMessage("Error adding robot ");
}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 45a27214..c1d1f289 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -45,10 +45,10 @@ const Home: React.FC = () => {
width: "100%",
}}
onClick={() => {
- navigate("/robots/your/1");
+ navigate("/robots/me/1");
}}
>
- View Your Robots
+ View My Robots
@@ -59,10 +59,10 @@ const Home: React.FC = () => {
width: "100%",
}}
onClick={() => {
- navigate("/parts/your/1");
+ navigate("/parts/me/1");
}}
>
- View Your Parts
+ View My Parts
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index 343bc149..90a4e973 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -16,7 +16,7 @@ const Login = () => {
const handleGithubSubmit = async (event: FormEvent) => {
event.preventDefault();
try {
- const redirectUrl = await auth_api.send_register_github();
+ const redirectUrl = await auth_api.sendRegisterGithub();
window.location.href = redirectUrl;
} catch (err) {
if (err instanceof Error) {
diff --git a/frontend/src/pages/YourParts.tsx b/frontend/src/pages/MyParts.tsx
similarity index 91%
rename from frontend/src/pages/YourParts.tsx
rename to frontend/src/pages/MyParts.tsx
index a7d606d6..b03fe57d 100644
--- a/frontend/src/pages/YourParts.tsx
+++ b/frontend/src/pages/MyParts.tsx
@@ -14,7 +14,7 @@ import {
import Markdown from "react-markdown";
import { Link, useNavigate, useParams } from "react-router-dom";
-const YourParts = () => {
+const MyParts = () => {
const auth = useAuthentication();
const auth_api = new api(auth.api);
const [partsData, setParts] = useState(null);
@@ -35,7 +35,7 @@ const YourParts = () => {
useEffect(() => {
const fetch_parts = async () => {
try {
- const partsQuery = await auth_api.getYourParts(pageNumber);
+ const partsQuery = await auth_api.getMyParts(pageNumber);
setParts(partsQuery[0]);
setMoreParts(partsQuery[1]);
} catch (err) {
@@ -70,7 +70,7 @@ const YourParts = () => {
<>
navigate("/")}>Home
- Your Parts
+ My Parts
@@ -120,12 +120,12 @@ const YourParts = () => {
{pageNumber > 1 && (
- Previous Page
+ Previous Page
)}
{moreParts && (
- Next Page
+ Next Page
)}
@@ -134,4 +134,4 @@ const YourParts = () => {
);
};
-export default YourParts;
+export default MyParts;
diff --git a/frontend/src/pages/YourRobots.tsx b/frontend/src/pages/MyRobots.tsx
similarity index 90%
rename from frontend/src/pages/YourRobots.tsx
rename to frontend/src/pages/MyRobots.tsx
index fa64c7ab..1b3f0e51 100644
--- a/frontend/src/pages/YourRobots.tsx
+++ b/frontend/src/pages/MyRobots.tsx
@@ -14,7 +14,7 @@ import {
import Markdown from "react-markdown";
import { Link, useNavigate, useParams } from "react-router-dom";
-const YourRobots = () => {
+const MyRobots = () => {
const auth = useAuthentication();
const auth_api = new api(auth.api);
const [robotsData, setRobot] = useState([]);
@@ -34,9 +34,9 @@ const YourRobots = () => {
}
useEffect(() => {
- const fetch_your_robots = async () => {
+ const fetchMyRobots = async () => {
try {
- const robotsQuery = await auth_api.getYourRobots(pageNumber);
+ const robotsQuery = await auth_api.getMyRobots(pageNumber);
setRobot(robotsQuery[0]);
setMoreRobots(robotsQuery[1]);
} catch (err) {
@@ -47,7 +47,7 @@ const YourRobots = () => {
}
}
};
- fetch_your_robots();
+ fetchMyRobots();
}, [pageNumber]);
const navigate = useNavigate();
@@ -70,7 +70,7 @@ const YourRobots = () => {
<>
navigate("/")}>Home
- Your Robots
+ My Robots
@@ -120,12 +120,12 @@ const YourRobots = () => {
{pageNumber > 1 && (
- Previous Page
+ Previous Page
)}
{moreRobots && (
- Next Page
+ Next Page
)}
@@ -134,4 +134,4 @@ const YourRobots = () => {
);
};
-export default YourRobots;
+export default MyRobots;
diff --git a/frontend/src/pages/NewPart.tsx b/frontend/src/pages/NewPart.tsx
index ff6e5806..59ad0de7 100644
--- a/frontend/src/pages/NewPart.tsx
+++ b/frontend/src/pages/NewPart.tsx
@@ -31,7 +31,7 @@ const NewPart: React.FC = () => {
try {
await auth_api.addPart(newFormData);
setMessage(`Part added successfully.`);
- navigate("/parts/your/1");
+ navigate("/parts/me/1");
} catch (error) {
setMessage("Error adding Part ");
}
diff --git a/frontend/src/pages/NewRobot.tsx b/frontend/src/pages/NewRobot.tsx
index 208bc763..71bad6a9 100644
--- a/frontend/src/pages/NewRobot.tsx
+++ b/frontend/src/pages/NewRobot.tsx
@@ -44,7 +44,7 @@ const NewRobot: React.FC = () => {
try {
await auth_api.addRobot(newFormData);
setMessage(`Robot added successfully.`);
- navigate(`/robots/your/1`);
+ navigate(`/robots/me/1`);
} catch (error) {
setMessage("Error adding robot ");
}
diff --git a/frontend/src/pages/PartDetails.tsx b/frontend/src/pages/PartDetails.tsx
index 1223ae6f..1be41861 100644
--- a/frontend/src/pages/PartDetails.tsx
+++ b/frontend/src/pages/PartDetails.tsx
@@ -168,7 +168,7 @@ const PartDetails = () => {
width: "100%",
}}
onClick={() => {
- navigate(`/edit-part/${id}/`);
+ navigate(`/part/edit/${id}/`);
}}
>
Edit Part
@@ -210,7 +210,7 @@ const PartDetails = () => {
variant="danger"
onClick={async () => {
await auth_api.deletePart(id);
- navigate(`/parts/your/1`);
+ navigate(`/parts/me/1`);
}}
>
Delete Part
diff --git a/frontend/src/pages/RobotDetails.tsx b/frontend/src/pages/RobotDetails.tsx
index 1b9114f1..c856c552 100644
--- a/frontend/src/pages/RobotDetails.tsx
+++ b/frontend/src/pages/RobotDetails.tsx
@@ -277,7 +277,7 @@ const RobotDetails = () => {
width: "100%",
}}
onClick={() => {
- navigate(`/edit-robot/${id}/`);
+ navigate(`/robot/edit/${id}/`);
}}
>
Edit Robot
@@ -319,7 +319,7 @@ const RobotDetails = () => {
variant="danger"
onClick={async () => {
await auth_api.deleteRobot(id);
- navigate(`/robots/your/1`);
+ navigate(`/robots/me/1`);
}}
>
Delete Robot
diff --git a/store/app/crud/base.py b/store/app/crud/base.py
index ac3b1da6..fb99227c 100644
--- a/store/app/crud/base.py
+++ b/store/app/crud/base.py
@@ -68,20 +68,25 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: #
if self.__s3 is not None:
await self.__s3.__aexit__(exc_type, exc_val, exc_tb)
- async def _add_item(self, item: RobolistBaseModel) -> None:
+ async def _add_item(self, item: RobolistBaseModel, unique_fields: list[str] | None = None) -> None:
table = await self.db.Table(TABLE_NAME)
item_data = item.model_dump()
if "type" in item_data:
raise ValueError("Cannot add item with 'type' attribute")
item_data["type"] = item.__class__.__name__
- await table.put_item(Item=item_data)
+ condition = "attribute_not_exists(id)"
+ if unique_fields:
+ for field in unique_fields:
+ assert hasattr(item, field), f"Item does not have field {field}"
+ condition += " AND " + " AND ".join(f"attribute_not_exists({field})" for field in unique_fields)
+ await table.put_item(
+ Item=item_data,
+ ConditionExpression=condition,
+ )
async def _delete_item(self, item: RobolistBaseModel | str) -> None:
table = await self.db.Table(TABLE_NAME)
- if isinstance(item, str):
- await table.delete_item(Key={"id": item})
- else:
- await table.delete_item(Key={"id": item.id})
+ await table.delete_item(Key={"id": item if isinstance(item, str) else item.id})
async def _list_items(
self,
@@ -131,7 +136,7 @@ async def _list(
sorted_items = sorted(response, key=sort_key, reverse=True)
return sorted_items[(page - 1) * ITEMS_PER_PAGE : page * ITEMS_PER_PAGE], page * ITEMS_PER_PAGE < len(response)
- async def _list_your(
+ async def _list_me(
self,
item_class: type[T],
user_id: str,
@@ -181,7 +186,7 @@ async def _get_item(self, item_id: str, item_class: type[T], throw_if_missing: b
item_dict = await table.get_item(Key={"id": item_id})
if "Item" not in item_dict:
if throw_if_missing:
- raise ValueError(f"Item {item_id} not found")
+ raise ValueError("Item not found")
return None
item_data = item_dict["Item"]
return self._validate_item(item_data, item_class)
diff --git a/store/app/crud/robots.py b/store/app/crud/robots.py
index dc1e00b0..90d2cb52 100644
--- a/store/app/crud/robots.py
+++ b/store/app/crud/robots.py
@@ -36,14 +36,14 @@ async def dump_parts(self) -> list[Part]:
async def list_robots(self, page: int, search_query: str | None = None) -> tuple[list[Robot], bool]:
return await self._list(Robot, page, lambda x: x.timestamp, search_query)
- async def list_your_robots(self, user_id: str, page: int, search_query: str) -> tuple[list[Robot], bool]:
- return await self._list_your(Robot, user_id, page, lambda x: x.timestamp, search_query)
+ async def list_user_robots(self, user_id: str, page: int, search_query: str) -> tuple[list[Robot], bool]:
+ return await self._list_me(Robot, user_id, page, lambda x: x.timestamp, search_query)
async def list_parts(self, page: int, search_query: str | None = None) -> tuple[list[Part], bool]:
return await self._list(Part, page, lambda x: x.timestamp, search_query)
- async def list_your_parts(self, user_id: str, page: int, search_query: str) -> tuple[list[Part], bool]:
- return await self._list_your(Part, user_id, page, lambda x: x.timestamp, search_query)
+ async def list_user_parts(self, user_id: str, page: int, search_query: str) -> tuple[list[Part], bool]:
+ return await self._list_me(Part, user_id, page, lambda x: x.timestamp, search_query)
async def upload_image(self, file: UploadFile) -> None:
bucket = await self.s3.Bucket(settings.s3.bucket)
diff --git a/store/app/crud/users.py b/store/app/crud/users.py
index ec5165ad..e8412992 100644
--- a/store/app/crud/users.py
+++ b/store/app/crud/users.py
@@ -31,50 +31,71 @@ def __init__(self) -> None:
def get_gsis(cls) -> list[GlobalSecondaryIndex]:
return super().get_gsis() + [
("emailIndex", "email", "S", "HASH"),
+ ("tokenIndex", "token", "S", "HASH"),
]
@overload
- async def get_user(self, id: str) -> User | None: ...
+ async def get_user(self, id: str, throw_if_missing: Literal[True]) -> User: ...
@overload
- async def get_user(self, id: str, throw_if_missing: Literal[True]) -> User: ...
+ async def get_user(self, id: str, throw_if_missing: bool = False) -> User | None: ...
async def get_user(self, id: str, throw_if_missing: bool = False) -> User | None:
return await self._get_item(id, User, throw_if_missing=throw_if_missing)
- async def create_user_from_token(self, token: str, email: str) -> User:
+ async def _create_user_from_email(self, email: str) -> User:
user = User.create(email=email)
- await self._add_item(user)
- key = OAuthKey.create(token, user.id)
- await self._add_item(key)
+ await self._add_item(user, unique_fields=["email"])
return user
- async def get_user_from_token(self, token: str) -> User | None:
- key = await self._get_item(token, OAuthKey, throw_if_missing=False)
- if key is None:
- return None
- return await self.get_user(key.user_id)
-
- async def create_user_from_github_token(self, github_id: str, email: str) -> User:
- return await self.create_user_from_token(github_auth_key(github_id), email)
-
- async def create_user_from_google_token(self, google_id: str, email: str) -> User:
- return await self.create_user_from_token(google_auth_key(google_id), email)
+ async def _create_user_from_auth_key(self, auth_key: str, email: str) -> User:
+ user = await self._create_user_from_email(email)
+ key = OAuthKey.create(auth_key, user.id)
+ await self._add_item(key, unique_fields=["user_token"])
+ return user
- async def get_user_from_github_token(self, token: str) -> User | None:
- return await self.get_user_from_token(github_auth_key(token))
+ @overload
+ async def _get_oauth_key(self, token: str, throw_if_missing: Literal[True]) -> OAuthKey: ...
- async def get_user_from_google_token(self, token: str) -> User | None:
- return await self.get_user_from_token(google_auth_key(token))
+ @overload
+ async def _get_oauth_key(self, token: str, throw_if_missing: bool = False) -> OAuthKey | None: ...
+
+ async def _get_oauth_key(self, token: str, throw_if_missing: bool = False) -> OAuthKey | None:
+ return await self._get_unique_item_from_secondary_index(
+ "tokenIndex",
+ "token",
+ token,
+ OAuthKey,
+ throw_if_missing=throw_if_missing,
+ )
+
+ async def _get_user_from_auth_key(self, token: str) -> User | None:
+ key = await self._get_oauth_key(token)
+ return None if key is None else await self.get_user(key.user_id)
+
+ async def get_user_from_github_token(self, token: str, email: str) -> User:
+ auth_key = github_auth_key(token)
+ user = await self._get_user_from_auth_key(auth_key)
+ if user is not None:
+ return user
+ return await self._create_user_from_auth_key(auth_key, email)
+
+ async def delete_github_token(self, github_id: str) -> None:
+ await self._delete_item(await self._get_oauth_key(github_auth_key(github_id), throw_if_missing=True))
+
+ async def get_user_from_google_token(self, token: str, email: str) -> User | None:
+ auth_key = google_auth_key(token)
+ user = await self._get_user_from_auth_key(auth_key)
+ if user is not None:
+ return user
+ return await self._create_user_from_auth_key(auth_key, email)
+
+ async def delete_google_token(self, google_id: str) -> None:
+ await self._delete_item(await self._get_oauth_key(google_auth_key(google_id), throw_if_missing=True))
async def get_user_from_email(self, email: str) -> User | None:
return await self._get_unique_item_from_secondary_index("emailIndex", "email", email, User)
- async def create_user_from_email(self, email: str) -> User:
- user = User.create(email=email)
- await self._add_item(user)
- return user
-
async def get_user_batch(self, ids: list[str]) -> list[User]:
return await self._get_item_batch(ids, User)
@@ -102,9 +123,9 @@ async def add_api_key(
source: APIKeySource,
permissions: APIKeyPermissionSet,
) -> APIKey:
- token = APIKey.create(user_id=user_id, source=source, permissions=permissions)
- await self._add_item(token)
- return token
+ api_key = APIKey.create(user_id=user_id, source=source, permissions=permissions)
+ await self._add_item(api_key)
+ return api_key
async def delete_api_key(self, token: APIKey | str) -> None:
await self._delete_item(token)
@@ -112,7 +133,7 @@ async def delete_api_key(self, token: APIKey | str) -> None:
async def test_adhoc() -> None:
async with UserCrud() as crud:
- await crud.create_user_from_email(email="ben@kscale.dev")
+ await crud._create_user_from_email(email="ben@kscale.dev")
if __name__ == "__main__":
diff --git a/store/app/model.py b/store/app/model.py
index f051a6a5..918d63bc 100644
--- a/store/app/model.py
+++ b/store/app/model.py
@@ -23,8 +23,7 @@ class RobolistBaseModel(BaseModel):
id: str
-class UserPermissions(BaseModel):
- is_admin: bool = False
+UserPermission = Literal["is_admin"]
class User(RobolistBaseModel):
@@ -36,26 +35,27 @@ class User(RobolistBaseModel):
"""
email: str
- permissions: UserPermissions = UserPermissions()
+ permissions: set[UserPermission] | None = None
@classmethod
def create(cls, email: str) -> Self:
- return cls(id=str(new_uuid()), email=email)
+ return cls(id=str(new_uuid()), email=email, permissions=None)
class OAuthKey(RobolistBaseModel):
"""Keys for OAuth providers which identify users."""
user_id: str
+ user_token: str
@classmethod
- def create(cls, token: str, user_id: str) -> Self:
- return cls(id=token, user_id=user_id)
+ def create(cls, user_token: str, user_id: str) -> Self:
+ return cls(id=str(new_uuid()), user_id=user_id, user_token=user_token)
APIKeySource = Literal["user", "oauth"]
APIKeyPermission = Literal["read", "write", "admin"]
-APIKeyPermissionSet = set[APIKeyPermission] | Literal["full"]
+APIKeyPermissionSet = set[APIKeyPermission] | Literal["full", None]
class APIKey(RobolistBaseModel):
@@ -68,7 +68,7 @@ class APIKey(RobolistBaseModel):
user_id: str
source: APIKeySource
- permissions: set[APIKeyPermission]
+ permissions: set[APIKeyPermission] | None = None
@classmethod
def create(
diff --git a/store/app/routers/auth/github.py b/store/app/routers/auth/github.py
index b27191e4..6711b729 100644
--- a/store/app/routers/auth/github.py
+++ b/store/app/routers/auth/github.py
@@ -8,7 +8,6 @@
from pydantic.main import BaseModel
from store.app.db import Crud
-from store.app.model import UserPermissions
from store.settings import settings
logger = logging.getLogger(__name__)
@@ -45,17 +44,16 @@ async def github_email_req(headers: dict[str, str]) -> HttpxResponse:
return await client.get("https://api.github.com/user/emails", headers=headers)
-class UserInfoResponse(BaseModel):
- id: str
- permissions: UserPermissions
+class GithubAuthResponse(BaseModel):
+ api_key_id: str
-@github_auth_router.get("/code/{code}", response_model=UserInfoResponse)
+@github_auth_router.get("/code/{code}", response_model=GithubAuthResponse)
async def github_code(
code: str,
crud: Annotated[Crud, Depends(Crud.get)],
response: Response,
-) -> UserInfoResponse:
+) -> GithubAuthResponse:
"""Gives the user a session token upon successful github authentication and creation of user.
Args:
@@ -84,14 +82,7 @@ async def github_code(
github_id = oauth_response.json()["html_url"]
email = next(entry["email"] for entry in oauth_email_response.json() if entry["primary"])
- user = await crud.get_user_from_github_token(github_id)
-
- # We create a new user if the user does not exist yet.
- if user is None:
- user = await crud.create_user_from_github_token(
- email=email,
- github_id=github_id,
- )
+ user = await crud.get_user_from_github_token(github_id, email)
api_key = await crud.add_api_key(
user_id=user.id,
@@ -101,4 +92,4 @@ async def github_code(
response.set_cookie(key="session_token", value=api_key.id, httponly=True, samesite="lax")
- return UserInfoResponse(id=user.id, permissions=user.permissions)
+ return GithubAuthResponse(api_key_id=api_key.id)
diff --git a/store/app/routers/image.py b/store/app/routers/image.py
index 6575349d..33bf4317 100644
--- a/store/app/routers/image.py
+++ b/store/app/routers/image.py
@@ -5,8 +5,9 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, UploadFile
-from fastapi.responses import JSONResponse, RedirectResponse
+from fastapi.responses import RedirectResponse
from PIL import Image
+from pydantic.main import BaseModel
from store.app.db import Crud
from store.settings import settings
@@ -17,8 +18,12 @@
logger = logging.getLogger(__name__)
+class UserInfoResponse(BaseModel):
+ image_id: str
+
+
@image_router.post("/upload/")
-async def upload_image(crud: Annotated[Crud, Depends(Crud.get)], file: UploadFile) -> JSONResponse:
+async def upload_image(crud: Annotated[Crud, Depends(Crud.get)], file: UploadFile) -> UserInfoResponse:
try:
if file.content_type != "image/png":
raise HTTPException(status_code=400, detail="Only PNG images are supported")
@@ -36,7 +41,7 @@ async def upload_image(crud: Annotated[Crud, Depends(Crud.get)], file: UploadFil
upload = UploadFile(filename="mini" + image_id + ".png", file=compressed_image_io)
await crud.upload_image(upload)
- return JSONResponse(status_code=200, content={"id": image_id})
+ return UserInfoResponse(image_id=image_id)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
diff --git a/store/app/routers/part.py b/store/app/routers/part.py
index d2022c37..b463736c 100644
--- a/store/app/routers/part.py
+++ b/store/app/routers/part.py
@@ -34,14 +34,14 @@ async def dump_parts(crud: Annotated[Crud, Depends(Crud.get)]) -> list[Part]:
return await crud.dump_parts()
-@parts_router.get("/your/")
-async def list_your_parts(
+@parts_router.get("/me/")
+async def list_my_parts(
crud: Annotated[Crud, Depends(Crud.get)],
user: Annotated[User, Depends(get_session_user_with_read_permission)],
page: int = Query(description="Page number for pagination"),
search_query: str = Query(None, description="Search query string"),
) -> tuple[list[Part], bool]:
- return await crud.list_your_parts(user.id, page, search_query=search_query)
+ return await crud.list_user_parts(user.id, page, search_query=search_query)
@parts_router.get("/{part_id}")
@@ -97,7 +97,7 @@ async def delete_part(
# TODO: Improve part type annotations.
-@parts_router.post("/edit-part/{part_id}/")
+@parts_router.post("/edit/{part_id}/")
async def edit_part(
part_id: str,
part: dict[str, Any],
diff --git a/store/app/routers/robot.py b/store/app/routers/robot.py
index 089905f7..054f6296 100644
--- a/store/app/routers/robot.py
+++ b/store/app/routers/robot.py
@@ -46,14 +46,14 @@ async def list_robots(
return await crud.list_robots(page, search_query=search_query)
-@robots_router.get("/your/")
-async def list_your_robots(
+@robots_router.get("/me/")
+async def list_my_robots(
crud: Annotated[Crud, Depends(Crud.get)],
user: Annotated[User, Depends(get_session_user_with_read_permission)],
page: int = Query(description="Page number for pagination"),
search_query: str = Query(None, description="Search query string"),
) -> tuple[list[Robot], bool]:
- return await crud.list_your_robots(user.id, page, search_query=search_query)
+ return await crud.list_user_robots(user.id, page, search_query=search_query)
@robots_router.post("/add/")
@@ -96,7 +96,7 @@ async def delete_robot(
return True
-@robots_router.post("/edit-robot/{id}/")
+@robots_router.post("/edit/{id}/")
async def edit_robot(
id: str,
robot: dict[str, Any],
diff --git a/store/app/routers/users.py b/store/app/routers/users.py
index 2528018d..c214ea6d 100644
--- a/store/app/routers/users.py
+++ b/store/app/routers/users.py
@@ -9,7 +9,7 @@
from pydantic.main import BaseModel as PydanticBaseModel
from store.app.db import Crud
-from store.app.model import User, UserPermissions
+from store.app.model import User, UserPermission
from store.app.routers.auth.github import github_auth_router
from store.app.utils.email import send_delete_email
@@ -64,7 +64,7 @@ async def get_session_user_with_read_permission(
api_key_id: Annotated[str, Depends(get_request_api_key_id)],
) -> User:
api_key = await crud.get_api_key(api_key_id)
- if "read" not in api_key.permissions:
+ if api_key.permissions is None or "read" not in api_key.permissions:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied")
return await crud.get_user(api_key.user_id, throw_if_missing=True)
@@ -74,7 +74,7 @@ async def get_session_user_with_write_permission(
api_key_id: Annotated[str, Depends(get_request_api_key_id)],
) -> User:
api_key = await crud.get_api_key(api_key_id)
- if "write" not in api_key.permissions:
+ if api_key.permissions is None or "write" not in api_key.permissions:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied")
return await crud.get_user(api_key.user_id, throw_if_missing=True)
@@ -84,7 +84,7 @@ async def get_session_user_with_admin_permission(
api_key_id: Annotated[str, Depends(get_request_api_key_id)],
) -> User:
api_key = await crud.get_api_key(api_key_id)
- if "admin" not in api_key.permissions:
+ if api_key.permissions is None or "admin" not in api_key.permissions:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied")
return await crud.get_user(api_key.user_id, throw_if_missing=True)
@@ -106,18 +106,17 @@ class UserRegister(BaseModel):
class UserInfoResponse(BaseModel):
- id: str
- permissions: UserPermissions
+ user_id: str
+ permissions: set[UserPermission] | None
@users_router.get("/me", response_model=UserInfoResponse)
async def get_user_info_endpoint(
user: Annotated[User, Depends(get_session_user_with_read_permission)],
- crud: Annotated[Crud, Depends(Crud.get)],
) -> UserInfoResponse | None:
try:
return UserInfoResponse(
- id=user.id,
+ user_id=user.id,
permissions=user.permissions,
)
except ValueError:
diff --git a/tests/test_users.py b/tests/test_users.py
index 311c9d6f..2c3d1578 100644
--- a/tests/test_users.py
+++ b/tests/test_users.py
@@ -22,21 +22,21 @@ async def test_user_auth_functions(app_client: AsyncClient) -> None:
assert response.status_code == 200, response.json()
assert "session_token" in response.cookies
token = response.cookies["session_token"]
-
- user_id = response.json()["id"]
+ assert token == response.json()["api_key_id"]
# Checks that with the session token we get a 200 response.
response = await app_client.get("/users/me")
assert response.status_code == 200, response.json()
- # Check the id of the user we are authenticated as matches the id of the user we created.
- assert response.json()["id"] == user_id
+ user_id = response.json()["user_id"]
# Use the Authorization header instead of the cookie.
response = await app_client.get(
- "/users/me", cookies={"session_token": ""}, headers={"Authorization": f"Bearer {token}"}
+ "/users/me",
+ cookies={"session_token": ""},
+ headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200, response.json()
- assert response.json()["id"] == user_id
+ assert response.json()["user_id"] == user_id
# Log the user out, which deletes the session token.
response = await app_client.delete("/users/logout")
@@ -61,4 +61,4 @@ async def test_user_auth_functions(app_client: AsyncClient) -> None:
# Tries deleting the user again, which should fail.
response = await app_client.delete("/users/me")
assert response.status_code == 400, response.json()
- assert response.json()["detail"] == "Item " + user_id + " not found"
+ assert response.json()["detail"] == "Item not found"