@ssheleg/sheleg-dev 0.4.0

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/LICENSE +21 -0
  3. package/README.md +132 -0
  4. package/SECURITY.md +63 -0
  5. package/bin/sheleg-dev.js +99 -0
  6. package/package.json +45 -0
  7. package/plugins/sheleg-dev/.claude-plugin/plugin.json +28 -0
  8. package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +442 -0
  9. package/plugins/sheleg-dev/skills/ad-tracking/references/consent-mode.md +257 -0
  10. package/plugins/sheleg-dev/skills/ad-tracking/references/event-tracking.md +201 -0
  11. package/plugins/sheleg-dev/skills/ad-tracking/references/gtag-api.md +389 -0
  12. package/plugins/sheleg-dev/skills/ad-tracking/references/meta-linkedin.md +103 -0
  13. package/plugins/sheleg-dev/skills/ad-tracking/references/performance-security.md +288 -0
  14. package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +384 -0
  15. package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +1357 -0
  16. package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +147 -0
  17. package/plugins/sheleg-dev/skills/frontend-performance/references/accessibility-perf.md +172 -0
  18. package/plugins/sheleg-dev/skills/frontend-performance/references/css-performance.md +180 -0
  19. package/plugins/sheleg-dev/skills/frontend-performance/references/nextjs.md +271 -0
  20. package/plugins/sheleg-dev/skills/google-auth/SKILL.md +275 -0
  21. package/plugins/sheleg-dev/skills/google-auth/references/adc-and-service-accounts.md +464 -0
  22. package/plugins/sheleg-dev/skills/google-auth/references/oauth2-web-server.md +615 -0
  23. package/plugins/sheleg-dev/skills/google-auth/references/sign-in-with-google.md +593 -0
  24. package/plugins/sheleg-dev/skills/google-auth/references/workload-identity.md +376 -0
  25. package/plugins/sheleg-dev/skills/google-signin/SKILL.md +121 -0
  26. package/plugins/sheleg-dev/skills/google-signin/references/full-guide.md +421 -0
  27. package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +458 -0
  28. package/plugins/sheleg-dev/skills/stripe-billing/references/price-integrity.md +201 -0
  29. package/plugins/sheleg-dev/skills/stripe-billing/references/stripe-agent-toolchain.md +173 -0
  30. package/plugins/sheleg-dev/skills/stripe-billing/references/subscription-lifecycle.md +294 -0
  31. package/plugins/sheleg-dev/skills/stripe-billing/references/testing-and-local-dev.md +214 -0
  32. package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +211 -0
@@ -0,0 +1,615 @@
1
+ # OAuth 2.0 Web Server Flow (Node.js & Python)
2
+
3
+
4
+ ## Contents
5
+
6
+ - [Table of Contents](#table-of-contents)
7
+ - [Prerequisites](#prerequisites)
8
+ - [Step 1: Set Authorization Parameters](#step-1-set-authorization-parameters)
9
+ - [Step 2: Redirect to Google](#step-2-redirect-to-google)
10
+ - [Step 3: Handle Callback](#step-3-handle-callback)
11
+ - [Step 4: Use Access Token](#step-4-use-access-token)
12
+ - [Refreshing Tokens](#refreshing-tokens)
13
+ - [Revoking Tokens](#revoking-tokens)
14
+ - [Incremental Authorization](#incremental-authorization)
15
+ - [Error Reference](#error-reference)
16
+ - [Complete Express Example](#complete-express-example)
17
+ - [Complete Flask Example](#complete-flask-example)
18
+ - [Complete FastAPI Example](#complete-fastapi-example)
19
+ - [Scopes Reference](#scopes-reference)
20
+
21
+ ## Table of Contents
22
+ - [Prerequisites](#prerequisites)
23
+ - [Step 1: Set Authorization Parameters](#step-1-set-authorization-parameters)
24
+ - [Step 2: Redirect to Google](#step-2-redirect-to-google)
25
+ - [Step 3: Handle Callback](#step-3-handle-callback)
26
+ - [Step 4: Use Access Token](#step-4-use-access-token)
27
+ - [Refreshing Tokens](#refreshing-tokens)
28
+ - [Revoking Tokens](#revoking-tokens)
29
+ - [Incremental Authorization](#incremental-authorization)
30
+ - [Error Reference](#error-reference)
31
+ - [Complete Express Example](#complete-express-example)
32
+ - [Complete Flask Example](#complete-flask-example)
33
+ - [Complete FastAPI Example](#complete-fastapi-example)
34
+
35
+ ## Prerequisites
36
+
37
+ 1. Enable APIs in [Google Cloud Console](https://console.developers.google.com/apis/library)
38
+ 2. Create OAuth 2.0 Client ID at [Clients page](https://console.developers.google.com/auth/clients):
39
+ - Application type: **Web application**
40
+ - Set authorized redirect URIs
41
+ 3. Download `client_secret.json`
42
+ 4. Install dependencies:
43
+
44
+ **Node.js**
45
+
46
+ ```bash
47
+ npm install googleapis crypto express express-session
48
+ ```
49
+
50
+ **Python**
51
+
52
+ ```bash
53
+ pip install google-auth google-auth-oauthlib google-api-python-client flask
54
+ ```
55
+
56
+ ## Step 1: Set Authorization Parameters
57
+
58
+ ### Node.js
59
+
60
+ ```js
61
+ const {google} = require('googleapis');
62
+ const crypto = require('crypto');
63
+
64
+ const oauth2Client = new google.auth.OAuth2(
65
+ YOUR_CLIENT_ID,
66
+ YOUR_CLIENT_SECRET,
67
+ YOUR_REDIRECT_URL
68
+ );
69
+
70
+ const state = crypto.randomBytes(32).toString('hex');
71
+ req.session.state = state;
72
+
73
+ const authorizationUrl = oauth2Client.generateAuthUrl({
74
+ access_type: 'offline', // 'online' (default) or 'offline' (gets refresh_token)
75
+ scope: [
76
+ 'https://www.googleapis.com/auth/drive.metadata.readonly',
77
+ 'https://www.googleapis.com/auth/calendar.readonly'
78
+ ],
79
+ include_granted_scopes: true, // incremental authorization
80
+ state: state, // CSRF protection
81
+ // prompt: 'consent', // force re-consent (returns new refresh_token)
82
+ // login_hint: 'user@example.com'
83
+ });
84
+ ```
85
+
86
+ ### Python
87
+
88
+ ```python
89
+ from google_auth_oauthlib.flow import Flow
90
+ import secrets
91
+
92
+ flow = Flow.from_client_secrets_file(
93
+ 'client_secret.json',
94
+ scopes=[
95
+ 'https://www.googleapis.com/auth/drive.metadata.readonly',
96
+ 'https://www.googleapis.com/auth/calendar.readonly'
97
+ ],
98
+ redirect_uri=YOUR_REDIRECT_URL
99
+ )
100
+
101
+ state = secrets.token_hex(32)
102
+ session['state'] = state
103
+
104
+ authorization_url, state = flow.authorization_url(
105
+ access_type='offline', # gets refresh_token
106
+ include_granted_scopes='true', # incremental authorization
107
+ state=state, # CSRF protection
108
+ # prompt='consent', # force re-consent
109
+ # login_hint='user@example.com'
110
+ )
111
+ ```
112
+
113
+ ### Authorization Parameters Reference
114
+
115
+ | Parameter | Required | Description |
116
+ |-----------|----------|-------------|
117
+ | `client_id` | Yes | From Cloud Console |
118
+ | `redirect_uri` | Yes | Must match authorized redirect URI exactly |
119
+ | `response_type` | Yes | Set to `code` (handled by library) |
120
+ | `scope` | Yes | Space-delimited or array of scopes |
121
+ | `access_type` | Recommended | `offline` to get refresh_token |
122
+ | `state` | Recommended | Random string for CSRF protection |
123
+ | `include_granted_scopes` | Optional | `true` for incremental authorization |
124
+ | `login_hint` | Optional | Email or `sub` identifier |
125
+ | `prompt` | Optional | `none`, `consent`, or `select_account` |
126
+
127
+ ## Step 2: Redirect to Google
128
+
129
+ ### Node.js
130
+
131
+ ```js
132
+ res.redirect(authorizationUrl);
133
+ ```
134
+
135
+ ### Python (Flask)
136
+
137
+ ```python
138
+ return redirect(authorization_url)
139
+ ```
140
+
141
+ ## Step 3: Handle Callback
142
+
143
+ ### Node.js
144
+
145
+ ```js
146
+ const url = require('url');
147
+
148
+ app.get('/oauth2callback', async (req, res) => {
149
+ const q = url.parse(req.url, true).query;
150
+
151
+ if (q.error) {
152
+ console.log('Error: ' + q.error);
153
+ return res.status(400).send('Authorization failed');
154
+ }
155
+
156
+ if (q.state !== req.session.state) {
157
+ return res.status(403).send('State mismatch. Possible CSRF attack');
158
+ }
159
+
160
+ const {tokens} = await oauth2Client.getToken(q.code);
161
+ oauth2Client.setCredentials(tokens);
162
+
163
+ // tokens.access_token — short-lived access token
164
+ // tokens.refresh_token — long-lived (only on first auth!)
165
+ // tokens.expiry_date — expiration timestamp
166
+ // tokens.scope — granted scopes (check for partial consent)
167
+
168
+ const grantedScopes = tokens.scope.split(' ');
169
+ });
170
+ ```
171
+
172
+ ### Python (Flask)
173
+
174
+ ```python
175
+ @app.route('/oauth2callback')
176
+ def oauth2callback():
177
+ if request.args.get('error'):
178
+ return 'Authorization failed', 400
179
+
180
+ if request.args.get('state') != session.get('state'):
181
+ abort(403, 'State mismatch. Possible CSRF attack')
182
+
183
+ flow = Flow.from_client_secrets_file(
184
+ 'client_secret.json',
185
+ scopes=SCOPES,
186
+ redirect_uri=YOUR_REDIRECT_URL,
187
+ state=session['state']
188
+ )
189
+ flow.fetch_token(authorization_response=request.url)
190
+
191
+ credentials = flow.credentials
192
+ # credentials.token — access token
193
+ # credentials.refresh_token — only on first auth!
194
+ # credentials.expiry — expiration datetime
195
+ # credentials.scopes — granted scopes
196
+
197
+ session['credentials'] = {
198
+ 'token': credentials.token,
199
+ 'refresh_token': credentials.refresh_token,
200
+ 'token_uri': credentials.token_uri,
201
+ 'client_id': credentials.client_id,
202
+ 'client_secret': credentials.client_secret,
203
+ 'scopes': list(credentials.scopes)
204
+ }
205
+ return redirect('/profile')
206
+ ```
207
+
208
+ ### Python (FastAPI)
209
+
210
+ ```python
211
+ from fastapi import APIRouter, Request
212
+ from fastapi.responses import RedirectResponse
213
+ from google_auth_oauthlib.flow import Flow
214
+
215
+ @router.get('/oauth2callback')
216
+ async def oauth2callback(request: Request):
217
+ if request.query_params.get('error'):
218
+ raise HTTPException(400, 'Authorization failed')
219
+
220
+ state = request.session.get('state')
221
+ if request.query_params.get('state') != state:
222
+ raise HTTPException(403, 'State mismatch')
223
+
224
+ flow = Flow.from_client_secrets_file(
225
+ 'client_secret.json',
226
+ scopes=SCOPES,
227
+ redirect_uri=YOUR_REDIRECT_URL,
228
+ state=state
229
+ )
230
+ flow.fetch_token(code=request.query_params.get('code'))
231
+ credentials = flow.credentials
232
+
233
+ # Store credentials in session or database
234
+ request.session['credentials'] = credentials_to_dict(credentials)
235
+ return RedirectResponse('/profile')
236
+ ```
237
+
238
+ ## Step 4: Use Access Token
239
+
240
+ ### Node.js
241
+
242
+ ```js
243
+ // Preferred: Authorization header
244
+ const res = await oauth2Client.fetch('https://www.googleapis.com/drive/v3/files');
245
+
246
+ // Or with googleapis client:
247
+ const drive = google.drive({version: 'v3', auth: oauth2Client});
248
+ const fileList = await drive.files.list();
249
+ ```
250
+
251
+ ### Python
252
+
253
+ ```python
254
+ from google.oauth2.credentials import Credentials
255
+ from googleapiclient.discovery import build
256
+
257
+ credentials = Credentials(**session['credentials'])
258
+ drive = build('drive', 'v3', credentials=credentials)
259
+ file_list = drive.files().list().execute()
260
+ ```
261
+
262
+ ## Refreshing Tokens
263
+
264
+ ### Node.js
265
+
266
+ ```js
267
+ // Auto-refresh: the library handles this if refresh_token is set
268
+ oauth2Client.setCredentials({
269
+ refresh_token: STORED_REFRESH_TOKEN
270
+ });
271
+
272
+ // Manual refresh:
273
+ const {credentials} = await oauth2Client.refreshAccessToken();
274
+
275
+ // Listen for new tokens:
276
+ oauth2Client.on('tokens', (tokens) => {
277
+ if (tokens.refresh_token) {
278
+ // Store in database — only sent once!
279
+ }
280
+ console.log('New access_token:', tokens.access_token);
281
+ });
282
+ ```
283
+
284
+ ### Python
285
+
286
+ ```python
287
+ from google.oauth2.credentials import Credentials
288
+ from google.auth.transport.requests import Request
289
+
290
+ credentials = Credentials(
291
+ token=stored_access_token,
292
+ refresh_token=stored_refresh_token,
293
+ token_uri='https://oauth2.googleapis.com/token',
294
+ client_id=CLIENT_ID,
295
+ client_secret=CLIENT_SECRET
296
+ )
297
+
298
+ # Manual refresh:
299
+ if credentials.expired and credentials.refresh_token:
300
+ credentials.refresh(Request())
301
+ # credentials.token is the new access token
302
+ # credentials.expiry is the new expiration datetime
303
+ # Persist new token to storage
304
+ ```
305
+
306
+ **Note**: `google-api-python-client` (`build(...)`) auto-refreshes credentials before each API call if a refresh_token is available.
307
+
308
+ ## Revoking Tokens
309
+
310
+ ### Node.js
311
+
312
+ ```js
313
+ // Revoke access token:
314
+ await oauth2Client.revokeToken(access_token);
315
+
316
+ // Revoke refresh token:
317
+ await oauth2Client.revokeToken(refresh_token);
318
+
319
+ // Or revoke credentials on the client:
320
+ await oauth2Client.revokeCredentials();
321
+ ```
322
+
323
+ ### Python
324
+
325
+ ```python
326
+ import google.oauth2.credentials
327
+ from google.auth.transport.requests import Request
328
+
329
+ # Revoke via requests library:
330
+ import requests as http_requests
331
+
332
+ http_requests.post(
333
+ 'https://oauth2.googleapis.com/revoke',
334
+ params={'token': credentials.token},
335
+ headers={'content-type': 'application/x-www-form-urlencoded'}
336
+ )
337
+ ```
338
+
339
+ After revocation, redirect user to re-consent if needed.
340
+
341
+ ## Incremental Authorization
342
+
343
+ Request additional scopes later without re-requesting all scopes:
344
+
345
+ ### Node.js
346
+
347
+ ```js
348
+ const newAuthUrl = oauth2Client.generateAuthUrl({
349
+ access_type: 'offline',
350
+ scope: ['https://www.googleapis.com/auth/gmail.readonly'],
351
+ include_granted_scopes: true,
352
+ state: newState
353
+ });
354
+ ```
355
+
356
+ ### Python
357
+
358
+ ```python
359
+ flow = Flow.from_client_secrets_file(
360
+ 'client_secret.json',
361
+ scopes=['https://www.googleapis.com/auth/gmail.readonly'],
362
+ redirect_uri=YOUR_REDIRECT_URL
363
+ )
364
+ authorization_url, state = flow.authorization_url(
365
+ access_type='offline',
366
+ include_granted_scopes='true',
367
+ state=new_state
368
+ )
369
+ ```
370
+
371
+ ## Error Reference
372
+
373
+ | Error | Cause |
374
+ |-------|-------|
375
+ | `access_denied` | User denied consent |
376
+ | `admin_policy_enforced` | Workspace admin blocked scopes |
377
+ | `redirect_uri_mismatch` | redirect_uri doesn't match Cloud Console config |
378
+ | `invalid_client` | Wrong client_secret |
379
+ | `deleted_client` | OAuth client was deleted |
380
+ | `invalid_grant` | Token expired or invalidated; re-authenticate user |
381
+ | `invalid_request` | Missing/malformed parameters |
382
+ | `org_internal` | Project restricts to specific GCP org accounts |
383
+
384
+ ## Complete Express Example
385
+
386
+ ```js
387
+ const express = require('express');
388
+ const session = require('express-session');
389
+ const crypto = require('crypto');
390
+ const {google} = require('googleapis');
391
+
392
+ const app = express();
393
+ app.use(session({secret: 'your-secret', resave: false, saveUninitialized: false}));
394
+
395
+ const oauth2Client = new google.auth.OAuth2(
396
+ process.env.GOOGLE_CLIENT_ID,
397
+ process.env.GOOGLE_CLIENT_SECRET,
398
+ 'http://localhost:3000/oauth2callback'
399
+ );
400
+
401
+ const SCOPES = ['https://www.googleapis.com/auth/userinfo.profile'];
402
+
403
+ app.get('/auth', (req, res) => {
404
+ const state = crypto.randomBytes(32).toString('hex');
405
+ req.session.state = state;
406
+
407
+ const url = oauth2Client.generateAuthUrl({
408
+ access_type: 'offline',
409
+ scope: SCOPES,
410
+ state,
411
+ include_granted_scopes: true
412
+ });
413
+ res.redirect(url);
414
+ });
415
+
416
+ app.get('/oauth2callback', async (req, res) => {
417
+ if (req.query.error) return res.redirect('/error');
418
+ if (req.query.state !== req.session.state) return res.status(403).send('CSRF');
419
+
420
+ const {tokens} = await oauth2Client.getToken(req.query.code);
421
+ req.session.tokens = tokens;
422
+ res.redirect('/profile');
423
+ });
424
+
425
+ app.get('/profile', async (req, res) => {
426
+ if (!req.session.tokens) return res.redirect('/auth');
427
+ oauth2Client.setCredentials(req.session.tokens);
428
+
429
+ const oauth2 = google.oauth2({version: 'v2', auth: oauth2Client});
430
+ const {data} = await oauth2.userinfo.get();
431
+ res.json(data);
432
+ });
433
+
434
+ app.listen(3000);
435
+ ```
436
+
437
+ ## Complete Flask Example
438
+
439
+ ```python
440
+ import os
441
+ import secrets
442
+ from flask import Flask, redirect, request, session, jsonify, abort
443
+ from google_auth_oauthlib.flow import Flow
444
+ from google.oauth2.credentials import Credentials
445
+ from googleapiclient.discovery import build
446
+
447
+ app = Flask(__name__)
448
+ app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'dev-secret')
449
+
450
+ CLIENT_ID = os.environ['GOOGLE_CLIENT_ID']
451
+ CLIENT_SECRET = os.environ['GOOGLE_CLIENT_SECRET']
452
+ REDIRECT_URI = 'http://localhost:5000/oauth2callback'
453
+ SCOPES = ['https://www.googleapis.com/auth/userinfo.profile']
454
+
455
+ @app.route('/auth')
456
+ def auth():
457
+ flow = Flow.from_client_config(
458
+ {
459
+ 'web': {
460
+ 'client_id': CLIENT_ID,
461
+ 'client_secret': CLIENT_SECRET,
462
+ 'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
463
+ 'token_uri': 'https://oauth2.googleapis.com/token',
464
+ }
465
+ },
466
+ scopes=SCOPES,
467
+ redirect_uri=REDIRECT_URI
468
+ )
469
+ state = secrets.token_hex(32)
470
+ session['state'] = state
471
+ authorization_url, _ = flow.authorization_url(
472
+ access_type='offline',
473
+ include_granted_scopes='true',
474
+ state=state
475
+ )
476
+ return redirect(authorization_url)
477
+
478
+
479
+ @app.route('/oauth2callback')
480
+ def oauth2callback():
481
+ if request.args.get('error'):
482
+ return 'Authorization failed', 400
483
+ if request.args.get('state') != session.get('state'):
484
+ abort(403, 'State mismatch')
485
+
486
+ flow = Flow.from_client_config(
487
+ {
488
+ 'web': {
489
+ 'client_id': CLIENT_ID,
490
+ 'client_secret': CLIENT_SECRET,
491
+ 'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
492
+ 'token_uri': 'https://oauth2.googleapis.com/token',
493
+ }
494
+ },
495
+ scopes=SCOPES,
496
+ redirect_uri=REDIRECT_URI,
497
+ state=session['state']
498
+ )
499
+ flow.fetch_token(authorization_response=request.url)
500
+ creds = flow.credentials
501
+ session['credentials'] = {
502
+ 'token': creds.token,
503
+ 'refresh_token': creds.refresh_token,
504
+ 'token_uri': creds.token_uri,
505
+ 'client_id': creds.client_id,
506
+ 'client_secret': creds.client_secret,
507
+ 'scopes': list(creds.scopes or [])
508
+ }
509
+ return redirect('/profile')
510
+
511
+
512
+ @app.route('/profile')
513
+ def profile():
514
+ if 'credentials' not in session:
515
+ return redirect('/auth')
516
+ creds = Credentials(**session['credentials'])
517
+ service = build('oauth2', 'v2', credentials=creds)
518
+ user_info = service.userinfo().get().execute()
519
+ return jsonify(user_info)
520
+
521
+
522
+ if __name__ == '__main__':
523
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # dev only
524
+ app.run(port=5000, debug=True)
525
+ ```
526
+
527
+ ## Complete FastAPI Example
528
+
529
+ ```python
530
+ import os
531
+ import secrets
532
+ from fastapi import FastAPI, Request, HTTPException
533
+ from fastapi.responses import RedirectResponse
534
+ from starlette.middleware.sessions import SessionMiddleware
535
+ from google_auth_oauthlib.flow import Flow
536
+ from google.oauth2.credentials import Credentials
537
+ from googleapiclient.discovery import build
538
+
539
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # dev only
540
+
541
+ app = FastAPI()
542
+ app.add_middleware(SessionMiddleware, secret_key='dev-secret')
543
+
544
+ CLIENT_ID = os.environ['GOOGLE_CLIENT_ID']
545
+ CLIENT_SECRET = os.environ['GOOGLE_CLIENT_SECRET']
546
+ REDIRECT_URI = 'http://localhost:8000/oauth2callback'
547
+ SCOPES = ['https://www.googleapis.com/auth/userinfo.profile']
548
+
549
+ CLIENT_CONFIG = {
550
+ 'web': {
551
+ 'client_id': CLIENT_ID,
552
+ 'client_secret': CLIENT_SECRET,
553
+ 'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
554
+ 'token_uri': 'https://oauth2.googleapis.com/token',
555
+ }
556
+ }
557
+
558
+ @app.get('/auth')
559
+ async def auth(request: Request):
560
+ flow = Flow.from_client_config(CLIENT_CONFIG, scopes=SCOPES, redirect_uri=REDIRECT_URI)
561
+ state = secrets.token_hex(32)
562
+ request.session['state'] = state
563
+ authorization_url, _ = flow.authorization_url(
564
+ access_type='offline',
565
+ include_granted_scopes='true',
566
+ state=state
567
+ )
568
+ return RedirectResponse(authorization_url)
569
+
570
+
571
+ @app.get('/oauth2callback')
572
+ async def oauth2callback(request: Request):
573
+ if request.query_params.get('error'):
574
+ raise HTTPException(400, 'Authorization failed')
575
+ if request.query_params.get('state') != request.session.get('state'):
576
+ raise HTTPException(403, 'State mismatch')
577
+
578
+ flow = Flow.from_client_config(
579
+ CLIENT_CONFIG, scopes=SCOPES, redirect_uri=REDIRECT_URI,
580
+ state=request.session['state']
581
+ )
582
+ flow.fetch_token(code=request.query_params.get('code'))
583
+ creds = flow.credentials
584
+ request.session['credentials'] = {
585
+ 'token': creds.token,
586
+ 'refresh_token': creds.refresh_token,
587
+ 'token_uri': creds.token_uri,
588
+ 'client_id': creds.client_id,
589
+ 'client_secret': creds.client_secret,
590
+ 'scopes': list(creds.scopes or [])
591
+ }
592
+ return RedirectResponse('/profile')
593
+
594
+
595
+ @app.get('/profile')
596
+ async def profile(request: Request):
597
+ if 'credentials' not in request.session:
598
+ return RedirectResponse('/auth')
599
+ creds = Credentials(**request.session['credentials'])
600
+ service = build('oauth2', 'v2', credentials=creds)
601
+ user_info = service.userinfo().get().execute()
602
+ return user_info
603
+ ```
604
+
605
+ ## Scopes Reference
606
+
607
+ Full list: https://developers.google.com/identity/protocols/oauth2/scopes
608
+
609
+ Common scopes:
610
+ - `openid` — OpenID Connect
611
+ - `https://www.googleapis.com/auth/userinfo.email` — User email
612
+ - `https://www.googleapis.com/auth/userinfo.profile` — User profile info
613
+ - `https://www.googleapis.com/auth/drive.readonly` — Read-only Drive
614
+ - `https://www.googleapis.com/auth/calendar.readonly` — Read-only Calendar
615
+ - `https://www.googleapis.com/auth/cloud-platform` — Full GCP access