All the implementations of this documentation is done on Ubuntu 22.04 LTS ## Discourse server setup **[Before you start](#before-you-start)** 1. [Preparing your domain name](#1-preparing-your-domain-name) 2. [Setting up email](#2-setting-up-email) **[Installation](#installation)** 3. [Create new cloud server](#3-create-new-cloud-server) 4. [Access new cloud server](#4-access-your-cloud-server) 5. [Install Discourse](#5-install-discourse) 6. [Edit Discourse configuration](#6-edit-discourse-configuration) 7. [Start Discourse](#7-start-discourse) 8. [Register new account and become admin](#8-register-new-account-and-become-admin) 9. [Post-install maintenance](#9-post-install-maintenance) 10. [(Optional) Add more Discourse features](#10-optional-add-more-discourse-features) ## Before you start ### 1. Preparing your domain name > 🔔 Discourse will not work from an IP address, you must own a domain name such as `example.com` to proceed. - Already own a domain name? Great. Select a subdomain such as `discourse.example.com` or `talk.example.com` or `forum.example.com` for your Discourse instance. - No domain name? Get one! We can [recommend NameCheap](https://www.namecheap.com/domains/domain-name-search/), or there are many other [great domain name registrars](https://www.google.com/search?q=best+domain+name+registrars) to choose from. - Your DNS controls should be accessible from the place where you purchased your domain name. This is where you will create a DNS [ `A` record](https://support.dnsimple.com/articles/a-record/) for the `discourse.example.com` hostname once you know the IP address of the cloud server where you are installing Discourse, as well as enter your [SPF and DKIM records](https://www.google.com/search?q=what+is+spf+dkim) for your email. ### 2. Setting Up Email > ⚠️ **Email is CRITICAL for account creation and notifications in Discourse.** If you do not properly configure email before bootstrapping YOU WILL HAVE A BROKEN SITE! > 💡 Email here refers to [Transactional Email](https://www.google.com/search?q=what+is+transactional+email) not the usual email service like Gmail, Outlook and/or Yahoo. - No existing mail server? Check out our [**Recommended Email Providers for Discourse**][mailconfig]. - Already have a mail server? Great. Use your existing mail server credentials. (Free email services like Gmail/Outlook/Yahoo do not support transactional emails.) - To ensure mail deliverability, you must add valid [SPF and DKIM records](https://www.google.com/search?q=what+is+spf+dkim) in your DNS. See your mail provider instructions for specifics. - If you're having trouble getting emails to work, follow our [Email Troubleshooting Guide](https://meta.discourse.org/t/troubleshooting-email-on-a-new-discourse-install/16326) ## Installation ### 3. Create New Cloud Server Create your new cloud server, for example [on DigitalOcean][do]: - The default of **the current supported LTS release of Ubuntu Server** works fine. At minimum, a 64-bit Linux OS with a modern kernel version is required. - The default of **1 GB** RAM works fine for small Discourse communities. We recommend 2 GB RAM for larger communities. - The default of **New York** is a good choice for most US and European audiences. Or select a region that is geographically closer to your audience. - Enter your domain `discourse.example.com` as the Droplet name. Create your new Droplet. You may receive an email with the root password, however, [you should set up SSH keys](https://www.google.com/search?q=digitalocean+ssh+keys), as they are more secure. > ⚠️ Now you have created your cloud server! Go back to your DNS controls and use the IP address to set up an `A record` for your `discourse.example.com` hostname. ### 4. Access Your Cloud Server Connect to your server via its IP address using SSH, or [Putty][put] on Windows: ssh root@192.168.1.1 Either use the root password from the email DigitalOcean sent you when the server was set up, or have a valid SSH key configured on your local machine. ### 5. Install Discourse Clone the [Official Discourse Docker Image][dd] into `/var/discourse`. sudo -s git clone https://github.com/discourse/discourse_docker.git /var/discourse cd /var/discourse chmod 700 containers You will need to be root through the rest of the setup and bootstrap process. ### 6. Edit Discourse Configuration Launch the setup tool at ./discourse-setup Answer the following questions when prompted: Hostname for your Discourse? [discourse.example.com]: Email address for admin account(s)? [me@example.com,you@example.com]: SMTP server address? [smtp.example.com]: SMTP port? [587]: SMTP user name? [user@example.com]: SMTP password? [pa$$word]: Let's Encrypt account email? (ENTER to skip) [me@example.com]: Optional Maxmind License key () [xxxxxxxxxxxxxxxx]: You'll get the SMTP details from your [email](#email) setup, be sure to complete that section. Let's Encrypt account setup is to give you a free HTTPS certificate for your site, be sure to set that up if you want your site secure. This will generate an `app.yml` configuration file on your behalf, and then kicks off bootstrap. Bootstrapping takes between **2-8 minutes** to set up your Discourse. If you need to change these settings after bootstrapping, you can run `./discourse-setup` again (it will re-use your previous values from the file) or edit `/containers/app.yml` manually with `nano` and then `./launcher rebuild app`, otherwise your changes will not take effect. ### 7. Start Discourse Once bootstrapping is complete, your Discourse should be accessible in your web browser via the domain name `discourse.example.com` you entered earlier. <img src="https://www.discourse.org/images/install/17/discourse-congrats.png" width="650"> ### 8. Register New Account and Become Admin Register a new admin account using one of the email addresses you entered before bootstrapping. <img src="https://www.discourse.org/images/install/17/discourse-register.png" width="650"> <img src="https://www.discourse.org/images/install/17/discourse-activate.png" width="650"> (If you are unable to register your admin account, check the logs at `/var/discourse/shared/standalone/log/rails/production.log` and see our [Email Troubleshooting checklist](https://meta.discourse.org/t/troubleshooting-email-on-a-new-discourse-install/16326).) After registering your admin account, the setup wizard will launch and guide you through basic configuration of your Discourse. <img src="https://www.discourse.org/images/install/17/discourse-wizard-step-1.png" width="650"> After completing the setup wizard, you should see Staff topics and **READ ME FIRST: Admin Quick Start Guide**. This guide contains advice for further configuring and customizing your Discourse install. <img src="https://www.discourse.org/images/install/17/discourse-homepage.png"> ### 9. Post-Install Maintenance - We strongly suggest you turn on automatic security updates for your OS. In Ubuntu use the `dpkg-reconfigure -plow unattended-upgrades` command. In CentOS/RHEL, use the [`yum-cron`](https://www.redhat.com/sysadmin/using-yum-cron) package. - If you are using a password and not a SSH key, be sure to enforce a strong root password. In Ubuntu use the `apt install libpam-cracklib` package. We also recommend `fail2ban` which blocks any IP addresses for 10 minutes that attempt more than 3 password retries. - **Ubuntu**: `apt install fail2ban` - **CentOS/RHEL**: `sudo dnf install fail2ban` - If you need or want a default firewall, [turn on ufw](https://meta.discourse.org/t/configure-a-firewall-for-discourse/20584) for Ubuntu or use `firewalld` for CentOS/RHEL. > 💡 Discourse will send you an email notification when new versions of Discourse are released. Please stay current to get the latest features and security fixes. To **upgrade Discourse to the latest version**, visit `https://discourse.example.com/admin/upgrade` in your browser and click the Upgrade button. Alternatively, you can ssh into your server and rebuild using: ``` cd /var/discourse git pull ./launcher rebuild app ``` The `launcher` command in the `/var/discourse` folder can be used for various kinds of maintenance: ```text Usage: launcher COMMAND CONFIG [--skip-prereqs] [--docker-args STRING] Commands: start: Start/initialize a container stop: Stop a running container restart: Restart a container destroy: Stop and remove a container enter: Use nsenter to get a shell into a container logs: View the Docker logs for a container bootstrap: Bootstrap a container for the config based on a template rebuild: Rebuild a container (destroy old, bootstrap, start new) cleanup: Remove all containers that have stopped for > 24 hours Options: --skip-prereqs Don't check launcher prerequisites --docker-args Extra arguments to pass when running docker ``` ### 10. (Optional) Add More Discourse Features Do you want... - Users to log in _only_ via your pre-existing website's registration system? [Configure Single-Sign-On](https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045). * Users to log in via [Google](https://meta.discourse.org/t/configuring-google-oauth2-login-for-discourse/15858), [Twitter](https://meta.discourse.org/t/configuring-twitter-login-for-discourse/13395), [GitHub](https://meta.discourse.org/t/configuring-github-login-for-discourse/13745), or [Facebook](https://meta.discourse.org/t/configuring-facebook-login-for-discourse/13394)? * Users to post replies via email? [Configure reply via email](https://meta.discourse.org/t/set-up-reply-via-email-support/14003). * Automatic daily backups? [Configure backups](https://meta.discourse.org/t/configure-automatic-backups-for-discourse/14855). * Free HTTPS / SSL support? [Configure Let's Encrypt](https://meta.discourse.org/t/setting-up-lets-encrypt-cert-with-discourse-docker/40709). Paid HTTPS / SSL support? [Configure SSL](https://meta.discourse.org/t/allowing-ssl-for-your-discourse-docker-setup/13847). * Use a plugin [from Discourse](https://github.com/discourse) or a third party? [Configure plugins](https://meta.discourse.org/t/install-a-plugin/19157) * Multiple Discourse sites on the same server? [Configure multisite](https://meta.discourse.org/t/multisite-configuration-with-docker/14084). * Webhooks when events happen in Discourse? [Configure webhooks](https://meta.discourse.org/t/setting-up-webhooks/49045). * A Content Delivery Network to speed up worldwide access? [Configure a CDN](https://meta.discourse.org/t/enable-a-cdn-for-your-discourse/14857). We recommend [Fastly](http://www.fastly.com/). * Import/migrate old content from vBulletin, PHPbb, Vanilla, Drupal, BBPress, etc? [See our open source importers](https://github.com/discourse/discourse/tree/main/script/import_scripts) and our [migration guide](https://meta.discourse.org/t/how-to-migrate-from-one-platform-forum-to-discourse/197236). * A user friendly [offline page when rebuilding or upgrading?](https://meta.discourse.org/t/adding-an-offline-page-when-rebuilding/45238) * To embed Discourse [in your WordPress install](https://github.com/discourse/wp-discourse), or [on your static HTML site](https://meta.discourse.org/t/embedding-discourse-comments-via-javascript/31963)? Help us improve this guide! Feel free to ask about it on [meta.discourse.org][meta], or even better, submit a pull request. [dd]: https://github.com/discourse/discourse_docker [ssh]: https://help.github.com/articles/generating-ssh-keys [meta]: https://meta.discourse.org [do]: https://www.digitalocean.com/?refcode=5fa48ac82415 [put]: http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html [mailconfig]: https://github.com/discourse/discourse/blob/main/docs/INSTALL-email.md ## Install protonmail bridge We will need root access for the setup ``` su root ``` Import the protonmail bridge public key ``` nano bridge_pubkey.gpg (copy the content of the file and save) or the public key bridge_pubkey.gpg can found:https://protonmail.com/download/bridge_pubkey.gpg rpm --import bridge_pubkey.gpg ``` Download the protonmail-bridge package ``` cd /tmp wget --no-check-certificate https://protonmail.com/download/bridge/protonmail-bridge_2.1.3-1_amd64.deb ``` Check the rpm package is correctly signed ``` rpm --checksig protonmail-bridge_2.1.3-1_amd64.deb ``` Install dependencies for protonmail-bridge ``` apt install qt5-default libqt5designer5 libqt5multimediawidgets5 libqt5quickwidgets5 libpulse-mainloop-glib0 libsecret-1-0 ttf-dejavu net-tools libsecret-common libsecret-tools ``` Install the protonmail bridge client ``` dpkg -i protonmail-bridge_2.1.3-1_amd64.deb ``` ## Install additional tools required for the setup Install the "pass" password manager that protonmail bridge will use to store the passwords ``` apt install pass ``` Install the "screen" utility to daemonize the protonmail bridge client ``` apt install screen ``` ## Create a new user We will create a new user mainly to isolate the access to the passwords of other users. Notice that the new user will be locked to disable access to this user from outside. ``` useradd protonmail usermod -L protonmail ``` Create a protonmail directory in /home ``` cd /home mkdir protonmail ``` Change folder owner ``` chown -R protonmail:protonmail /home/protonmail ``` ## Setup "pass" password manager Login as the new isolated user ``` su protonmail cd ~ ``` Run a script session to avoid the PGP key passphrase prompt to fail (https://bugzilla.redhat.com/show_bug.cgi?id=659512). This is required if we are not using a graphical interface due to the way our isolated user runs the shell commands ``` script /dev/null ``` Generate PGP key pair for the new user with an empty passphrase. The empty passphrase is required to run the protonmail bridge on the background on system startup without being prompted for the password and hence causing the process to fail. ``` gpg --full-generate-key >>>> Choose 1 (1) RSA and RSA (default) >>>> Choose 2048 (default) >>>> Choose 0 0 = key does not expire >>>> Type your name e.g. Proty McProtonFace >>>> Type your email e.g. a@a.com >>>> Leave empty comment >>>> Leave empty passphrase ``` List the keys to ensure they were created correctly ``` gpg --list-keys ``` Init the password manager for the chosen email address in the PGP keys step ``` pass init a@a.com ``` ## Setup the protonmail bridge client At this point we already set up the password manager that will allow the protonmail bridge to store the passwords so we will now setup your protonmail account. ``` protonmail-bridge --cli >>>> add (add your protonmail account to bridge) >>>> (enter your protonmail account email address) >>>> (enter your protonmail account password) >>>> list (list configured accounts) >>>> info (list SMTP credentials for configuring any local SMTP compatible service) >>>> help (get familiarized with the bridge options) >>>> exit (exit the bridge console which stops the local SMTP server created) ``` Exit the scripted mode of the isolated user if you previously ran "script /dev/null" ``` exit ``` ## Daemonize the protonmail bridge client In order to start automatically the bridge client on system startup we will create a script to run it in the background. Notice that we will use the "screen" utility since there is no way to run the protonmail linux client in the background currently without a graphical interface. For this we will need root access again. ``` exit ``` Create a basic script that will be able to launch the protonmail bridge client in the background and kill it. ``` mkdir /var/lib/protonmail nano /var/lib/protonmail/protonmail.sh (copy the content of the file and save) chmod +x /var/lib/protonmail/protonmail.sh ``` Create a systemd service ``` nano /etc/systemd/system/protonmail.service (copy the content of the file and save) ``` Enable the script so that it can run on system startup ``` systemctl enable protonmail ``` Test the protonmail service ``` systemctl start protonmail netstat -tulpn | grep 1025 ``` Reboot you system and check if protonmail bridge is bound to the default ports ``` reboot netstat -tulpn | grep 1025 ``` Protonmail Bridge cannot listen to IP other than `127.0.0.1` and Discourse in a Docker container cannot send SMTP requests to `127.0.0.1` of the host machine. We must configure it to send to `172.17.0.1(the Docker's bridge IP on the host machine)`, and then redirect to `127.0.0.1`, on which the Protonmail Bridge is listening We do it using Socat # Configure SMTP services Now that you have the protonmail bridge running in the background you can configure SMTP emails on local instances of Jenkins, Jira, Bitbucket, Thunderbird or any service of your choice. Remember that required credentials and configuration details can be found by executing: ``` protonmail-bridge --cli >>>> info >>>> exit ``` # Problems **Note:** When sending an email via PHPMailer, the following message is displayed: ``` Connection failed. Error #2: stream_socket_client(): unable to connect to 127.0.0.1:1026 (Connection refused) SMTP ERROR: Failed to connect to server: Connection refused (111) ``` OR ``` SMTP INBOUND: "454 4.7.0 account is logged out, use the app to login again" SERVER -> CLIENT: 454 4.7.0 account is logged out, use the app to login again SMTP ERROR: Password command failed: 454 4.7.0 account is logged out, use the app to login again SMTP Error: Could not authenticate. ``` **Solution 1 :** More than one process listens on the same port. Changing the port in Protonmail-bridge may correct the problem. To solve it I had to: Login as the new isolated user ``` su protonmail cd ~ ``` This is required if we are not using a graphical interface due to the way our isolated user runs the shell commands ``` script /dev/null ``` Change port setting ``` change port ``` **Solution 2 :** Two user processes (root and protonmail) are executed at the same time. 1. Stopping the "proton-bridge" process using the killall command ``` killall -9 proton-bridge ``` 2. Full uninstall protonmail-bridge ``` apt purge protonmail-bridge ``` 2. remove all protonmail folders and configuration files in the 'root' profile 3. remove the protonmail folder in the "home" folder ``` rm -rf /home/protonmail ``` 4. reboot 5. Repeat the protonmail-bridge installation procedure # Problems **Note:** When running Bridge on command line, I the following message is printed: ``` WARN[0000] Failed to add test credentials to keychain error="exit status 1: gpg: Passwords: skipped: No public key\ngpg: [stdin]: encryption failed: No public key\nPassword encryption aborted.\n" helper="*pass.Pass" ``` He had a bug with your keyring and pass. **Solution:** To solve it I had to: 1. uninstall gnupg and pass `apt remove gnupg pass` 2. delete the `.gnupg` and `.password-store` folders ``` rm -rf /home/protonmail/.gnupg rm -rf /home/protonmail/.password-store ``` 3. reinstall gnupg and pass `apt install gnupg pass` 4. login as the new isolated user ``` su protonmail cd ~ ``` 5. run a script session to avoid the PGP key passphrase prompt to fail `script /dev/null` 6. run gpg to create the database and its folder `gpg --list-keys` 7. create a new key ``` gpg --full-generate-key >>>> Choose 1 (1) RSA and RSA (default) >>>> Choose 2048 (default) >>>> Choose 0 0 = key does not expire >>>> Type your name e.g. Proty McProtonFace >>>> Type your email e.g. a@a.com >>>> Leave empty comment >>>> Leave empty passphrase ``` 8. Init the password manager for the chosen email address in the PGP keys step `pass init a@a.com` 9. List the keys to ensure they were created correctly `gpg --list-keys` 10. Setup the protonmail bridge client, follow the procedure I described here --- **set configuration on containers/app.yml** ``` ## TODO: List of comma delimited emails that will be made admin and developer ## on initial signup example 'user1@example.com,user2@example.com' DISCOURSE_DEVELOPER_EMAILS: 'admin@radiant.capital' ## TODO: The SMTP mail server used to validate new accounts and send notifications # SMTP ADDRESS, username, and password are required # WARNING the char '#' in SMTP password can cause problems! DISCOURSE_SMTP_ADDRESS: 172.17.0.1 DISCOURSE_SMTP_PORT: 1026 DISCOURSE_SMTP_USER_NAME: admin@radiant.capital DISCOURSE_SMTP_PASSWORD: "INsDNvoaYJ6RTiLaxTFApg" #DISCOURSE_SMTP_ENABLE_START_TLS: true # (optional, default true) DISCOURSE_SMTP_DOMAIN: radiant.capital DISCOURSE_NOTIFICATION_EMAIL: admin@radiant.capital DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none ## If you added the Lets Encrypt template, uncomment below to get a free SSL certificate LETSENCRYPT_ACCOUNT_EMAIL: admin@radiant.capital ``` ## Install unlock plugin radiant.rake file ``` # frozen_string_literal: true namespace :radiant do desc "Radiant Task: remove user from group" # description. task :radiant_task => :environment do proposer_group = Group.find_by(name: "proposer") read_only_group = Group.find_by(name: "read-only") proposers = proposer_group.users readers = read_only_group.users proposers.each do |user| if user.lock_timestamp == nil user.groups.delete(proposer_group) user.save elsif Time.now - 1800 > user.lock_timestamp user.groups.delete(proposer_group) user.save! end end readers.each do |user| if user.lock_timestamp == nil user.groups.delete(read_only_group) user.save elsif Time.now - user.lock_timestamp > 1800 user.groups.delete(read_only_group) user.save! end end end end ``` Create radiant.rake file by following content and copy it insider radiant docker container /lib/tasks ``` - setup scheduler on crontab - - - - - /var/discourse/unlock_cron_job - unlock_cron_job pid=`docker ps -aqf "name=app"` docker exec ${pid} rake radiant:radiant_task echo ${pid} >> /var/discourse/cron.log ``` ``` How to build docker container - build base_slim image - build actual image and publish for detailed info, plz check .github/workflows/build.yml ``` ## Discourse docker rebuild guidance 1. Switch to test-passed branch in discourse docker repo 2. build base_slim image with the command `ruby auto_build.rb base_slim` ``` # simple build file to be used locally by Sam # require 'pty' require 'optparse' images = { base_slim: { name: 'base', tag: "discourse/base:build_slim", squash: true, extra_args: '-f slim.Dockerfile' }, base: { name: 'base', tag: "discourse/base:build", extra_args: '-f release.Dockerfile' }, discourse_test_build: { name: 'discourse_test', tag: "discourse/discourse_test:build", squash: false}, discourse_dev: { name: 'discourse_dev', tag: "discourse/discourse_dev:build", squash: false }, } def run(command) lines = [] PTY.spawn(command) do |stdin, stdout, pid| begin stdin.each do |line| lines << line puts line end rescue Errno::EIO # we are done end Process.wait(pid) end raise "'#{command}' exited with status #{$?.exitstatus}" if $?.exitstatus != 0 lines end def build(image) lines = run("cd #{image[:name]} && docker build . --no-cache --tag #{image[:tag]} #{image[:squash] ? '--squash' : ''} #{image[:extra_args] ? image[:extra_args] : ''}") raise "Error building the image for #{image[:name]}: #{lines[-1]}" if lines[-1] =~ /successfully built/ end def dev_deps() run("sed -e 's/\(db_name: discourse\)/\1_development/' ../templates/postgres.template.yml > discourse_dev/postgres.template.yml") run("cp ../templates/redis.template.yml discourse_dev/redis.template.yml") end if ARGV.length != 1 puts <<~TEXT Usage: ruby auto_build.rb IMAGE Available images: #{images.keys.join(", ")} TEXT exit 1 else image = ARGV[0].to_sym if !images.include?(image) $stderr.puts "Image not found" exit 1 end puts "Building #{images[image]}" dev_deps() if image == :discourse_dev build(images[image]) end ``` 4. build base image with the command `ruby auto_build.rb base` ``` ARG from=discourse/base ARG tag=build_slim FROM $from:$tag RUN cd /var/www/discourse &&\ sudo -u discourse bundle config --local deployment true &&\ sudo -u discourse bundle config --local path ./vendor/bundle &&\ sudo -u discourse bundle config --local without test development &&\ sudo -u discourse bundle install --jobs 4 &&\ sudo -u discourse yarn install --production --frozen-lockfile &&\ sudo -u discourse yarn cache clean &&\ bundle exec rake maxminddb:get &&\ find /var/www/discourse/vendor/bundle -name tmp -type d -exec rm -rf {} + ``` 6. Push the image to the docker hub ## Unlock plugin development Unlock plugin is used for web3 authentication in discourse community server https://github.com/radiant-capital/unlock Discourse plugin is supported by community and the unlock repo is the main part for widget customization ### unlock plugin customization **The main purpose of customizing unlock plugin is for db operation and api endpoint upgrade** db configuration looks like this app/controllers/unlock_controller.rb ``` # frozen_string_literal: true class UnlockController < ApplicationController def unlock lock = params.require(:lock) wallet = params[:wallet] transaction = params[:transaction] return unless current_user return unless settings = ::Unlock.settings return if settings["lock_address"].blank? || settings["lock_address"] != lock.downcase return unless group = Group.find_by(name: settings["unlocked_group_name"]) user = User.find_by(id: current_user.id) user.lock_timestamp = Time.now user.save if group.users.find_by(id: current_user.id) else group.users << current_user end value = { lock: lock } value[:wallet] = wallet if wallet.present? value[:transaction] = transaction if transaction.present? PluginStore.set(::Unlock::PLUGIN_NAME, "#{::Unlock::TRANSACTION}_#{current_user.id}", value) render json: success_json end def unlock_premium lock = params.require(:lock) wallet = params[:wallet] transaction = params[:transaction] return unless current_user return unless settings = ::Unlock.settings return if settings["lock_address"].blank? || settings["lock_address"] != lock.downcase return unless group = Group.find_by(name: settings["unlocked_group_name"]) user = User.find_by(id: current_user.id) user.lock_timestamp = Time.now user.save if group.users.find_by(id: current_user.id) else group.users << current_user end group = Group.find_by(name: "rfp-author") if group.users.find_by(id: current_user.id) else group.users << current_user end value = { lock: lock } value[:wallet] = wallet if wallet.present? value[:transaction] = transaction if transaction.present? PluginStore.set(::Unlock::PLUGIN_NAME, "#{::Unlock::TRANSACTION}_#{current_user.id}", value) render json: success_json end end ``` There're two endpoints (unlock, unlock_premium) to keep users data by locked amount of token Required params in db 1. lock_timestamp: used to refresh the group ids by cron job 2. group: clarify if the user is for premium access The client side to call the endpoint looks like this assets/javascripts/discourse/initializers/extend-for-unlock.js.es6 ``` window.addEventListener("unlockProtocol.status", ({ detail }) => { const { state, locks } = detail; console.log("detail", detail); if (state === "unlocked" && window._wallet) { const data = { lock: window._lock || settings.lock_address, wallet: window._wallet, }; if (window._transaction) data["transaction"] = window._transaction; let url; if (window._redirectUrl == null) url = document.location.origin; else url = document.location.origin + _redirectUrl; console.log("out of if case befor reset", detail); reset(); console.log("out of if case after reset", detail); if (locks.length == 2) { console.log("preimum", data); return ajax("/unlock_premium.json", { type: "POST", data }).then( () => (window.location.href = url + "?n=" + new Date().getTime()) // random number ); } else if (locks.length == 1) { console.log("non-preimum", data); return ajax("/unlock.json", { type: "POST", data }).then( () => (window.location.href = url + "?n=" + new Date().getTime()) ); } } }); ``` Depending on the users' lock amount, it calls the api endpoint with dedicated infos ### Unlock modal configuration The unlock repo has a lot of parts (we only use iframe section) and the main files we should keep focus on is `unlock_app/src/components/` The starting point of the modal is main component in `unlock_app/src/components/interface/checkout/main/index.tsx` ``` import React, { useCallback, useEffect, useMemo } from 'react' import type { PaywallConfig } from '~/unlockTypes' import { checkoutMachine } from './checkoutMachine' import { Select } from './Select' import { useActor, useInterpret } from '@xstate/react' import { useAuth } from '~/contexts/AuthenticationContext' import { isEqual } from 'lodash' import { CheckoutHead, CheckoutTransition, TopNavigation } from '../Shell' interface Props { injectedProvider: unknown paywallConfig: PaywallConfig // eslint-disable-next-line @typescript-eslint/no-explicit-any communication?: any redirectURI?: URL } export function Checkout({ paywallConfig, injectedProvider, communication, redirectURI, }: Props) { console.log('checkout page communication', communication) const checkoutService = useInterpret(checkoutMachine, { context: { paywallConfig, }, }) const [state] = useActor(checkoutService) const { account } = useAuth() const matched = state.value.toString() const paywallConfigChanged = !isEqual( paywallConfig, state.context.paywallConfig ) useEffect(() => { if (paywallConfigChanged) { checkoutService.send({ type: 'UPDATE_PAYWALL_CONFIG', config: paywallConfig, }) } }, [paywallConfig, checkoutService, paywallConfigChanged]) useEffect(() => { const user = account ? { address: account } : {} if (communication?.insideIframe) { communication.emitUserInfo(user) } }, [account, communication]) const onBack = useMemo(() => { const unlockAccount = state.children?.unlockAccount const canBackInUnlockAccountService = unlockAccount ?.getSnapshot() .can('BACK') const canBack = state.can('BACK') if (canBackInUnlockAccountService) { return () => unlockAccount.send('BACK') } if (canBack) { return () => checkoutService.send('BACK') } return undefined }, [state, checkoutService]) const onClose = useCallback( (params: Record<string, string> = {}) => { if (redirectURI) { for (const [key, value] of Object.entries(params)) { redirectURI.searchParams.append(key, value) } return window.location.assign(redirectURI) } if (!communication?.insideIframe) { window.history.back() } else { window?.top?.location.replace('https://community.radiant.capital') communication.emitCloseModal() } }, [communication, redirectURI] ) const Content = useCallback(() => { switch (matched) { case 'SELECT': { return ( <Select injectedProvider={injectedProvider} checkoutService={checkoutService} isReady={communication && communication.isReady} /> ) } default: { return null } } }, [matched, injectedProvider, checkoutService, communication]) return ( <CheckoutTransition> <div className="bg-white max-w-md rounded-xl flex flex-col w-full h-[90vh] sm:h-[60vh] min-h-[32rem] max-h-[42rem]" style={{ height: '56vh' }} > <TopNavigation onClose={onClose} onBack={onBack} /> <CheckoutHead iconURL={paywallConfig.icon} title={paywallConfig.title} /> <Content /> </div> </CheckoutTransition> ) } ``` The designs for the popup modal implemented in this file and it will be loaded as iframe We also use unlock's smart contract but I don't think maintenance is not needed Currently unlock app is deployed to our own DigitalOcean droplet You can find deploy script here ``` #!/usr/bin/env bash # Script fails if any command fails set -e # This script deploys a static build to netlify. # It requires AUTH_TOKEN and SITE_ID to be set (see details on how to set them using deploy.sh) APP_PATH=$1 DEPLOY_ENV=$2 COMMIT=$3 PUBLISH=$4 BUILD_PATH="out/" if [ "$DEPLOY_ENV" = "staging" ]; then if [ "$PUBLISH" = "true" ]; then # This is a build on master, we deploy to staging as a published build PROD="--prod" MESSAGE="Deploying $COMMIT to staging. See logs below." else # we deploy as a draft on staging PROD="" MESSAGE="Deploying $COMMIT to draft. See draft URL and logs below." fi fi if [ "$DEPLOY_ENV" = "prod" ]; then PROD="--prod" MESSAGE="Deploying version $COMMIT to production" fi if [ -n "$SITE_ID" ] && [ -n "$AUTH_TOKEN" ]; then # Package UNLOCK_ENV="$DEPLOY_ENV" yarn deploy # And ship! echo $MESSAGE npx -y netlify-cli deploy --build -s $SITE_ID -a $AUTH_TOKEN --dir=$BUILD_PATH $PROD --message="$MESSAGE" else echo "Failed to deploy to Netlify because we're missing SITE_ID and/or AUTH_TOKEN" exit 1 fi ```