Forms Feature Documentation
link Introduction
The TCN Forms feature allows you to create a customizable and extendable form. You can control access based on the user's TCN Council position as well as perform input validation. External form validation is also supported, allowing you to completely customize question validation and conditional pages with code.
To access this feature, go to /forms (you can find this under Staff Area > Documents if you are logged in as a council member).
To create a new form, click "New Form" on the forms home page. You can also edit
forms that you have edit access for if you have the edit link (/docs/edit/:ID
).
link Basics
link Structure
The first section is the name and the pages. We will first go over setting up questions with no conditions or validation, and we will introduce those in a later section. You can skip ahead to this section for documentation on external validation / conditions.
You can enable "Collect Names" to collect the submitter's name. However, this option is not allowed if logged-out users can access the form (see the section on access controls).
The name is required. You must have at least one page. However, a page may contain no questions. Page names and descriptions are both optional. The page description may use markdown (refer to the docs documentation for a reference sheet). Each page contains questions, each of which must have a name. Question descriptions are optional and also support markdown.
The following types of questions exist:
- Short Text - single-line text input
- Long Text - paragraph text input
- Number - numerical input
- Multiple Choice - select from a list of options
- Date - date/time input (you can choose date-only, time-only, or full datetime)
link Actions
The next section is the form actions. You may select a webhook to post form submissions to. It does not have to be a Discord webhook. This section will go over Discord webhooks, and we will go over external webhooks in a later section.
To use this, check "Post Submission to Webhook". Then, paste your Discord webhook into the webhook URL. You can now select "Only Post Link" to not send answers to the webhook and only post a link to the submission on the website.
If the webhook is in a forum channel, you will need to select the "Forum Channel" option. If you do not, it will not work and will silently error. You must then select a naming scheme for how forum posts will be named:
- Static Name - all forum posts will be named the same
- Use Submitter Name - if "Collect Names" is enabled, you can choose this and forum posts will be named the user's name/tag
- Base On Answer - you can select a question and forum posts will be named based on the submission's answer to that question
link Access
In this section, you can control who is allowed to access the form.
- Only you can edit the form's access permissions.
- Only you and observers can delete the document.
- Observers always have view access.
- Allow Observers - allow TCN observers to submit
- Allow TCN Council - allow all council members view and submit access
- Allow Logged In Users - allow any user, but they must be logged in to view the form and submit
- Allow Observers to Edit - observers will also be able to edit the form but not access permissions
- Allow TCN Council to Edit - allow all TCN Council Members to edit the form but not access permissions
- Make Public - allow everyone to view and submit to the form, even if they are not logged in (disables the "Collect Names option")
- Allowlist - you can allow specific users to view and submit to the form by ID
link Appearance
Here, you can customize how the embed looks when the link is posted into Discord.
link Conditions & Validation
Each page can be shown conditionally. If a page's condition is not met, it will not be shown to the user. Even if the user enters answers onto one of these pages and then change some answers that then hide the page, their answers will not be included in their submission.
You can select the following options for conditions:
- TCN Council Status - you can choose to show the page to observers, server owners, council advisors, and non-council members separately. The user's highest classification will determine if they see this page; that is, unchecking "if observer" and checking "if owner" will still hide the page from observers that are server owners.
- Question - you can show the page conditionally based on a question. The
question must be in an earlier page to be the condition for a page.
- Number - you can show the page based on how the user's answer compares to a fixed value, supporting >, ≥, =, ≤, <, and ≠. If the question is not required, you can also select whether or not the page should be shown if the question is skipped.
- Multiple Choice - you can show the page based on whether the user has any / all of a fixed list of options selected / not selected.
- Date - you can show the page based on whether the user's answer is before or on, before, after or on, after, between, or not between fixed dates. If the question is not required, you can also select whether or not the page should be shown if the question is skipped.
Each question also supports validation, and the user will not be allowed to go to the next page until all of their answers pass validation.
You can select the following validation options:
- Short Text
- You can require an email address.
- You can require a URL.
- You can require a user ID (it will require it to correspond to a valid user.)
- If none of these are selected, you can require a minimum and/or maximum length.
- Long Text
- You can require a minimum and/or maximum length.
- Number
- By default, the answer must be an integer, but you can allow non-integer input.
- You can require a minimum and/or maximum value.
- Multiple Choice
- You can require a minimum and/or maximum number of selected options.
- If the maximum value is 1, the input goes from checkboxes to radio buttons.
- If the maximum and minimum values are 1, you can choose to display the input as a dropdown instead of a list of radio buttons.
- Date
- You can choose to require the answer to be in the past or in the future.
link External Webhooks
You can enter a non-Discord webhook to process submission data yourself. To do this, enable "Post Submission to Webhook" and enter a non-Discord URL. You should see an info pop-up informing you that it is not a Discord webhook URL.
It is also highly recommended that you set a secret, which you should generate using a cryptographically secure method and should be at least 32 bytes. We will go over how to use the secret to ensure submissions are actually being sent by the TCN server.
Your webhook will receive the submission in the following format:
{
"id": "[string]: this is the form's ID (present in the URL)",
"sid": "[number]: this is an incrementing ID unique to each submission per form",
"user": "[string | null]: this is the user's ID if Collect Names is enabled and null otherwise",
"answers": [
{
"id": "[number]: the question ID",
"question": "[string]: the question text at the time of submission",
"answer": "[variable]: for short/long text, it is a string; for number, it is a number; for multiple-choice, it is an array; for date, it is a date string",
"show_date": "[boolean]: for date questions, this is whether or not the date is shown for this question",
"show_time": "[boolean]: for date questions, this is whether or not the time is shown for this question"
}
]
}
For verification with the secret:
- You will receive the following headers:
X-Signature-Timestamp
- This is the millisecond timestamp of when the server generated the data to send. You should reject submissions that are more than a few seconds old to avoid repetition attacks.X-Signature-Nonce
- This is a randomly generated nonce value to avoid repetition attacks (it is a non-negative integer less than one billion). The same nonce will not be used with the same timestamp, so if you see the same combination again, you should reject it.X-Signature-Hash
- This is the hex digest of the SHA-512 hash of{timestamp}:{nonce}:{secret}:{body}
, where{body}
is the input body, which is a JSON string with no extra whitespace.
- The purpose of setting a secret is so that you can ensure that requests are actually sent by us. If your URL is exposed, other people could send garbage requests to it, and by verifying the signature, you can ignore those requests.
- In case an existing request gets intercepted, an attacker could repeat the
request to your server. To avoid this, we recommend the following.
- Store the nonce and timestamp of each request. If you see this combination again, ignore the request.
- Ignore requests whose timestamp is more than a few seconds old, e.g. 10 seconds. You can therefore discard nonce-timestamp pairs after 10 seconds (to avoid running out of memory).
- The most important part is to ensure the signature is correct. Join the
timestamp, nonce, secret, and body with colons and hash it with SHA-512. The
timestamp and nonce are sent in the headers
X-Signature-Timestamp
andX-Signature-Nonce
respectively, and the body is simply the POST request body. The secret is the fixed value you provide us, which you must keep secret for this to work.- Join the timestamp, nonce, secret, and body with colons.
- Hash it with SHA-512 and obtain the hex string output.
- Ensure this hash is equal to the value of the header
X-Signature-Hash
using a timing safe equality check (to avoid timing attacks).
Here is a sample of how to validate the request in Node.JS:
const secret = "...";
const timestamp = request.headers["X-Signature-Timestamp"];
const nonce = request.headers["X-Signature-Nonce"];
const body = await request.text();
const signature = request.headers["X-Signature-Hash"];
if (!timestamp.match(/^\d+$/)) return new Response("Invalid timestamp.", { status: 400 });
if (!nonce.match(/^\d+$/)) return new Response("Invalid nonce.", { status: 400 });
if (new Date().getTime() - parseInt(timestamp) > 10000) return new Response("Request is too old.", { status: 400 });
const key = timestamp + ":" + nonce;
if (set.has(key)) return new Response("Timestamp-nonce pair is duplicate.", { status: 400 });
const stream = new TextEncoder().encode(key + ":" + secret + ":" + body);
const hash = await crypto.subtle.digest("SHA-512", stream);
const hex = Buffer.from(hash).toString("hex");
if (hex !== signature) return new Response("Invalid signature.", { status: 400 });
set.add(key);
setTimeout(() => set.remove(key), 100000);
// At this point, the request is validated and not duplicate.
// You will need to define a set of strings outside of the handler function.
// You can get the data using JSON.stringify(body).
link External Controls
To enable external form controls, check the "Use External Validation/Conditions" option at the top. You will then need to specify a URL which will serve as the API. If external controls are enabled, the default options for page conditions and page validation are removed. The following features exist.
Validation requests are sent by the client to determine which pages are shown and whether the user may proceed and then by the server before submission to ensure they are not tampering with data. You can always be sure that submissions are valid according to your API; however, you may receive garbage data. You should handle all data that meets the following specifications properly but not error on faulty data, as your API endpoint will be exposed to the user.
- External Access Controls - In addition to the supported access controls,
you can check "Use External Access" in the access section. This is only allowed
if the form is not visible to logged-out users; that is, "Make Public" is
disabled.
Then, you must implementGET /access
on your API route, which will receive the query argumentuser
with the ID of the user being checked. You must return a truthy value if the user should be allowed to access the form and a falsy value otherwise in JSON format, e.g."true"
or"false"
. - External Page Conditions - Per page, you can check "Show Conditionally"
to make the page conditional.
Then, you must implementPOST /condition/:index
where:index
is the page number (starting at 1 - it is the same as the page number in the form editor). This route will receive a JSON object (with headerContent-Type: application/json
). The object contains the following keys:user
- The user's ID ornull
if the user is logged out.answers
- An object containing all of the user's current answers. The keys are the question IDs (which you can find at the top of each question in the editor) and the values are strings for text input, numbers for number input, an array of integers for multiple-choice questions (0
for the first option), and a date string for date questions.
"true"
or"false"
. - External Question Validation - Per question, you can check "Use
Validation" to make the question validated.
Then, you must implementPOST /validation/:id
where:id
is the question ID (which you can find at the top of each question in the editor). This route will receive a JSON object (with headerContent-Type: application/json
). This object contains the following keys:user
- The user's ID ornull
if the user is logged out.answer
- The user's answer for this question, which is a string for text input, number for number input, array of integers for multiple-choice questions (0
for the first option), and date string for date questions.
"false"
or'"You must select all options."'
.