reflex-google-auth 0.1.1__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/.github/workflows/publish.yml +3 -5
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/PKG-INFO +17 -2
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/README.md +16 -1
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/custom_components/reflex_google_auth/google_auth.py +18 -1
- reflex_google_auth-0.2.0/custom_components/reflex_google_auth/state.py +206 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/custom_components/reflex_google_auth.egg-info/PKG-INFO +17 -2
- reflex_google_auth-0.2.0/google_auth_demo/google_auth_demo/google_auth_demo.py +263 -0
- reflex_google_auth-0.2.0/google_auth_demo/requirements.txt +3 -0
- reflex_google_auth-0.1.1/custom_components/reflex_google_auth/state.py +0 -115
- reflex_google_auth-0.1.1/google_auth_demo/google_auth_demo/google_auth_demo.py +0 -83
- reflex_google_auth-0.1.1/google_auth_demo/requirements.txt +0 -2
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/.github/workflows/pre-commit.yaml +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/.gitignore +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/.pre-commit-config.yaml +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/custom_components/reflex_google_auth/__init__.py +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/custom_components/reflex_google_auth/decorator.py +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/custom_components/reflex_google_auth.egg-info/SOURCES.txt +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/custom_components/reflex_google_auth.egg-info/dependency_links.txt +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/custom_components/reflex_google_auth.egg-info/requires.txt +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/custom_components/reflex_google_auth.egg-info/top_level.txt +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/google_auth_demo/.gitignore +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/google_auth_demo/assets/favicon.ico +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/google_auth_demo/google_auth_demo/__init__.py +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/google_auth_demo/rxconfig.py +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/pyproject.toml +0 -0
- {reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/setup.cfg +0 -0
@@ -15,15 +15,13 @@ jobs:
|
|
15
15
|
HAS_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN != '' }}
|
16
16
|
steps:
|
17
17
|
- uses: actions/checkout@master
|
18
|
-
- name:
|
19
|
-
uses:
|
18
|
+
- name: Install the latest version of uv
|
19
|
+
uses: astral-sh/setup-uv@v6
|
20
20
|
with:
|
21
21
|
python-version: "3.12"
|
22
|
-
- name: Install package
|
23
|
-
run: pip install .
|
24
22
|
- name: Publish to PyPI
|
25
23
|
if: ${{ env.HAS_PYPI_TOKEN == 'true' }}
|
26
|
-
run:
|
24
|
+
run: uv build && uv publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}
|
27
25
|
deploy:
|
28
26
|
name: Deploy Demo App to Reflex Cloud
|
29
27
|
runs-on: ubuntu-latest
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: reflex-google-auth
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: Sign in with Google
|
5
5
|
Author-email: Masen Furer <m_github@0x26.net>
|
6
6
|
License: Apache-2.0
|
@@ -127,3 +127,18 @@ def custom_button() -> rx.Component:
|
|
127
127
|
f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
|
128
128
|
)
|
129
129
|
```
|
130
|
+
|
131
|
+
### Requesting Scopes
|
132
|
+
|
133
|
+
By default, only the basic profile scopes are requested. To request additional
|
134
|
+
scopes for accessing other Google APIs in the context of the authenticated user:
|
135
|
+
|
136
|
+
1. Update your app registration in the Google Cloud Console to add the additional scopes
|
137
|
+
you want to request.
|
138
|
+
2. Pass the `scope` parameter to `handle_google_login` event handler. NOTE:
|
139
|
+
scopes can only be requested with the `auth-code` flow, so you must set
|
140
|
+
`GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables as
|
141
|
+
described above.
|
142
|
+
|
143
|
+
See the `custom_scope` example in `google_auth_demo.py` for how to request
|
144
|
+
Drive scopes and and then use Google Drive API to store and retrieve app data.
|
@@ -109,4 +109,19 @@ def custom_button() -> rx.Component:
|
|
109
109
|
return rx.vstack(
|
110
110
|
f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
|
111
111
|
)
|
112
|
-
```
|
112
|
+
```
|
113
|
+
|
114
|
+
### Requesting Scopes
|
115
|
+
|
116
|
+
By default, only the basic profile scopes are requested. To request additional
|
117
|
+
scopes for accessing other Google APIs in the context of the authenticated user:
|
118
|
+
|
119
|
+
1. Update your app registration in the Google Cloud Console to add the additional scopes
|
120
|
+
you want to request.
|
121
|
+
2. Pass the `scope` parameter to `handle_google_login` event handler. NOTE:
|
122
|
+
scopes can only be requested with the `auth-code` flow, so you must set
|
123
|
+
`GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables as
|
124
|
+
described above.
|
125
|
+
|
126
|
+
See the `custom_scope` example in `google_auth_demo.py` for how to request
|
127
|
+
Drive scopes and and then use Google Drive API to store and retrieve app data.
|
@@ -43,8 +43,18 @@ google_login = GoogleLogin.create
|
|
43
43
|
|
44
44
|
|
45
45
|
def handle_google_login(
|
46
|
+
scope: str | list[str] | rx.Var[str] | rx.Var[list[str]] = "openid profile email",
|
46
47
|
on_success: EventType[dict] = GoogleAuthState.on_success,
|
47
48
|
) -> rx.Var[rx.EventChain]:
|
49
|
+
"""Create a login event chain to handle Google login.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
scope: The space-separated OAuth scopes to request (default "openid profile email").
|
53
|
+
on_success: The event to call on successful login (default GoogleAuthState.on_success).
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
An event chain that handles the login process.
|
57
|
+
"""
|
48
58
|
on_success_event_chain = rx.Var.create(
|
49
59
|
rx.EventChain.create(
|
50
60
|
value=on_success, # type: ignore
|
@@ -52,6 +62,9 @@ def handle_google_login(
|
|
52
62
|
key="on_success",
|
53
63
|
)
|
54
64
|
)
|
65
|
+
scope = rx.Var.create(scope) if not isinstance(scope, rx.Var) else scope
|
66
|
+
if isinstance(scope, rx.vars.ArrayVar):
|
67
|
+
scope = scope.join(" ")
|
55
68
|
return rx.Var(
|
56
69
|
"() => login()",
|
57
70
|
_var_type=rx.EventChain,
|
@@ -61,8 +74,12 @@ def handle_google_login(
|
|
61
74
|
const login = useGoogleLogin({
|
62
75
|
onSuccess: %s,
|
63
76
|
flow: 'auth-code',
|
77
|
+
scope: %s,
|
64
78
|
});"""
|
65
|
-
% on_success_event_chain:
|
79
|
+
% (on_success_event_chain, scope): rx.vars.VarData.merge(
|
80
|
+
on_success_event_chain._get_all_var_data(),
|
81
|
+
scope._get_all_var_data(),
|
82
|
+
),
|
66
83
|
},
|
67
84
|
imports={LIBRARY: "useGoogleLogin"},
|
68
85
|
),
|
@@ -0,0 +1,206 @@
|
|
1
|
+
"""Handle Google Auth."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import time
|
6
|
+
from typing import TypedDict
|
7
|
+
|
8
|
+
import reflex as rx
|
9
|
+
from google.auth.transport import requests
|
10
|
+
from google.oauth2.id_token import verify_oauth2_token
|
11
|
+
from httpx import AsyncClient
|
12
|
+
|
13
|
+
TOKEN_URI = "https://oauth2.googleapis.com/token"
|
14
|
+
CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
|
15
|
+
CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "")
|
16
|
+
REDIRECT_URI = os.environ.get("GOOGLE_REDIRECT_URI", "")
|
17
|
+
|
18
|
+
|
19
|
+
def set_client_id(client_id: str):
|
20
|
+
"""Set the client id."""
|
21
|
+
global CLIENT_ID
|
22
|
+
CLIENT_ID = client_id
|
23
|
+
|
24
|
+
|
25
|
+
class TokenResponse(TypedDict, total=False):
|
26
|
+
access_token: str
|
27
|
+
expires_in: int
|
28
|
+
refresh_token: str
|
29
|
+
scope: str
|
30
|
+
token_type: str
|
31
|
+
id_token: str
|
32
|
+
|
33
|
+
|
34
|
+
class TokenCredential(TypedDict, total=False):
|
35
|
+
iss: str
|
36
|
+
azp: str
|
37
|
+
aud: str
|
38
|
+
sub: str
|
39
|
+
hd: str
|
40
|
+
email: str
|
41
|
+
email_verified: bool
|
42
|
+
at_hash: str
|
43
|
+
nbf: int
|
44
|
+
name: str
|
45
|
+
picture: str
|
46
|
+
given_name: str
|
47
|
+
family_name: str
|
48
|
+
iat: int
|
49
|
+
exp: int
|
50
|
+
jti: str
|
51
|
+
|
52
|
+
|
53
|
+
async def get_token(auth_code) -> TokenResponse:
|
54
|
+
"""Get the token(s) from an auth code.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
auth_code: Returned from an 'auth-code' flow.
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
The token response, containing access_token, refresh_token and id_token.
|
61
|
+
"""
|
62
|
+
async with AsyncClient() as client:
|
63
|
+
response = await client.post(
|
64
|
+
TOKEN_URI,
|
65
|
+
data={
|
66
|
+
"code": auth_code,
|
67
|
+
"client_id": CLIENT_ID,
|
68
|
+
"client_secret": CLIENT_SECRET,
|
69
|
+
"redirect_uri": REDIRECT_URI,
|
70
|
+
"grant_type": "authorization_code",
|
71
|
+
},
|
72
|
+
)
|
73
|
+
response.raise_for_status()
|
74
|
+
response_data = response.json()
|
75
|
+
return TokenResponse(response_data)
|
76
|
+
|
77
|
+
|
78
|
+
async def get_id_token(auth_code: str) -> str:
|
79
|
+
token_data = await get_token(auth_code)
|
80
|
+
if "id_token" not in token_data:
|
81
|
+
raise ValueError("No id_token in token response")
|
82
|
+
return token_data["id_token"]
|
83
|
+
|
84
|
+
|
85
|
+
class GoogleAuthState(rx.State):
|
86
|
+
token_response_json: str = rx.LocalStorage()
|
87
|
+
refresh_token: str = rx.LocalStorage()
|
88
|
+
|
89
|
+
@rx.var
|
90
|
+
def id_token_json(self) -> str:
|
91
|
+
"""For compatibility only. Use token_response_json instead."""
|
92
|
+
try:
|
93
|
+
return json.dumps(
|
94
|
+
{"credential": json.loads(self.token_response_json).get("id_token", "")}
|
95
|
+
)
|
96
|
+
except Exception:
|
97
|
+
return ""
|
98
|
+
|
99
|
+
@rx.event
|
100
|
+
async def on_success(self, response: dict):
|
101
|
+
if "code" in response:
|
102
|
+
# Handle auth-code flow
|
103
|
+
token_response = await get_token(response["code"])
|
104
|
+
self.token_response_json = json.dumps(token_response)
|
105
|
+
if "refresh_token" in token_response:
|
106
|
+
self.refresh_token = token_response["refresh_token"]
|
107
|
+
elif "credential" in response:
|
108
|
+
# Handle id-token flow
|
109
|
+
self.token_response_json = json.dumps({"id_token": response["credential"]})
|
110
|
+
self.refresh_token = ""
|
111
|
+
else:
|
112
|
+
self.token_response_json = ""
|
113
|
+
self.refresh_token = ""
|
114
|
+
raise ValueError("No code or credential in response")
|
115
|
+
|
116
|
+
@rx.event
|
117
|
+
async def refresh_access_token(self):
|
118
|
+
try:
|
119
|
+
if not self.access_token:
|
120
|
+
return # no token to refresh
|
121
|
+
if not self.refresh_token:
|
122
|
+
# token not available, must re-auth
|
123
|
+
self.token_response_json = ""
|
124
|
+
return
|
125
|
+
async with AsyncClient() as client:
|
126
|
+
response = await client.post(
|
127
|
+
TOKEN_URI,
|
128
|
+
data={
|
129
|
+
"client_id": CLIENT_ID,
|
130
|
+
"client_secret": CLIENT_SECRET,
|
131
|
+
"refresh_token": self.refresh_token,
|
132
|
+
"grant_type": "refresh_token",
|
133
|
+
},
|
134
|
+
)
|
135
|
+
response.raise_for_status()
|
136
|
+
new_token_data = response.json()
|
137
|
+
# Save the new refresh token if provided
|
138
|
+
if "refresh_token" in new_token_data:
|
139
|
+
self.refresh_token = new_token_data["refresh_token"]
|
140
|
+
self.token_response_json = json.dumps(new_token_data)
|
141
|
+
except Exception as exc:
|
142
|
+
print(f"Error refreshing token: {exc!r}") # noqa: T201
|
143
|
+
|
144
|
+
@rx.var(cache=True)
|
145
|
+
def client_id(self) -> str:
|
146
|
+
return CLIENT_ID or os.environ.get("GOOGLE_CLIENT_ID", "")
|
147
|
+
|
148
|
+
@rx.var
|
149
|
+
def scopes(self) -> list[str]:
|
150
|
+
try:
|
151
|
+
scope_str = json.loads(self.token_response_json).get("scope", "")
|
152
|
+
return scope_str.split(" ") if scope_str else []
|
153
|
+
except Exception:
|
154
|
+
return []
|
155
|
+
|
156
|
+
@rx.var
|
157
|
+
def access_token(self) -> str:
|
158
|
+
try:
|
159
|
+
return json.loads(self.token_response_json).get("access_token", "")
|
160
|
+
except Exception:
|
161
|
+
return ""
|
162
|
+
|
163
|
+
@rx.var
|
164
|
+
def id_token(self) -> str:
|
165
|
+
try:
|
166
|
+
return json.loads(self.token_response_json).get("id_token", "")
|
167
|
+
except Exception:
|
168
|
+
return ""
|
169
|
+
|
170
|
+
@rx.var(cache=True)
|
171
|
+
def tokeninfo(self) -> TokenCredential:
|
172
|
+
try:
|
173
|
+
return TokenCredential(
|
174
|
+
verify_oauth2_token(
|
175
|
+
self.id_token,
|
176
|
+
requests.Request(),
|
177
|
+
self.client_id,
|
178
|
+
)
|
179
|
+
)
|
180
|
+
except Exception as exc:
|
181
|
+
if self.token_response_json:
|
182
|
+
print(f"Error verifying token: {exc!r}") # noqa: T201
|
183
|
+
self.token_response_json = ""
|
184
|
+
return {}
|
185
|
+
|
186
|
+
@rx.event
|
187
|
+
def logout(self):
|
188
|
+
self.token_response_json = ""
|
189
|
+
self.refresh_token = ""
|
190
|
+
|
191
|
+
@rx.var(cache=False)
|
192
|
+
def token_is_valid(self) -> bool:
|
193
|
+
try:
|
194
|
+
return bool(
|
195
|
+
self.tokeninfo and int(self.tokeninfo.get("exp", 0)) > time.time()
|
196
|
+
)
|
197
|
+
except Exception:
|
198
|
+
return False
|
199
|
+
|
200
|
+
@rx.var(cache=True)
|
201
|
+
def user_name(self) -> str:
|
202
|
+
return self.tokeninfo.get("name", "")
|
203
|
+
|
204
|
+
@rx.var(cache=True)
|
205
|
+
def user_email(self) -> str:
|
206
|
+
return self.tokeninfo.get("email", "")
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: reflex-google-auth
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: Sign in with Google
|
5
5
|
Author-email: Masen Furer <m_github@0x26.net>
|
6
6
|
License: Apache-2.0
|
@@ -127,3 +127,18 @@ def custom_button() -> rx.Component:
|
|
127
127
|
f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
|
128
128
|
)
|
129
129
|
```
|
130
|
+
|
131
|
+
### Requesting Scopes
|
132
|
+
|
133
|
+
By default, only the basic profile scopes are requested. To request additional
|
134
|
+
scopes for accessing other Google APIs in the context of the authenticated user:
|
135
|
+
|
136
|
+
1. Update your app registration in the Google Cloud Console to add the additional scopes
|
137
|
+
you want to request.
|
138
|
+
2. Pass the `scope` parameter to `handle_google_login` event handler. NOTE:
|
139
|
+
scopes can only be requested with the `auth-code` flow, so you must set
|
140
|
+
`GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables as
|
141
|
+
described above.
|
142
|
+
|
143
|
+
See the `custom_scope` example in `google_auth_demo.py` for how to request
|
144
|
+
Drive scopes and and then use Google Drive API to store and retrieve app data.
|
@@ -0,0 +1,263 @@
|
|
1
|
+
import io
|
2
|
+
import json
|
3
|
+
|
4
|
+
import google.oauth2.credentials
|
5
|
+
import reflex as rx
|
6
|
+
from googleapiclient.discovery import build
|
7
|
+
from googleapiclient.errors import HttpError
|
8
|
+
from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
|
9
|
+
from reflex_google_auth import (
|
10
|
+
GoogleAuthState,
|
11
|
+
handle_google_login,
|
12
|
+
require_google_login,
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
class State(GoogleAuthState):
|
17
|
+
@rx.var(cache=True)
|
18
|
+
def protected_content(self) -> str:
|
19
|
+
if self.token_is_valid:
|
20
|
+
return f"This content can only be viewed by a logged in User. Nice to see you {self.tokeninfo.get('name')}"
|
21
|
+
return "Not logged in."
|
22
|
+
|
23
|
+
|
24
|
+
def user_info(tokeninfo: rx.vars.ObjectVar) -> rx.Component:
|
25
|
+
return rx.hstack(
|
26
|
+
rx.avatar(
|
27
|
+
src=tokeninfo["picture"],
|
28
|
+
fallback=tokeninfo["name"],
|
29
|
+
size="5",
|
30
|
+
),
|
31
|
+
rx.vstack(
|
32
|
+
rx.heading(tokeninfo["name"], size="6"),
|
33
|
+
rx.text(tokeninfo["email"]),
|
34
|
+
align_items="flex-start",
|
35
|
+
),
|
36
|
+
rx.button("Logout", on_click=GoogleAuthState.logout),
|
37
|
+
padding="10px",
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
def index():
|
42
|
+
return rx.vstack(
|
43
|
+
rx.heading("Google OAuth", size="8"),
|
44
|
+
rx.link("Protected Page", href="/protected"),
|
45
|
+
rx.link("Partially Protected Page", href="/partially-protected"),
|
46
|
+
rx.link("Custom Login Button", href="/custom-button"),
|
47
|
+
rx.link("Custom Scope", href="/custom-scope"),
|
48
|
+
align="center",
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
@rx.page(route="/protected")
|
53
|
+
@require_google_login
|
54
|
+
def protected() -> rx.Component:
|
55
|
+
return rx.vstack(
|
56
|
+
user_info(GoogleAuthState.tokeninfo),
|
57
|
+
rx.text(State.protected_content),
|
58
|
+
rx.link("Home", href="/"),
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
@require_google_login
|
63
|
+
def user_name_or_sign_in() -> rx.Component:
|
64
|
+
return rx.heading(GoogleAuthState.tokeninfo["name"], size="6")
|
65
|
+
|
66
|
+
|
67
|
+
@rx.page(route="/partially-protected")
|
68
|
+
def partially_protected() -> rx.Component:
|
69
|
+
return rx.vstack(
|
70
|
+
rx.heading("This page is partially protected."),
|
71
|
+
rx.text(
|
72
|
+
"If you are signed in with google, you should see your name below, otherwise "
|
73
|
+
"you will see a sign in button",
|
74
|
+
),
|
75
|
+
user_name_or_sign_in(),
|
76
|
+
)
|
77
|
+
|
78
|
+
|
79
|
+
@rx.page(route="/custom-button")
|
80
|
+
@require_google_login(
|
81
|
+
button=rx.button("Google Login 🚀", on_click=handle_google_login())
|
82
|
+
)
|
83
|
+
def custom_button() -> rx.Component:
|
84
|
+
return rx.vstack(
|
85
|
+
user_info(GoogleAuthState.tokeninfo),
|
86
|
+
"You clicked a custom button to login, nice",
|
87
|
+
)
|
88
|
+
|
89
|
+
|
90
|
+
class DriveState(rx.State):
|
91
|
+
app_data: str = ""
|
92
|
+
loading: bool = False
|
93
|
+
|
94
|
+
async def _get_config_json_metadata(self) -> dict[str, str] | None:
|
95
|
+
google_auth_state = await self.get_state(GoogleAuthState)
|
96
|
+
if not google_auth_state.token_is_valid:
|
97
|
+
return None
|
98
|
+
credentials = google.oauth2.credentials.Credentials(
|
99
|
+
google_auth_state.access_token,
|
100
|
+
refresh_token=google_auth_state.refresh_token,
|
101
|
+
)
|
102
|
+
service = build("drive", "v3", credentials=credentials)
|
103
|
+
results = await rx.run_in_thread(
|
104
|
+
service.files()
|
105
|
+
.list(
|
106
|
+
spaces="appDataFolder",
|
107
|
+
fields="nextPageToken, files(id, name)",
|
108
|
+
pageSize=10,
|
109
|
+
)
|
110
|
+
.execute
|
111
|
+
)
|
112
|
+
items = results.get("files", [])
|
113
|
+
if not items:
|
114
|
+
print("No files found.") # noqa: T201
|
115
|
+
return None
|
116
|
+
return items[0]
|
117
|
+
|
118
|
+
async def _save_file_to_drive(self, content: str):
|
119
|
+
google_auth_state = await self.get_state(GoogleAuthState)
|
120
|
+
if not google_auth_state.token_is_valid:
|
121
|
+
return
|
122
|
+
credentials = google.oauth2.credentials.Credentials(
|
123
|
+
google_auth_state.access_token,
|
124
|
+
refresh_token=google_auth_state.refresh_token,
|
125
|
+
)
|
126
|
+
payload = {
|
127
|
+
"content": content,
|
128
|
+
}
|
129
|
+
service = build("drive", "v3", credentials=credentials)
|
130
|
+
upload_content = MediaIoBaseUpload(
|
131
|
+
io.BytesIO(json.dumps(payload).encode("utf-8")),
|
132
|
+
mimetype="application/json",
|
133
|
+
resumable=True,
|
134
|
+
)
|
135
|
+
|
136
|
+
existing_file = await self._get_config_json_metadata()
|
137
|
+
if existing_file:
|
138
|
+
file = await rx.run_in_thread(
|
139
|
+
service.files()
|
140
|
+
.update(
|
141
|
+
fileId=existing_file["id"], media_body=upload_content, fields="id"
|
142
|
+
)
|
143
|
+
.execute
|
144
|
+
)
|
145
|
+
else:
|
146
|
+
file_metadata = {
|
147
|
+
"name": "config.json",
|
148
|
+
"parents": ["appDataFolder"],
|
149
|
+
}
|
150
|
+
file = await rx.run_in_thread(
|
151
|
+
service.files()
|
152
|
+
.create(body=file_metadata, media_body=upload_content, fields="id")
|
153
|
+
.execute
|
154
|
+
)
|
155
|
+
print(f"File ID: {file.get('id')}") # noqa: T201
|
156
|
+
|
157
|
+
async def _load_file_from_drive(self) -> str:
|
158
|
+
google_auth_state = await self.get_state(GoogleAuthState)
|
159
|
+
if not google_auth_state.token_is_valid:
|
160
|
+
return ""
|
161
|
+
credentials = google.oauth2.credentials.Credentials(
|
162
|
+
google_auth_state.access_token,
|
163
|
+
refresh_token=google_auth_state.refresh_token,
|
164
|
+
)
|
165
|
+
service = build("drive", "v3", credentials=credentials)
|
166
|
+
existing_file = await self._get_config_json_metadata()
|
167
|
+
if not existing_file:
|
168
|
+
return ""
|
169
|
+
try:
|
170
|
+
# pylint: disable=maybe-no-member
|
171
|
+
request = await rx.run_in_thread(
|
172
|
+
lambda: service.files().get_media(fileId=existing_file["id"])
|
173
|
+
)
|
174
|
+
file = io.BytesIO()
|
175
|
+
downloader = MediaIoBaseDownload(file, request)
|
176
|
+
done = False
|
177
|
+
while done is False:
|
178
|
+
status, done = await rx.run_in_thread(downloader.next_chunk)
|
179
|
+
except HttpError as error:
|
180
|
+
print(f"An error occurred: {error}") # noqa: T201
|
181
|
+
file = None
|
182
|
+
if file:
|
183
|
+
file.seek(0)
|
184
|
+
try:
|
185
|
+
payload = json.loads(file.read().decode("utf-8"))
|
186
|
+
return payload.get("content", "")
|
187
|
+
except Exception as exc:
|
188
|
+
print(f"Error reading config file: {exc!r}") # noqa: T201
|
189
|
+
return ""
|
190
|
+
|
191
|
+
@rx.event
|
192
|
+
async def set_app_data(self, value: str):
|
193
|
+
self.app_data = value
|
194
|
+
self.loading = True
|
195
|
+
yield
|
196
|
+
try:
|
197
|
+
await self._save_file_to_drive(value)
|
198
|
+
finally:
|
199
|
+
self.loading = False
|
200
|
+
|
201
|
+
@rx.event
|
202
|
+
async def on_load(self):
|
203
|
+
self.loading = True
|
204
|
+
yield
|
205
|
+
try:
|
206
|
+
self.app_data = await self._load_file_from_drive()
|
207
|
+
finally:
|
208
|
+
self.loading = False
|
209
|
+
|
210
|
+
|
211
|
+
@rx.page(route="/custom-scope", on_load=DriveState.on_load)
|
212
|
+
@require_google_login(
|
213
|
+
button=rx.button(
|
214
|
+
"Login with Drive API scope",
|
215
|
+
on_click=handle_google_login(
|
216
|
+
scope=(
|
217
|
+
"https://www.googleapis.com/auth/drive.appdata "
|
218
|
+
"https://www.googleapis.com/auth/drive.file "
|
219
|
+
"https://www.googleapis.com/auth/drive.install "
|
220
|
+
),
|
221
|
+
),
|
222
|
+
)
|
223
|
+
)
|
224
|
+
def custom_scope() -> rx.Component:
|
225
|
+
return rx.vstack(
|
226
|
+
user_info(GoogleAuthState.tokeninfo),
|
227
|
+
rx.vstack(
|
228
|
+
rx.heading("App Data Stored in Drive"),
|
229
|
+
rx.hstack(
|
230
|
+
rx.text_area(
|
231
|
+
default_value=DriveState.app_data,
|
232
|
+
key=DriveState.app_data,
|
233
|
+
on_blur=DriveState.set_app_data,
|
234
|
+
),
|
235
|
+
rx.cond(
|
236
|
+
DriveState.loading,
|
237
|
+
rx.spinner(),
|
238
|
+
rx.button("Refresh", on_click=DriveState.on_load),
|
239
|
+
),
|
240
|
+
),
|
241
|
+
rx.heading("Authorized Scopes"),
|
242
|
+
rx.foreach(
|
243
|
+
GoogleAuthState.scopes,
|
244
|
+
lambda scope: rx.code(scope),
|
245
|
+
),
|
246
|
+
rx.heading("Raw Access Token"),
|
247
|
+
rx.code(GoogleAuthState.access_token),
|
248
|
+
rx.button("Refresh AT", on_click=GoogleAuthState.refresh_access_token),
|
249
|
+
rx.hstack(
|
250
|
+
"Time until token expiry",
|
251
|
+
rx.moment(
|
252
|
+
GoogleAuthState.tokeninfo.exp,
|
253
|
+
unix=True,
|
254
|
+
duration_from_now=True,
|
255
|
+
interval=1000,
|
256
|
+
),
|
257
|
+
),
|
258
|
+
),
|
259
|
+
)
|
260
|
+
|
261
|
+
|
262
|
+
app = rx.App()
|
263
|
+
app.add_page(index)
|
@@ -1,115 +0,0 @@
|
|
1
|
-
"""Handle Google Auth."""
|
2
|
-
|
3
|
-
import json
|
4
|
-
import os
|
5
|
-
import time
|
6
|
-
from typing import TypedDict
|
7
|
-
|
8
|
-
import reflex as rx
|
9
|
-
from google.auth.transport import requests
|
10
|
-
from google.oauth2.id_token import verify_oauth2_token
|
11
|
-
from httpx import AsyncClient
|
12
|
-
|
13
|
-
TOKEN_URI = "https://oauth2.googleapis.com/token"
|
14
|
-
CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
|
15
|
-
CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "")
|
16
|
-
REDIRECT_URI = os.environ.get("GOOGLE_REDIRECT_URI", "")
|
17
|
-
|
18
|
-
|
19
|
-
def set_client_id(client_id: str):
|
20
|
-
"""Set the client id."""
|
21
|
-
global CLIENT_ID
|
22
|
-
CLIENT_ID = client_id
|
23
|
-
|
24
|
-
|
25
|
-
class TokenCredential(TypedDict, total=False):
|
26
|
-
iss: str
|
27
|
-
azp: str
|
28
|
-
aud: str
|
29
|
-
sub: str
|
30
|
-
hd: str
|
31
|
-
email: str
|
32
|
-
email_verified: bool
|
33
|
-
nbf: int
|
34
|
-
name: str
|
35
|
-
picture: str
|
36
|
-
given_name: str
|
37
|
-
family_name: str
|
38
|
-
iat: int
|
39
|
-
exp: int
|
40
|
-
jti: str
|
41
|
-
|
42
|
-
|
43
|
-
async def get_id_token(auth_code) -> str:
|
44
|
-
"""Get the id token credential from an auth code.
|
45
|
-
|
46
|
-
Args:
|
47
|
-
auth_code: Returned from an 'auth-code' flow.
|
48
|
-
|
49
|
-
Returns:
|
50
|
-
The id token credential.
|
51
|
-
"""
|
52
|
-
async with AsyncClient() as client:
|
53
|
-
response = await client.post(
|
54
|
-
TOKEN_URI,
|
55
|
-
data={
|
56
|
-
"code": auth_code,
|
57
|
-
"client_id": CLIENT_ID,
|
58
|
-
"client_secret": CLIENT_SECRET,
|
59
|
-
"redirect_uri": REDIRECT_URI,
|
60
|
-
"grant_type": "authorization_code",
|
61
|
-
},
|
62
|
-
)
|
63
|
-
response.raise_for_status()
|
64
|
-
response_data = response.json()
|
65
|
-
return response_data.get("id_token")
|
66
|
-
|
67
|
-
|
68
|
-
class GoogleAuthState(rx.State):
|
69
|
-
id_token_json: str = rx.LocalStorage()
|
70
|
-
|
71
|
-
@rx.event
|
72
|
-
async def on_success(self, id_token: dict):
|
73
|
-
if "code" in id_token:
|
74
|
-
# Handle auth-code flow
|
75
|
-
id_token["credential"] = await get_id_token(id_token["code"])
|
76
|
-
self.id_token_json = json.dumps(id_token)
|
77
|
-
|
78
|
-
@rx.var(cache=True)
|
79
|
-
def client_id(self) -> str:
|
80
|
-
return CLIENT_ID or os.environ.get("GOOGLE_CLIENT_ID", "")
|
81
|
-
|
82
|
-
@rx.var(cache=True)
|
83
|
-
def tokeninfo(self) -> TokenCredential:
|
84
|
-
try:
|
85
|
-
return verify_oauth2_token(
|
86
|
-
json.loads(self.id_token_json)["credential"],
|
87
|
-
requests.Request(),
|
88
|
-
self.client_id,
|
89
|
-
)
|
90
|
-
except Exception as exc:
|
91
|
-
if self.id_token_json:
|
92
|
-
print(f"Error verifying token: {exc!r}") # noqa: T201
|
93
|
-
self.id_token_json = ""
|
94
|
-
return {}
|
95
|
-
|
96
|
-
@rx.event
|
97
|
-
def logout(self):
|
98
|
-
self.id_token_json = ""
|
99
|
-
|
100
|
-
@rx.var(cache=False)
|
101
|
-
def token_is_valid(self) -> bool:
|
102
|
-
try:
|
103
|
-
return bool(
|
104
|
-
self.tokeninfo and int(self.tokeninfo.get("exp", 0)) > time.time()
|
105
|
-
)
|
106
|
-
except Exception:
|
107
|
-
return False
|
108
|
-
|
109
|
-
@rx.var(cache=True)
|
110
|
-
def user_name(self) -> str:
|
111
|
-
return self.tokeninfo.get("name", "")
|
112
|
-
|
113
|
-
@rx.var(cache=True)
|
114
|
-
def user_email(self) -> str:
|
115
|
-
return self.tokeninfo.get("email", "")
|
@@ -1,83 +0,0 @@
|
|
1
|
-
import reflex as rx
|
2
|
-
from reflex_google_auth import (
|
3
|
-
GoogleAuthState,
|
4
|
-
handle_google_login,
|
5
|
-
require_google_login,
|
6
|
-
)
|
7
|
-
|
8
|
-
|
9
|
-
class State(GoogleAuthState):
|
10
|
-
@rx.var(cache=True)
|
11
|
-
def protected_content(self) -> str:
|
12
|
-
if self.token_is_valid:
|
13
|
-
return f"This content can only be viewed by a logged in User. Nice to see you {self.tokeninfo.get('name')}"
|
14
|
-
return "Not logged in."
|
15
|
-
|
16
|
-
|
17
|
-
def user_info(tokeninfo: rx.vars.ObjectVar) -> rx.Component:
|
18
|
-
return rx.hstack(
|
19
|
-
rx.avatar(
|
20
|
-
src=tokeninfo["picture"],
|
21
|
-
fallback=tokeninfo["name"],
|
22
|
-
size="5",
|
23
|
-
),
|
24
|
-
rx.vstack(
|
25
|
-
rx.heading(tokeninfo["name"], size="6"),
|
26
|
-
rx.text(tokeninfo["email"]),
|
27
|
-
align_items="flex-start",
|
28
|
-
),
|
29
|
-
rx.button("Logout", on_click=GoogleAuthState.logout),
|
30
|
-
padding="10px",
|
31
|
-
)
|
32
|
-
|
33
|
-
|
34
|
-
def index():
|
35
|
-
return rx.vstack(
|
36
|
-
rx.heading("Google OAuth", size="8"),
|
37
|
-
rx.link("Protected Page", href="/protected"),
|
38
|
-
rx.link("Partially Protected Page", href="/partially-protected"),
|
39
|
-
rx.link("Custom Login Button", href="/custom-button"),
|
40
|
-
align="center",
|
41
|
-
)
|
42
|
-
|
43
|
-
|
44
|
-
@rx.page(route="/protected")
|
45
|
-
@require_google_login
|
46
|
-
def protected() -> rx.Component:
|
47
|
-
return rx.vstack(
|
48
|
-
user_info(GoogleAuthState.tokeninfo),
|
49
|
-
rx.text(State.protected_content),
|
50
|
-
rx.link("Home", href="/"),
|
51
|
-
)
|
52
|
-
|
53
|
-
|
54
|
-
@require_google_login
|
55
|
-
def user_name_or_sign_in() -> rx.Component:
|
56
|
-
return rx.heading(GoogleAuthState.tokeninfo["name"], size="6")
|
57
|
-
|
58
|
-
|
59
|
-
@rx.page(route="/partially-protected")
|
60
|
-
def partially_protected() -> rx.Component:
|
61
|
-
return rx.vstack(
|
62
|
-
rx.heading("This page is partially protected."),
|
63
|
-
rx.text(
|
64
|
-
"If you are signed in with google, you should see your name below, otherwise "
|
65
|
-
"you will see a sign in button",
|
66
|
-
),
|
67
|
-
user_name_or_sign_in(),
|
68
|
-
)
|
69
|
-
|
70
|
-
|
71
|
-
@rx.page(route="/custom-button")
|
72
|
-
@require_google_login(
|
73
|
-
button=rx.button("Google Login 🚀", on_click=handle_google_login())
|
74
|
-
)
|
75
|
-
def custom_button() -> rx.Component:
|
76
|
-
return rx.vstack(
|
77
|
-
user_info(GoogleAuthState.tokeninfo),
|
78
|
-
"You clicked a custom button to login, nice",
|
79
|
-
)
|
80
|
-
|
81
|
-
|
82
|
-
app = rx.App()
|
83
|
-
app.add_page(index)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{reflex_google_auth-0.1.1 → reflex_google_auth-0.2.0}/google_auth_demo/google_auth_demo/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|