Skip to content

Commit 5d4c2b2

Browse files
committed
feat: dynamic oauth buttons rendering
1 parent e74b264 commit 5d4c2b2

8 files changed

Lines changed: 82 additions & 19 deletions

File tree

.env.example

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,29 @@ ENV=dev # dev | prod | demo
33
DATABASE_URL=postgresql+psycopg2://evsy:evsy@db:5432/evsy
44
FRONTEND_URL=http://localhost:3000
55

6+
67
# Frontend
78
VITE_ENV=dev # dev | prod | demo
89
VITE_API_URL=http://localhost:8000/api/v1
910
VITE_LOG_LEVEL=error
10-
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=demo.evsy.dev
11+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=demo.evsy.dev
12+
13+
14+
# Auth
15+
16+
## Secret key for signing JWTs. Use a secure 32+ character string.
17+
SECRET_KEY=YOUR_32_CHAR_SECRET_KEY
18+
19+
## GitHub OAuth credentials
20+
## Create your app here: https://github.com/settings/developers
21+
## Set "Authorization callback URL" to:
22+
## http://localhost:8000/api/v1/auth/oauth/callback
23+
GITHUB_CLIENT_ID=
24+
GITHUB_CLIENT_SECRET=
25+
26+
## Google OAuth credentials
27+
## Create credentials here: https://console.cloud.google.com/apis/credentials
28+
## Set "Authorized redirect URI" to:
29+
## http://localhost:8000/api/v1/auth/oauth/callback
30+
GOOGLE_CLIENT_ID=
31+
GOOGLE_CLIENT_SECRET=

backend/app/modules/auth/router.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
UserLogin,
1919
)
2020
from app.modules.auth.token import create_access_token, get_current_user
21-
from app.settings import Settings
22-
23-
settings = Settings()
21+
from app.settings import get_settings
2422

2523
router = APIRouter()
2624

@@ -102,8 +100,12 @@ def start_oauth_login(
102100
"/oauth/callback", name="oauth_callback", dependencies=[Depends(ensure_not_demo)]
103101
)
104102
def handle_oauth_callback(
105-
code: str = Query(...),
106-
state: str = Query(...),
103+
code: str = Query(...), state: str = Query(...), settings=Depends(get_settings)
107104
):
108105
final_url = f"{settings.frontend_url}/oauth/callback?code={code}&state={state}"
109106
return RedirectResponse(url=final_url)
107+
108+
109+
@router.get("/providers", tags=["auth"])
110+
def list_oauth_providers(settings=Depends(get_settings)):
111+
return {"providers": settings.available_oauth_providers}

backend/app/settings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ def is_prod(self):
4343
def is_demo(self):
4444
return self.env == "demo"
4545

46+
@property
47+
def available_oauth_providers(self) -> list[str]:
48+
providers = []
49+
if self.github_client_id and self.github_client_secret:
50+
providers.append("github")
51+
if self.google_client_id and self.google_client_secret:
52+
providers.append("google")
53+
return providers
54+
4655

4756
@lru_cache()
4857
def get_settings() -> Settings:

backend/tests/conftest.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1+
import bcrypt
12
import pytest
23
from fastapi.testclient import TestClient
34
from sqlalchemy.orm import Session
45

56
from app.core.database import Base, get_db, init_db
67
from app.factory import create_app
7-
from app.settings import Settings
8-
9-
import bcrypt
10-
from app.modules.auth.token import create_access_token
118
from app.modules.auth.crud import create_user
9+
from app.modules.auth.token import create_access_token
10+
from app.settings import Settings
1211

1312
# Load test settings from .env.test
1413
test_settings = Settings(_env_file=".env.test")
@@ -59,13 +58,15 @@ def test_user(override_get_db):
5958
def access_token(test_user):
6059
return create_access_token({"sub": str(test_user.email)})
6160

