Skip to content

Commit

Permalink
Serhii milestone4 (#69)
Browse files Browse the repository at this point in the history
* fix:10.21

* chroe:lint

* fix:clean_up_apis

* add:page_index

* chrome-extension
  • Loading branch information
Serhii Ofii authored Nov 13, 2024
1 parent a5b10d0 commit fe956df
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 4 deletions.
10 changes: 10 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LoadingProvider } from "contexts/LoadingContext";
import { SocketProvider } from "contexts/SocketContext";
import { AlertQueue, AlertQueueProvider } from "hooks/alerts";
import { ThemeProvider } from "hooks/theme";
import APIKeyPage from "pages/Apikey";
import CollectionPage from "pages/Collection";
import Collections from "pages/Collections";
import Home from "pages/Home";
Expand Down Expand Up @@ -71,6 +72,15 @@ const App = () => {
/>
}
/>
<Route
path="/api-key"
element={
<PrivateRoute
element={<APIKeyPage />}
requiredSubscription={true} // Set true if subscription is required for this route
/>
}
/>
<Route path="/login" element={<LoginPage />} />
<Route
path="/subscription_type"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/nav/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Navbar = () => {
const navItems = [
{ name: "My Collections", path: "/collections", isExternal: false },
{ name: "Subscription", path: "/subscription", isExternal: false },
{ name: "API key", path: "/api-key", isExternal: false },
];

return (
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/gen/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api-key/generate": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["generate_api_key"];
put?: never;
/** Login User */
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/user/me": {
parameters: {
query?: never;
Expand Down Expand Up @@ -401,6 +418,9 @@ export interface components {
success: boolean;
error: string;
};
APIkeyResponse: {
api_key: string;
};
/** GithubAuthResponse */
GithubAuthResponse: {
/** Api Key */
Expand Down Expand Up @@ -690,6 +710,7 @@ export interface components {
email: string;
is_subscription: boolean;
is_auth: boolean;
api_key: string;
};
/**
* UserPublic
Expand Down Expand Up @@ -1375,6 +1396,35 @@ export interface operations {
};
};
};
generate_api_key: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["APIkeyResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_signup_token_email_signup_get__id__get: {
parameters: {
query?: never;
Expand Down
102 changes: 102 additions & 0 deletions frontend/src/pages/Apikey.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useAuth } from "contexts/AuthContext";
import { useAlertQueue } from "hooks/alerts";
import React, { useEffect, useState } from "react";

const ApiKeyManager: React.FC = () => {
const [apiKey, setApiKey] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [copied, setCopied] = useState(false);
const { client, auth } = useAuth();
const { addAlert } = useAlertQueue();
// Simulate API call to generate or regenerate a new API key
useEffect(() => {
if (auth?.api_key) setApiKey(auth.api_key);
}, [auth]);
const generateApiKey = async () => {
setIsLoading(true);
setCopied(false); // Reset copy state
try {
// Replace this with your actual API call
const { data, error } = await client.GET("/api-key/generate");
if (error) addAlert(error.detail?.toString(), "error");
else setApiKey(data.api_key);
} catch (error) {
console.error("Error generating API key:", error);
} finally {
setIsLoading(false);
}
};
// Copy the API key to the clipboard
const copyToClipboard = () => {
if (apiKey) {
navigator.clipboard.writeText(apiKey);
setCopied(true);
setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds
}
};

// Mask part of the API key for security
const getMaskedApiKey = (key: string) => {
if (key.length <= 8) return key; // Return as-is if too short to mask
return `${key.slice(0, 14)}..........${key.slice(-4)}`;
};

return (
<div className="flex-column rounded-md min-h-full items-center bg-gray-3 p-3">
<div className="flex flex-col rounded-md items-start p-24 gap-6">
<h1 className="text-3xl text-gray-900">API Key</h1>
<p className="text-gray-11 text-lg">
Please retain the API key, as it&apos;s essential for enabling image
uploads via our browser extension.
</p>
{apiKey ? (
<div className="w-full flex">
<p className="flex-1 text-gray-900 bg-gray-4 p-2 px-4 rounded-lg">
{getMaskedApiKey(apiKey)}
</p>
<button
className={`ml-4 text-sm hover:bg-blue-700 w-16 px-1 ${
copied ? "bg-green-600 hover:bg-green-700" : ""
}`}
onClick={copyToClipboard}
>
{copied ? "Copied!" : "Copy"}
</button>
<button
className={`ml-4 text-sm hover:bg-blue-700 ${
isLoading ? "cursor-not-allowed" : ""
}`}
onClick={generateApiKey}
disabled={isLoading}
>
{isLoading
? "Generating..."
: apiKey
? "Regenerate API Key"
: "Generate API Key"}
</button>
</div>
) : (
<>
<button
className={`text-sm hover:bg-blue-700 ${
isLoading ? "cursor-not-allowed" : ""
}`}
onClick={generateApiKey}
disabled={isLoading}
>
{isLoading
? "Generating..."
: apiKey
? "Regenerate API Key"
: "Generate API Key"}
</button>
<p className="text-gray-11">No API key generated yet.</p>
</>
)}
</div>
</div>
);
};

export default ApiKeyManager;
3 changes: 2 additions & 1 deletion linguaphoto/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from fastapi import APIRouter

from linguaphoto.api import collection, image, subscription, user
from linguaphoto.api import apikey, collection, image, subscription, user

# Create a new API router
router = APIRouter()
Expand All @@ -24,6 +24,7 @@
router.include_router(collection.router, prefix="/collection")
router.include_router(image.router, prefix="/image")
router.include_router(subscription.router, prefix="/subscription")
router.include_router(apikey.router, prefix="/api-key")


# Define a root endpoint that returns a simple message
Expand Down
24 changes: 24 additions & 0 deletions linguaphoto/api/apikey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Collection API."""

from fastapi import APIRouter, Depends
from pydantic import BaseModel

from linguaphoto.crud.user import UserCrud
from linguaphoto.utils.auth import get_current_user_id, subscription_validate

router = APIRouter()


class ApiKeyResponse(BaseModel):
api_key: str


@router.get("/generate", response_model=ApiKeyResponse)
async def generate(
user_id: str = Depends(get_current_user_id),
user_crud: UserCrud = Depends(),
is_subscribed: bool = Depends(subscription_validate),
) -> ApiKeyResponse:
async with user_crud:
new_key = await user_crud.generate_api_key(user_id)
return ApiKeyResponse(api_key=new_key)
12 changes: 11 additions & 1 deletion linguaphoto/api/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
CollectionPublishFragment,
FeaturedImageFragnment,
)
from linguaphoto.utils.auth import get_current_user_id
from linguaphoto.utils.auth import get_current_user_id, get_current_user_id_by_api_key

router = APIRouter()

Expand Down Expand Up @@ -54,6 +54,16 @@ async def getcollections(
return collections


@router.get("/get_all_api_key", response_model=List[Collection])
async def getcollection_api_key(
user_id: str = Depends(get_current_user_id_by_api_key), collection_crud: CollectionCrud = Depends()
) -> List[Collection]:
print(user_id)
async with collection_crud:
collections = await collection_crud.get_collections(user_id=user_id)
return collections


@router.post("/edit")
async def editcollection(
collection: CollectionEditFragment,
Expand Down
24 changes: 23 additions & 1 deletion linguaphoto/api/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
from linguaphoto.models import Image
from linguaphoto.schemas.image import ImageTranslateFragment
from linguaphoto.socket_manager import notify_user
from linguaphoto.utils.auth import get_current_user_id, subscription_validate
from linguaphoto.utils.auth import (
get_current_user_id,
get_current_user_id_by_api_key,
subscription_validate,
subscription_validate_by_api_key,
)

router = APIRouter()
translating_images: List[str] = []
Expand Down Expand Up @@ -42,6 +47,23 @@ async def upload_image(
return image


@router.post("/upload_by_api_key", response_model=Image)
async def upload_image_by_api_key(
file: UploadFile = File(...),
id: Annotated[str, Form()] = "",
user_id: str = Depends(get_current_user_id_by_api_key),
is_subscribed: bool = Depends(subscription_validate_by_api_key),
image_crud: ImageCrud = Depends(),
) -> Image:
"""Upload Image and create new Image."""
async with image_crud:
image = await image_crud.create_image(file, user_id, id)
if image:
# Run translate in the background
asyncio.create_task(translate_background(image.id, image_crud, user_id))
return image


@router.get("/get_all", response_model=List[Image])
async def get_images(collection_id: str, image_crud: ImageCrud = Depends()) -> List[Image]:
async with image_crud:
Expand Down
25 changes: 25 additions & 0 deletions linguaphoto/crud/user.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
"""Defines CRUD interface for user API."""

import random
import string
from typing import List

from linguaphoto.crud.base import BaseCrud
from linguaphoto.models import User
from linguaphoto.schemas.user import UserSigninFragment, UserSignupFragment


def generate_api_key() -> str:
# Generate a random API key (example: sk-abc123def456)
prefix = "lingua-sk-"
key = "".join(random.choices(string.ascii_lowercase + string.digits, k=16))
return f"{prefix}{key}"


class UserCrud(BaseCrud):
async def create_user_from_email(self, user: UserSignupFragment) -> User | None:
duplicated_user = await self._get_items_from_secondary_index("email", user.email, User)
Expand All @@ -23,6 +32,17 @@ async def get_user_by_email(self, email: str) -> User | None:
res = await self._get_items_from_secondary_index("email", email, User)
return res[0]

async def get_user_by_api_key(self, api_key: str) -> User | None:
res = await self._list_items(
item_class=User,
filter_expression="#api_key=:api_key",
expression_attribute_names={"#api_key": "api_key"},
expression_attribute_values={":api_key": api_key},
)
if res:
return res[0]
return None

async def verify_user_by_email(self, user: UserSigninFragment) -> bool:
users: List[User] = await self._get_items_from_secondary_index("email", user.email, User)
# Access the first user in the list and verify the password
Expand All @@ -34,3 +54,8 @@ async def verify_user_by_email(self, user: UserSigninFragment) -> bool:

async def update_user(self, id: str, data: dict) -> None:
await self._update_item(id, User, data)

async def generate_api_key(self, id: str) -> str:
new_key = generate_api_key()
await self._update_item(id, User, {"api_key": new_key})
return new_key
3 changes: 2 additions & 1 deletion linguaphoto/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Models!"""

from typing import List, Self
from typing import List, Optional, Self
from uuid import uuid4

from bcrypt import checkpw, gensalt, hashpw
Expand Down Expand Up @@ -33,6 +33,7 @@ class User(LinguaBaseModel):
email: str
password_hash: str
is_subscription: bool
api_key: Optional[str] = None

@classmethod
def create(cls, user: UserSignupFragment) -> Self:
Expand Down
1 change: 1 addition & 0 deletions linguaphoto/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ class UserSigninRespondFragment(BaseModel):
email: EmailStr
is_subscription: bool
is_auth: bool
api_key: str
Loading

0 comments on commit fe956df

Please sign in to comment.