Skip to content

Commit 02e4c7a

Browse files
committed
Merge branch 'main' of github.com:Women-in-Computing-at-RIT/WicHacker-Manager into feature/checkIn
� Conflicts: � api/src/server.py
2 parents 5776fff + 810f1ca commit 02e4c7a

12 files changed

Lines changed: 233 additions & 5 deletions

File tree

api/src/controller/discord.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import logging
2+
3+
from flask_restful import Resource
4+
from flask import request
5+
from utils.authentication import authenticate
6+
from data.discord import getHackerDataByDiscordId
7+
8+
logger = logging.getLogger("Discord")
9+
class Discord(Resource):
10+
PATH = '/discord/user/<discord_id>'
11+
12+
def get(self, discord_id):
13+
authenticationPayload = authenticate(request.headers)
14+
if authenticationPayload is None:
15+
return {"message": "Must be logged in"}, 401
16+
if not authenticationPayload['gty'] == 'client-credentials':
17+
# check that the grant type is client-credentials which will exist only for the machine to machine
18+
# auth0 connections using client id and secret, aka: discord bots
19+
logger.error("Non-Bot/Non-Client Request to get discord information")
20+
return {"message": "You shall not pass"}, 403
21+
22+
if discord_id is None:
23+
return {"Message": "Must include discord id"}, 400
24+
# ==========
25+
# Authentication with Bots via API Keys, auth0 ID doesn't mean anything here
26+
# ==========
27+
28+
hackerData = getHackerDataByDiscordId(discord_id)
29+
if hackerData is None:
30+
return {}, 500
31+
return hackerData
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import logging
2+
3+
import requests
4+
from flask_restful import Resource
5+
from flask import request, redirect
6+
from data.users import getUserByAuthID, getUserIdFromAuthID
7+
from utils.authentication import authenticate
8+
from data.discord import saveDiscordId
9+
from urllib.parse import quote
10+
from hashlib import sha256
11+
from utils.aws import getDiscordClientID, getDiscordClientSecret, getRedirectDomain
12+
13+
logger = logging.getLogger("Discord")
14+
15+
16+
def getRedirectUrl():
17+
return getRedirectDomain() + "/discord/callback"
18+
19+
20+
def getUrlSafeRedirectUrl():
21+
url = getRedirectUrl()
22+
return quote(url, safe='')
23+
24+
25+
class DiscordIntegration(Resource):
26+
PATH = '/discord'
27+
28+
def get(self):
29+
userId = request.args.get('id')
30+
if userId is None:
31+
return {}, 400
32+
33+
state = sha256(str(userId).encode("ASCII")).hexdigest()
34+
urlEncodedCallback = getUrlSafeRedirectUrl()
35+
discordAuthorizationURL = f'https://discord.com/oauth2/authorize?response_type=code&client_id={getDiscordClientID()}&scope=identify&state={state}&redirect_uri={urlEncodedCallback}'
36+
return redirect(discordAuthorizationURL, code=302)
37+
38+
def post(self):
39+
authenticationPayload = authenticate(request.headers)
40+
if authenticationPayload is None:
41+
return {"message": "Authorization Header Failure"}, 401
42+
auth0_id = authenticationPayload['sub']
43+
userId = getUserIdFromAuthID(auth0_id)
44+
45+
state = request.args.get('state')
46+
code = request.args.get('code')
47+
if not state == sha256(str(userId).encode("ASCII")).hexdigest():
48+
user = getUserByAuthID(auth0_id)
49+
logger.error("A Hacker is under a CSRF attack. user=%s", user)
50+
return {}, 400
51+
if code is None:
52+
return {}, 400
53+
54+
payload = {
55+
'client_id': getDiscordClientID(),
56+
'client_secret': getDiscordClientSecret(),
57+
'grant_type': 'authorization_code',
58+
'code': code,
59+
'redirect_uri': getRedirectUrl()
60+
}
61+
headers = {
62+
'Content-Type': 'application/x-www-form-urlencoded'
63+
}
64+
response = requests.post('https://discord.com/api/oauth2/token', data=payload, headers=headers)
65+
discordData = response.json()
66+
if not response.status_code == 200:
67+
logger.error("Requesting Token from code Failure: UserId=%s", userId)
68+
return {}, 500
69+
hackerAccessToken = discordData['access_token']
70+
headers = {
71+
'authorization': 'Bearer ' + hackerAccessToken
72+
}
73+
identityResponse = requests.get("https://discord.com/api/users/@me", headers=headers)
74+
if not identityResponse.status_code == 200:
75+
logger.error("Requesting Identity from Discord Failed: UserId=%s", userId)
76+
return {}, 500
77+
userDiscordId = identityResponse.json()['id']
78+
saveSuccessful = saveDiscordId(auth0Id=auth0_id, discordId=userDiscordId)
79+
if saveSuccessful:
80+
return {"Message": "Discord Integration Success"}
81+
return {}, 500

