Skip to content

Commit

Permalink
FastAPI working, need to finish demo
Browse files Browse the repository at this point in the history
  • Loading branch information
trevorpfiz committed May 9, 2024
1 parent e995145 commit eb0b9b1
Show file tree
Hide file tree
Showing 17 changed files with 798 additions and 14 deletions.
30 changes: 21 additions & 9 deletions apps/api/src/api/api_v1/auth/verify.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,57 @@
import logging
from datetime import datetime, timezone

from fastapi import HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from jose import ExpiredSignatureError, JWTError, jwt

from src.config import settings

logging.basicConfig(level=logging.DEBUG)

ALGORITHM = "HS256"
security = HTTPBearer()


def decode_jwt(token: str) -> dict:
def decode_jwt(token: str, secret: str) -> dict:
"""Decode a JWT token and verify its expiration."""
try:
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[ALGORITHM])
exp = payload.get("exp")
logging.info("Attempting to decode the JWT token.")
payload = jwt.decode(
token=token, key=secret, algorithms=[ALGORITHM], audience="authenticated"
)

# Verify that the token has not expired
# Verify expiration time
exp = payload.get("exp")
if exp and datetime.fromtimestamp(exp, tz=timezone.utc) < datetime.now(
tz=timezone.utc
):
logging.warning("Token has expired.")
return None

logging.info("JWT successfully decoded.")
return payload
except JWTError:

except ExpiredSignatureError:
logging.error("JWT has expired.")
return None
except JWTError as e:
logging.error(f"JWT decoding error: {e}")
return None


def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
"""Verify the incoming token using the `decode_jwt` function."""
token = credentials.credentials

credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)

# Decode the JWT token
payload = decode_jwt(token)
payload = decode_jwt(token, settings.JWT_SECRET)
if not payload or "sub" not in payload:
raise credentials_exception

# Return the user's subject or identifier for further checks
return payload["sub"]
2 changes: 2 additions & 0 deletions apps/api/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ class Settings(BaseSettings):
JWT_SECRET: str = os.getenv("JWT_SECRET")
ANTHROPIC_API_KEY: str = os.getenv("ANTHROPIC_API_KEY")
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY")

model_config = SettingsConfigDict(env_file=".env")
openapi_url: str = "/openapi.json"
API_VERSION: str = "/api/v1"
ROOT: str = ROOT_PATH

Expand Down
1 change: 1 addition & 0 deletions apps/api/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def get_application():
generate_unique_id_function=custom_generate_unique_id,
root_path=settings.ROOT,
root_path_in_servers=True,
openapi_url=settings.openapi_url,
)

_app.add_middleware(
Expand Down
13 changes: 9 additions & 4 deletions apps/nextjs/src/app/(app)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { SignOutButton } from "~/components/auth/sign-out-button";
import { api } from "~/trpc/server";
import { createClient } from "~/utils/supabase/server";

export default async function Home() {
const supabase = createClient();
const { data } = await supabase.auth.getUser();
const { data } = await supabase.auth.getSession();

const test = await api.report.test({
token: data?.session?.access_token ?? "",
});

console.log(test);

return (
<main className="">
<h1 className="my-2 text-2xl font-bold">Profile</h1>
<pre className="my-2 rounded-lg bg-secondary p-4">
{JSON.stringify(data.user, null, 2)}
</pre>
<p>{test?.message}</p>
<SignOutButton />
</main>
);
Expand Down
4 changes: 3 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
"generate-client": "openapi-ts --input https://next-fast-turbo.vercel.app/openapi.json --output ./src/lib/api/client --client fetch",
"generate-client:dev": "openapi-ts --input http://127.0.0.1:8000/openapi.json --output ./src/lib/api/client --client fetch"
},
"dependencies": {
"@wellchart/db": "workspace:*",
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";

import type { AppRouter } from "./root";
import { OpenAPI } from "./lib/api/client";
import { appRouter } from "./root";
import { createCallerFactory, createTRPCContext } from "./trpc";

if (process.env.NODE_ENV === "production") {
// FIXME: FastAPI deployment goes here
OpenAPI.BASE = "https://change-this-urlllll.vercel.app";
}

/**
* Create a server-side caller for the tRPC API
* @example
Expand Down
21 changes: 21 additions & 0 deletions packages/api/src/lib/api/client/core/ApiError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';

export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: unknown;
public readonly request: ApiRequestOptions;

constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);

this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}
13 changes: 13 additions & 0 deletions packages/api/src/lib/api/client/core/ApiRequestOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, unknown>;
readonly cookies?: Record<string, unknown>;
readonly headers?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly formData?: Record<string, unknown>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};
7 changes: 7 additions & 0 deletions packages/api/src/lib/api/client/core/ApiResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ApiResult<TData = any> = {
readonly body: TData;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
};
126 changes: 126 additions & 0 deletions packages/api/src/lib/api/client/core/CancelablePromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}

public get isCancelled(): boolean {
return true;
}
}

export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;

(cancelHandler: () => void): void;
}

export class CancelablePromise<T> implements Promise<T> {
private _isResolved: boolean;
private _isRejected: boolean;
private _isCancelled: boolean;
readonly cancelHandlers: (() => void)[];
readonly promise: Promise<T>;
private _resolve?: (value: T | PromiseLike<T>) => void;
private _reject?: (reason?: unknown) => void;

constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: unknown) => void,
onCancel: OnCancel
) => void
) {
this._isResolved = false;
this._isRejected = false;
this._isCancelled = false;
this.cancelHandlers = [];
this.promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;

const onResolve = (value: T | PromiseLike<T>): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isResolved = true;
if (this._resolve) this._resolve(value);
};

const onReject = (reason?: unknown): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isRejected = true;
if (this._reject) this._reject(reason);
};

const onCancel = (cancelHandler: () => void): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this.cancelHandlers.push(cancelHandler);
};

Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this._isResolved,
});

Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this._isRejected,
});

Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this._isCancelled,
});

return executor(onResolve, onReject, onCancel as OnCancel);
});
}

get [Symbol.toStringTag]() {
return "Cancellable Promise";
}

public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onFulfilled, onRejected);
}

public catch<TResult = never>(
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.promise.catch(onRejected);
}

public finally(onFinally?: (() => void) | null): Promise<T> {
return this.promise.finally(onFinally);
}

public cancel(): void {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isCancelled = true;
if (this.cancelHandlers.length) {
try {
for (const cancelHandler of this.cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.cancelHandlers.length = 0;
if (this._reject) this._reject(new CancelError('Request aborted'));
}

public get isCancelled(): boolean {
return this._isCancelled;
}
}
56 changes: 56 additions & 0 deletions packages/api/src/lib/api/client/core/OpenAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ApiRequestOptions } from './ApiRequestOptions';

type Headers = Record<string, string>;
type Middleware<T> = (value: T) => T | Promise<T>;
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;

export class Interceptors<T> {
_fns: Middleware<T>[];

constructor() {
this._fns = [];
}

eject(fn: Middleware<T>) {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
}
}

use(fn: Middleware<T>) {
this._fns = [...this._fns, fn];
}
}

export type OpenAPIConfig = {
BASE: string;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
ENCODE_PATH?: ((path: string) => string) | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
VERSION: string;
WITH_CREDENTIALS: boolean;
interceptors: {
request: Interceptors<RequestInit>;
response: Interceptors<Response>;
};
};

export const OpenAPI: OpenAPIConfig = {
BASE: 'http://127.0.0.1:8000',
CREDENTIALS: 'include',
ENCODE_PATH: undefined,
HEADERS: undefined,
PASSWORD: undefined,
TOKEN: undefined,
USERNAME: undefined,
VERSION: '0.1.0',
WITH_CREDENTIALS: false,
interceptors: {
request: new Interceptors(),
response: new Interceptors(),
},
};
Loading

0 comments on commit eb0b9b1

Please sign in to comment.