61+
6262
# FastAPI test client
6363
@pytest.fixture(scope="module")
6464
def client():
6565
with TestClient(app) as c:
6666
yield c
6767

68+
6869
@pytest.fixture
6970
def auth_client(client, access_token):
7071
client.headers.update({"Authorization": f"Bearer {access_token}"})
71-
return client
72+
return client

frontend/src/modules/auth/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ export const loginWithOAuth = (provider: OAuthProvider, code: string) =>
2121
provider,
2222
token: code,
2323
})
24+
25+
export const getAvailableOAuthProviders = () =>
26+
api.get<{ providers: OAuthProvider[] }>('/auth/providers')

frontend/src/modules/auth/components/LoginForm.vue

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,13 @@ const onSubmit = handleSubmit(values => {
4242
<form @submit="onSubmit" class="mt-2 space-y-6">
4343
<div class="grid gap-6">
4444
<!-- OAuth buttons -->
45-
<div v-if="!isDemo" class="flex flex-col gap-4">
46-
<OauthButton class="flex-1" provider="github" />
47-
<OauthButton class="flex-1" provider="google" />
45+
<div v-if="!isDemo && auth.availableProviders.length" class="flex flex-col gap-4">
46+
<OauthButton
47+
v-for="provider in auth.availableProviders"
48+
:key="provider"
49+
class="flex-1"
50+
:provider="provider"
51+
/>
4852
</div>
4953

5054
<!-- Divider -->

frontend/src/modules/auth/components/SignupForm.vue

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,13 @@ const onSubmit = handleSubmit(values => {
4545
<form @submit="onSubmit" class="mt-2 space-y-6">
4646
<div class="grid gap-6">
4747
<!-- OAuth Buttons -->
48-
<div v-if="!isDemo" class="flex flex-col gap-4">
49-
<OauthButton class="flex-1" provider="github" />
50-
<OauthButton class="flex-1" provider="google" />
48+
<div v-if="!isDemo && auth.availableProviders.length" class="flex flex-col gap-4">
49+
<OauthButton
50+
v-for="provider in auth.availableProviders"
51+
:key="provider"
52+
class="flex-1"
53+
:provider="provider"
54+
/>
5155
</div>
5256

5357
<!-- Divider -->

frontend/src/modules/auth/stores/useAuthStore.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { defineStore } from 'pinia'
2-
import { ref, watch } from 'vue'
3-
import { getUser, loginWithEmail, signupWithEmail, loginWithOAuth } from '../api'
2+
import { ref, watch, onMounted } from 'vue'
3+
import {
4+
getUser,
5+
loginWithEmail,
6+
signupWithEmail,
7+
loginWithOAuth,
8+
getAvailableOAuthProviders,
9+
} from '../api'
410
import type { OAuthProvider } from '../types'
511

612
export const useAuthStore = defineStore('auth', () => {
713
const token = ref<string | null>(localStorage.getItem('token'))
814
const email = ref<string | null>(localStorage.getItem('email'))
15+
const availableProviders = ref<OAuthProvider[]>([])
916

1017
watch(token, newToken => {
1118
if (newToken) {
@@ -23,6 +30,17 @@ export const useAuthStore = defineStore('auth', () => {
2330
}
2431
})
2532

33+
const fetchOAuthProviders = async () => {
34+
try {
35+
const response = await getAvailableOAuthProviders()
36+
availableProviders.value = response.data.providers
37+
} catch {
38+
availableProviders.value = []
39+
}
40+
}
41+
42+
onMounted(fetchOAuthProviders)
43+
2644
const login = async (emailInput: string, password: string) => {
2745
const response = await loginWithEmail({ email: emailInput, password })
2846
token.value = response.data.access_token
@@ -54,6 +72,7 @@ export const useAuthStore = defineStore('auth', () => {
5472
return {
5573
token,
5674
email,
75+
availableProviders,
5776
login,
5877
signup,
5978
loginWithOAuthCode,

0 commit comments

Comments
 (0)