api/src/data/discord.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import logging
2+
3+
from db.db_utils import exec_commit, exec_get_all
4+
5+
logger = logging.getLogger("Discord")
6+
7+
def saveDiscordId(auth0Id, discordId) -> bool:
8+
"""
9+
Save discord id for user by auth0 ID
10+
:param auth0Id:
11+
:param discordId:
12+
:return:
13+
"""
14+
saveSQL = "UPDATE Users set Users.discord_id = %(discordId)s WHERE auth0_id = %(auth0Id)s;"
15+
args = {"discordId": discordId, "auth0Id": auth0Id}
16+
17+
numberOfRowsAffected = exec_commit(saveSQL, args)
18+
if numberOfRowsAffected is None:
19+
return None
20+
if numberOfRowsAffected != 1:
21+
logger.error("Discord Update Affected Multiple Hackers: %s", numberOfRowsAffected)
22+
return False
23+
return True
24+
25+
26+
def getHackerDataByDiscordId(discordId) -> dict:
27+
"""
28+
Retrieve data for discord bots based on user's discordId
29+
:param discordId:
30+
:return: dictionary with user data or None if error, empty dictionary if no user found
31+
"""
32+
selectSQL = "SELECT u.first_name, u.last_name, app.status FROM Users u " \
33+
" INNER JOIN Applications app on u.application_id = app.application_id " \
34+
"WHERE u.discord_id = %(discordId)s;"
35+
args = {"discordId": discordId}
36+
37+
userData = exec_get_all(selectSQL, args)
38+
if userData is None:
39+
return None
40+
elif len(userData) > 1:
41+
logger.error("Multiple Users Found for Discord ID. DiscordID=%s", discordId)
42+
return None
43+
elif len(userData) == 0:
44+
logger.info("No User found for discord id. DiscordID=%s", discordId)
45+
return None
46+
return userData[0]

api/src/db/db_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def exec_get_all(sql, args={}) -> list:
135135
return None
136136

137137

138-
def exec_commit(sql, args={}) -> dict:
138+
def exec_commit(sql, args={}) -> int:
139139
"""
140140
executes sql statement and commits transaction
141141
:param sql:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE Users DROP COLUMN discord_id;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE Users ADD COLUMN discord_id BIGINT;

api/src/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from controller.emailPreset import EmailPreset
1919
from controller.confirmation import Confirmation
2020
from controller.userSearch import UserSearch
21+
from controller.discordIntegration import DiscordIntegration
22+
from controller.discord import Discord
2123
from db.migration import migration
2224
import logging
2325
from dotenv import load_dotenv
@@ -53,6 +55,8 @@
5355
api.add_resource(EmailPreset, EmailPreset.PATH)
5456
api.add_resource(Confirmation, Confirmation.PATH)
5557
api.add_resource(UserSearch, UserSearch.PATH)
58+
api.add_resource(DiscordIntegration, DiscordIntegration.PATH)
59+
api.add_resource(Discord, Discord.PATH)
5660

5761
if not initializeAWSClients():
5862
logger.error("AWS Client Initialization Failure")

api/src/utils/aws.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ def getAuth0ClientID() -> str:
6666
return getSSMSecureParameter('AUTH0_CLIENT_ID')
6767

6868

69+
def getDiscordClientID() -> str:
70+
return getSSMSecureParameter('DISCORD_CLIENT_ID')
71+
72+
73+
def getDiscordClientSecret() -> str:
74+
return getSSMSecureParameter('DISCORD_CLIENT_SECRET')
75+
76+
77+
def getRedirectDomain() -> str:
78+
return getSSMSecureParameter(f'/{environment}/REDIRECT_URL')
79+
80+
6981
def getSSMSecureParameter(parameterName) -> str:
7082
ssmResponse = None
7183
try:

ui/src/components/routes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {ManageApplicationView} from "../pages/manage/manageApplicationView";
1515
import ManageAccommodations from "../pages/manage/manageAccomodations";
1616
import ManageEmails from "../pages/manage/manageEmails";
1717
import {ConfirmUser} from "../pages/hackers/confirmAttendance";
18+
import {DiscordCallback} from "../pages/hackers/discord";
1819

1920
export default function AppRoutes(){
2021
return (
@@ -26,6 +27,7 @@ export default function AppRoutes(){
2627
<Route path="/user/application" element={<ProtectedComponent component={HackerApplicationView} />} />
2728
<Route path="/user/confirm" element={<ProtectedComponent component={ConfirmUser} />} />
2829
<Route path="/auth" element={<LoadingView />} />
30+
<Route path="/discord/callback" element={<ProtectedComponent component={DiscordCallback} />} />
2931
<Route path="/manage" element={<ProtectedComponent component={AdminRoute} permission={CONSOLE} type={READ} children={<HackathonManagerLandingPage />} />} />
3032
<Route path="/manage/applications" element={<ProtectedComponent component={AdminRoute} permission={HACKER_DATA} type={READ} children={<ManageApplications />} />} />
3133
<Route path="/manage/applications/:userId" element={<ProtectedComponent component={AdminRoute} permission={HACKER_DATA} type={READ} children={<ManageApplicationView />} />} />

ui/src/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ import {BrowserRouter} from "react-router-dom";
77

88
const root = ReactDOM.createRoot(document.getElementById('root'));
99
root.render(
10-
<React.StrictMode>
1110
<ErrorBoundry>
1211
<BrowserRouter>
1312
<App />
1413
</BrowserRouter>
1514
</ErrorBoundry>
16-
</React.StrictMode>
1715
);

0 commit comments

Comments
 (0)