Skip to content
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

Serhii milestone4 #69

Merged
merged 6 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading