SpreadSheets commited on
Commit
d85ed28
·
1 Parent(s): 365e9c1

feat: enhance OAuth handling with Discord and Google integration

Browse files
Files changed (1) hide show
  1. app/__init__.py +145 -19
app/__init__.py CHANGED
@@ -1,15 +1,14 @@
 
 
 
 
1
  from flask_dance.contrib.google import make_google_blueprint
2
- from flask_dance.consumer.storage.sqla import SQLAlchemyStorage
3
- from flask_migrate import Migrate, upgrade
4
- from flask_sqlalchemy import SQLAlchemy
5
- from alembic.command import revision
6
- from flask_login import LoginManager
7
- from alembic.config import Config
8
- from flask import Flask
9
 
10
- db = SQLAlchemy()
11
- migrate = Migrate()
12
- login_manager = LoginManager()
13
 
14
 
15
  def create_app():
@@ -19,19 +18,16 @@ def create_app():
19
  db.init_app(app)
20
  migrate.init_app(app, db)
21
  login_manager.init_app(app)
 
22
 
23
  @login_manager.user_loader
24
  def load_user(user_id):
25
- from .models import User
26
-
27
  return User.query.get(int(user_id))
28
 
29
  from .blueprints.main import main_bp
30
 
31
  app.register_blueprint(main_bp)
32
 
33
- from .models import OAuth
34
-
35
  google_bp = make_google_blueprint(
36
  client_id=app.config["GOOGLE_CLIENT_ID"],
37
  client_secret=app.config["GOOGLE_CLIENT_SECRET"],
@@ -40,13 +36,143 @@ def create_app():
40
  "https://www.googleapis.com/auth/userinfo.email",
41
  "openid",
42
  ],
43
- storage=SQLAlchemyStorage(OAuth, db.session),
44
  )
45
  app.register_blueprint(google_bp, url_prefix="/login")
46
 
47
- with app.app_context():
48
- config = Config("migrations/alembic.ini")
49
- revision(config, autogenerate=True, message="Auto migration")
50
- upgrade()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  return app
 
1
+ from typing import Optional
2
+
3
+ from flask_dance.consumer import oauth_authorized, oauth_error
4
+ from flask_dance.contrib.discord import make_discord_blueprint
5
  from flask_dance.contrib.google import make_google_blueprint
6
+ from flask import Flask, flash, redirect, url_for
7
+ from flask_login import login_user
8
+ import requests
 
 
 
 
9
 
10
+ from .extensions import db, login_manager, migrate
11
+ from .models import OAuth, User
 
12
 
13
 
14
  def create_app():
 
18
  db.init_app(app)
19
  migrate.init_app(app, db)
20
  login_manager.init_app(app)
21
+ login_manager.login_view = "main.login"
22
 
23
  @login_manager.user_loader
24
  def load_user(user_id):
 
 
25
  return User.query.get(int(user_id))
26
 
27
  from .blueprints.main import main_bp
28
 
29
  app.register_blueprint(main_bp)
30
 
 
 
31
  google_bp = make_google_blueprint(
32
  client_id=app.config["GOOGLE_CLIENT_ID"],
33
  client_secret=app.config["GOOGLE_CLIENT_SECRET"],
 
36
  "https://www.googleapis.com/auth/userinfo.email",
37
  "openid",
38
  ],
 
39
  )
40
  app.register_blueprint(google_bp, url_prefix="/login")
41
 
42
+ discord_bp = make_discord_blueprint(
43
+ client_secret=app.config["DISCORD_CLIENT_SECRET"],
44
+ scope=["identify", "email", "guilds.join"],
45
+ client_id=app.config["DISCORD_CLIENT_ID"],
46
+ prompt="consent",
47
+ )
48
+ app.register_blueprint(discord_bp, url_prefix="/login")
49
+
50
+ def _finish_login(
51
+ provider_name: str,
52
+ provider_user_id: str,
53
+ email: Optional[str],
54
+ name: Optional[str],
55
+ token: dict,
56
+ ):
57
+ if not email:
58
+ flash(
59
+ f"{provider_name.title()} Account Must Include A Public Email.", "error"
60
+ )
61
+ return False
62
+
63
+ oauth = OAuth.query.filter_by(
64
+ provider_user_id=provider_user_id,
65
+ provider=provider_name,
66
+ ).one_or_none()
67
+
68
+ if oauth:
69
+ user = oauth.user
70
+ oauth.token = token
71
+ else:
72
+ user = User.query.filter_by(email=email).one_or_none()
73
+
74
+ if not user:
75
+ user = User(name=name or email.split("@")[0], email=email)
76
+ db.session.add(user)
77
+ db.session.flush()
78
+
79
+ oauth = OAuth(
80
+ provider=provider_name,
81
+ provider_user_id=provider_user_id,
82
+ token=token,
83
+ user=user,
84
+ )
85
+
86
+ db.session.add(oauth)
87
+
88
+ db.session.commit()
89
+
90
+ login_user(user)
91
+
92
+ flash(f"Signed In With {provider_name.title()}", "success")
93
+ return redirect(url_for("main.index"))
94
+
95
+ def _join_discord_guild(discord_user_id: str, access_token: str):
96
+ bot_token = app.config.get("DISCORD_BOT_TOKEN")
97
+ guild_id = app.config.get("DISCORD_GUILD_ID")
98
+
99
+ if not guild_id or not bot_token:
100
+ return
101
+
102
+ url = f"https://discord.com/api/v10/guilds/{guild_id}/members/{discord_user_id}"
103
+ headers = {"Authorization": f"Bot {bot_token}"}
104
+ payload = {"access_token": access_token}
105
+
106
+ try:
107
+ response = requests.put(url, headers=headers, json=payload, timeout=10)
108
+
109
+ except requests.RequestException as exc:
110
+ app.logger.warning("Failed To Add User To Discord Guild: %s", exc)
111
+ return
112
+
113
+ if response.status_code not in (200, 201, 204):
114
+ app.logger.warning(
115
+ "Discord Guild Join Failed (%s): %s",
116
+ response.status_code,
117
+ response.text,
118
+ )
119
+
120
+ @oauth_authorized.connect_via(google_bp)
121
+ def google_logged_in(blueprint, token):
122
+ if not token:
123
+ flash("Google Authentication Failed.", "error")
124
+ return False
125
+
126
+ resp = blueprint.session.get("/oauth2/v2/userinfo")
127
+
128
+ if not resp.ok:
129
+ flash("Unable To Read Google Profile Information.", "error")
130
+ return False
131
+
132
+ google_info = resp.json()
133
+ google_user_id = str(google_info["id"])
134
+
135
+ return _finish_login(
136
+ provider_user_id=google_user_id,
137
+ email=google_info.get("email"),
138
+ provider_name=blueprint.name,
139
+ name=google_info.get("name"),
140
+ token=token,
141
+ )
142
+
143
+ @oauth_authorized.connect_via(discord_bp)
144
+ def discord_logged_in(blueprint, token):
145
+ if not token:
146
+ flash("Discord Authentication Failed.", "error")
147
+ return False
148
+
149
+ resp = blueprint.session.get("/api/users/@me")
150
+ if not resp.ok:
151
+ flash("Unable To Read Discord Profile Information.", "error")
152
+ return False
153
+
154
+ discord_info = resp.json()
155
+ discord_user_id = str(discord_info["id"])
156
+
157
+ redirect_response = _finish_login(
158
+ name=discord_info.get("username"),
159
+ provider_user_id=discord_user_id,
160
+ email=discord_info.get("email"),
161
+ provider_name=blueprint.name,
162
+ token=token,
163
+ )
164
+
165
+ if redirect_response and token.get("access_token"):
166
+ _join_discord_guild(discord_user_id, token["access_token"])
167
+
168
+ return redirect_response
169
+
170
+ @oauth_error.connect_via(google_bp)
171
+ @oauth_error.connect_via(discord_bp)
172
+ def oauth_error_handler(blueprint, error, error_description=None, error_uri=None):
173
+ message = f"{blueprint.name.title()} OAuth error: {error_description or error}."
174
+ app.logger.error(message)
175
+
176
+ flash(message, "error")
177
 
178
  return app