I just set up a self hosted Discourse forum that integrates with my Anvil community management app.
Discourse
Lots of info out there on how to set up a self hosted Discourse forum. I went with the Digital Ocean 1-click app. Guide here: https://www.youtube.com/watch?v=UU-o2Zsq3Ag
Discourse SSO
Configuring this was remarkably easy. I just needed to create a secret (generated in Anvil using the secrets service), and point it to my SSO endpoint in my Anvil app.
Anvil API Endpoint
This was a bit tricky for me to figure out, but after a lot of tinkering I finally got it. Code below.
@anvil.server.http_endpoint('/login-sso', cross_site_session=True)
def login_sso(sso, sig):
secret_key = anvil.secrets.get_secret('discourse_secret')
# Verify the signature
expected_sig = hmac.new(secret_key.encode(), msg=sso.encode(), digestmod=hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_sig, sig):
raise anvil.server.HttpError(403, "Signature mismatch")
# Decode the payload
payload = base64.b64decode(urllib.parse.unquote(sso)).decode()
params = dict(urllib.parse.parse_qsl(payload))
nonce = params['nonce']
user = None
if not user:
user = anvil.users.get_user(allow_remembered=True)
if not user or user['auth_forumchat'] != True:
return "User not logged in or does not have access to forum."
discourse_url = app_tables.forum.get(tenant=user['tenant'])['discourse_url']
# Prepare the return payload with user info
user_info = {
'nonce': nonce,
'email': user['email'],
'external_id': user.get_id(),
'username': user['first_name'] + '_' + user['last_name'],
'name': user['first_name'] + ' ' + user['last_name']
}
print(user_info)
# unsigned payload generated
return_payload = '&'.join([f"{key}={value}" for key, value in user_info.items()])
# Base64-encode and URL-encode the return payload
b64_return_payload = base64.b64encode(return_payload.encode()).decode()
print('b64 return payload')
url_encoded_payload = urllib.parse.quote_plus(b64_return_payload)
print('url encoded payload')
# Sign the return payload
return_sig = hmac.new(secret_key.encode(), msg=b64_return_payload.encode(), digestmod=hashlib.sha256).hexdigest()
print('return sig')
# Redirect back to Discourse
discourse_redirect_url = f"https://{discourse_url}/session/sso_login?sso={url_encoded_payload}&sig={return_sig}"
return anvil.server.HttpResponse(302, headers={"Location": discourse_redirect_url})
Some bells and whistles
I added a nav button to the top of my Anvil app to open the forum in the same tab:
With this event handler:
def link_forum_nav_click(self, **event_args):
"""This method is called when the link is clicked"""
anvil.js.window.location.href = Global.forumlink
And in Discourse, I added a top nav button to go back to the Anvil app by editing the Header HTML:
Conclusion
This saved me a LOT of time as I was considering re-creating forum functionality in my community app. I’m glad I didn’t have to do that, and I’m grateful that Discourse is open source and so well supported with plugins and such.
Open Source
By the way, my Anvil app is open source: