X:/Notes is a note taking web app I developed. It uses jQuery AJAX requests along with a custom private PHP API to save, modify and delete notes. The notes can also be locked using AES-256 military grade encryption and users' passwords are stored using BCrypt, making this one of the most secure web apps I've made. It also has a unique token system for the "Remember Me" feature during login. All of this and more will be explained in detail below.
In the PHP code above, the session is started in order to check whether or not the user is logged in. Their credentials and token are stored in JSON format in a configuration file that is only accessible by the script. When tokens are assigned as a result of the user wanting to be "remembered" for the next week, they are given an assignment time and if the user logs out, that time is reset to nothing. Consequently, the script checks to see whether or not the token has a time assigned to it (to see if the token is actually valid). It then checks whether or not the token was created more than a week ago. If it was, then it is reset and the cookie is removed. This is done for security purposes to ensure the token is always changing every week so even if a hacker hijacks a user's token, they can't actually take over the user's account entirely. After the token's validity is confirmed, the script checks whether or not a cookie is set with the token and if said token matches the one in the configuration file. If it does, then the user is given a pass.
The image above shows how the user's credentials are stored. Their username is stored in plaintext and their password is hashed using BCrypt, a one-way hashing algorithm that cannot be broken (or at least it hasn't been broken yet).
This image shows how the token is stored. There is a "key" field and a "time" field. The actual token is stored in the key field and upon its initial creation, the UNIX timestamp at the time is stored in the "time" field. All tokens must have a valid creation time otherwise the script rejects it and changes the token, making it impossible for a user to spoof a token. The token itself is generated by shuffling the SHA-512 of a shuffled up UNIX timestamp. While "str_shuffle()" isn't completely randomized and using "random_int()" would be more effective instead of "time()", the security provided is enough to deter hackers.
The top half of the image above shows the user configuration file being fetched and decoded into an associative array. These are settings that the user can change at any time using the "Settings" page. The next line of code imports the SVG icons I use in the web app. The next piece of code uses a quick PHP script that determines whether or not the user is on a mobile browser. The next line gets the current domain name for later use in case the user wishes to share any notes. After this, the script determines which theme the user has enabled through their settings, determining the body background color and the color of the navigation bar on supported browsers.
The settings I mentioned earlier are stored in a configuration file in JSON format as seen in the image above. Each setting fits into a category such as "appearance" and "behavior".
After all of the above is done, the script then checks whether or not the user is logged in. A user is considered logged in under one of two conditions: Either they have to be logged in through the use of their username (and password), which is then compared to the credentials saved in the configuration file, or they have to have a valid token. If the user is logged in, they can continue to the rest of the web app, otherwise, the login page is shown.
The login page is shown above. It features a very unique aspect in that the lines that can be seen on the top left are actually the blacked out title of a note along with its title and shared/locked status. They act as placeholders that show what's actually past the login page. If the user logs in, those blacked out lines will appear to be replaced by the actual notes' data. But not to worry, these placeholder black/gray bars are generated and requested from the API using a completely separate function that doesn't and cannot reveal the titles or any potentially personal information related to the notes of the user. This allows the login page to have a unique look to it without any privacy concerns. The placeholders are updated every 5 seconds as well, so if someone is at the login page and the actual user creates a new note, the person at the login page will see a new placeholder get added to the list, allowing for a cool looking realtime view into the user's note list, just without being able to see what the notes' titles are, when they were created or anything private.
The image above shows the piece of code responsible for listening to any click events on the login button, "remember me" button and any key presses on the login input fields. If the login button is clicked, the values of the username and password field are assigned to the "username" and "password" variables. If the "remember me" button is active, the variable "remember" is set to true. Afterwards, an AJAX request is made to the API with "login" as the action and the username, password and remember variables as the rest of the data. If the data returned is "done" then the login is seen as successful and the user is notified of it and the page is refreshed within 2.5 seconds, otherwise an error is shown. The next event listener is responsible for detecting when the user presses the "Enter" or "Return" key on their keyboard while the login input fields are in focus, which triggers the login button allowing the user to quickly login without using the mouse to click the button. The final bit of code in the image handles the "remember me" button's event listener. If the button is clicked and already is active, then it's deactivated. Otherwise, it's just activated.
The function shown above is responsible for fetching the placeholder black bars that can be seen instead of the note titles, dates and other information. This is done by making a request to the API to fetch the placeholders. If the returned data isn't empty, then clearly there are notes (and therefore placeholders) to be added to the list. The "notes-wrapper" is shown while the editor is pushed off to the right, and the note list is populated with the placeholder notes. If the returned data is empty, then the "notes-wrapper" is hidden, the editor takes up almost the entire screen and the note list is emptied. In either case, the "adjust_to_window_size()" function is called to ensure everything fits the screen correctly.
The "notify()" function is used throughout X:/Notes and will probably be used in future projects that require a notification system. It's a lightweight and stylish approach to handling notifications. I looked for external libraries to do the same thing, but they were all massive in size and way too overcomplicated for such a simple feature. My solution has four parameters: "title", which is should define what the notification is about (e.g. "Error" or "Creating Note"), "description" which provides a bit more information regarding what event just took place to invoke the notification, "color" to make sure the notification for an error (for example) can easily be discerned from another notification stating a user's action was completed successfully. Finally, there's "duration" which dictates how long the notification stays on-screen (some notification might be a quick heads up to the user to let them know their note was created, but perhaps another more serious notification which requires the user's attention would have to stay visible long enough for the user to actually read and acknowledge). The function itself starts by creating an HTML element using the "title" and "description" parameter values and showing the "notification-area" while appending the element to said area. This area is a "div" element that is on top of the page at all times but is hidden and can be clicked through even when visible. By default, the actual notification element is hidden and to the right of the screen (beyond the body's width which has its overflow set to hidden); the notification element is shown while animating its movement from beyond the window's right border to the left. This animation takes 0.4 seconds. The notification wrapper is then given a color based on the parameter. Finally, two functions are run. The first one is given a delay based on the "duration" parameter and the second one is given a delay of 0.4 seconds based on the animation duration. Once the animation is complete, the notification is removed from the notification area. If the notification area is then empty, it is hidden.
"adjust_to_window_size()" is another function that is used throughout X:/Notes, though the elements it adjusts are different in the main page compared to the login page. Here, the window's width and height are determined. If the window's width is less than 740 pixels and the note list isn't empty, then the editor is hidden, the note list is made bigger and the search area is also made bigger to fit almost the entire body. This is essentially meant for monitors and screens that are too small to show both the list of notes and the editor at the same time. The next thing this function checks is whether or not the window's height is smaller than 500 pixels. If it is, then the middle area of the sidebar is hidden, otherwise it's displayed. The last bit of code in the image adds an event listener to the window that detects when the browser window is resized. When a resize is detected, the "adjust_to_window_size()" function is called. This ensures the user always looks at what I want them to see, resulting in a smooth and responsive UX (user experience).
The PHP code above is part of the API. The API script checks whether or not the user is logged in using the same piece of code as the main page. If the user is logged in, then their platform is detected and a whole range of functions that will be explained later will be made available to them. For now, we'll assume the user is on the login page and therefore isn't logged in.
The login and logout actions are part of a script called "process.php", which carries out any tasks that require modifying or creating files in any way. The API only reads files. In this case, the login action gets the posted username, password and "remember" values. If the username matches the one in the configuration file and the hashed password matches the saved hash, then a new token is generated. If the user wants to be remembered for a week, then this token is given a time of creation and a cookie is set, otherwise, it's given an empty creation time (which the other pages would reject and therefore ask the user to login) and the cookie is removed. This way, even if someone manages to get lucky and spoof their cookie to match the generated token, their token is rejected since it doesn't have a creation time. Plus, the token is changed every time the API is invoked if it doesn't detect a creation time. The logout code is pretty simple; it starts the session, then destroys it while setting the "$_SESSION" array to an empty one, generating a random token and giving it an empty creation date. This way, the user's token is nullified and their session is forgotten, requiring them to login again.
This section of the code is the HTML for the main page of the web application. There is some inline PHP visible in certain parts, specifically for the different icons. These PHP variables have an SVG path assigned to them, which allows me to use icons of any size and shape in the web app without taking up more storage space than required. In the body tag, it's also possible to see a "data-*" attribute, which is used throughout the web app along with jQuery's "data()" method.
An image showing the main page of the app.
The HTML code for the settings section of the app. The parent wrapper has a copy of the current user settings, then each option has a category and accompanying setting with a choice. This will be further explained below.
What the settings pane looks like.
The "Notes" section in the settings pane. The user can delete notes (even if they're locked) without any restrictions from this area. This has to be a feature, otherwise the user could make a note and forget their password, resulting in a note that cannot be deleted. By allowing this sort of bypass, the user can delete notes but the option to do so won't be right in front of them, so they won't accidentally delete any important notes.
The help section of the app.
When a note is open, the user can click on a menu button on the top right to access the options above. They can rename the note, lock it, share it, delete it, copy its content, view its raw data in JSON format or close the note.
The "initialize()" function starts by determining what platform the user is on, desktop or mobile. It then fetches the user's notes, then their user configuration, it then applies those settings, adjusts the page content based on window size, creates the tooltips if the user is on desktop and then finally sets the required attributes for the text editor which will later be used. The "set_global_settings()" function is responsible for parsing the JSON string representing the user's settings and then assigning said settings to global variables in order to let the rest of the code know what the user has enabled and disabled. Finally, these settings are actually applied after a short wait for them to be fetched.
The code above listens to keypresses on the search bar and filters the notes list based on user input, allowing the user to search through notes based on notes' titles.
Each note is assigned a tag based on its settings. If the note is locked, then the tag "#locked-note" is assigned to it. If it's shared, then it's a "#shared-note". The code above handles the functionality of the buttons on the sidebar that let the user filter notes based on three categories: locked notes, shared notes and all notes.
The bottom section of the sidebar has the help button, settings button and the logout button. The code above listens for any clicks on said buttons. Clicking the help button shows the help pane, clicking the settings button calls the "open_settings_page()" function which will be explained later, and the logout button sends an AJAX request to the "process.php" script with "logout" as the action. If the logout is successful, then any open notes will be closed and the page is refreshed.
The "actions navbar" is the area at the top of the editor which lets the user format their text, save the note, or open the menu that allows them to lock, rename and modify the note in other ways. Clicking the actions navbar itself doesn't do anything, hence why the default event is prevented from being invoked. This way, the user doesn't lose focus when they click the formatting buttons and the note menu doesn't close. The text formatting is done using "execCommand" and saving the note is done by running the "save_note()" function which will be explained later.
The code above handles the functionality of the "more" menu which can be opened by clicking the ellipsis ("...") icon on the top right of the editor when a note is open. Pressing the delete button opens up a user prompt using the "open_user_confirmation()" function. The "action-menu" button opens up the menu that allows the user to lock the note and such. There are then two submenu buttons for locking and sharing notes. These have a little border that turns blue when activated to let the user know which menu they have open. The neutral border color changes based on the theme the user has chosen. Sharing or making a note private calls the "note_sharing()" function. The "copy link" button uses the "data-domain" value set by PHP on the body to get the domain, add the share ID and the "view.php?" to form a link and finally copy it to the user's clipboard using the "copy_to_clipboard()" function. The lock, relock and unlock buttons all use the "open_note_lock()" function to open a panel that lets the user set a password to a note, remove its password or change it. The rename button lets the user rename the note, the "raw-button" lets the user view the note's raw JSON data, and the "copy-button" copies the note's HTML content to the user's clipboard. All the functions above will be explained in-depth later.
If the compose button or the empty editor are clicked, a user prompt pops up asking the user to enter the new note's title. The functionality of these user prompts are separated from the functionality of the buttons that open them to keep the code organized; as a result, they will be analyzed later on.
The code above is just a simple way to make collapsible text in the help section.
The image above shows the code that handles the functionality of the settings pane. Each button on the side of the settings pane has a unique class, and when a click is detected on either of them, that class is checked and the appropriate pane is opened using the "open_settings_pane()" function.
The settings pane under "actions" has a button that lets the user reset the settings of X:/Notes. This is done by sending an AJAX request with "reset-settings" as the action. Once the request is sent and confirmed, a visual 5 second countdown begins before the page refreshes.
The first section of the code in the image above is how the user's choices in the settings are temporarily saved before being sent to the PHP script that saves them permanently. When a button is clicked in the settings pane that is part of 2 or more choices, then the clicked button is given the "active" class while the buttons next to it (in the same subcategory) lose their active classes. The clicked button will have three "data-*" attributes called category, setting and choice; these are assigned to three variables. The current configuration is then fetched from the settings' apply button's "data-change" attribute and the JSON is parsed. The appropriate field is then modified and the entire associative array is then turned into a JSON string and overwritten to the "data-change" attribute. If the configuration is equal to the current one, the apply button is hidden entirely, otherwise it's shown. When the apply button is clicked, the current configuration is compared to the new configuration, and if they aren't the same, then the user's settings are saved using the "save_settings()" function that will be analyzed later.
The images above show the code for the buttons that allow the user to change their username and password. If the button is pressed and the appropriate fields are filled out and the changing of their username or password is successful, the page is refreshed and the user is logged out.
The code above allows the user to click a button in the settings page that would delete all their notes. A password is required before such an action takes place.
The "save_settings()" function accepts a settings configuration in JSON format as a parameter and posts this to the "process.php" script that saves it in the user's configuration file. If things save is successful, the global configuration variables are updated, the settings are applied and the settings page is closed.
The "open_settings_pane()" function accepts a pane's name as a parameter and then based on that name, opens the corresponding settings pane. If the pane is the notes pane, then a request to the API is made to fetch a list of the user's notes, but in JSON format, which is then parsed and rendered as a list the user can scroll through and delete items from if need be.
The "get_config()" function sends an AJAX request to the API to fetch the user's current settings. These settings are returned in the form of a JSON string, are parsed, and assigned to the "data-config" attribute of the settings wrapper (using the "data()" method to keep the HTML code clean) and the apply button's "data-change" attribute.
Closing the settings page empties the attributes I mentioned above.
The "open_settings_page()" function starts by calling the "get_config()" function, then it removes the "active" class from all the settings choice buttons and hides the apply button. It then fetches the current configuration and determines which settings pane the user wants to open by default and assigns the rest of the settings to variables. These variables are then checked and the appropriate choice buttons are given the "active" class to show the user what their current configuration looks like.
When a locked note is opened, a base64 encoded copy of its password is temporarily stored in the "data-pw" attribute of the editor through jQuery's "data()" method. This hides it from plain sight while still making it accessible. This is done so that the user can rename, share or otherwise modify the note without having to enter the password every time. When the note is closed, this password is removed, so it doesn't get left behind and the server doesn't ever keep a copy of the password.
Clicking the confirm button on the UI prompt that lets the user change a note's password or set one assigns the action the user wants to perform to a variable which is then checked with with multiple if statements. If the user wishes to lock a note, the passwords are checked to be identical (so the user doesn't make a spelling mistake) and the "lock_note()" function is called. If the intent is to change the note's password, then the "relock_note()" function is called and if the user wants to completely unlock the note, then the "unlock_note()" function is used.
The code above is responsible for the functionality of the UI prompt that lets the user make a new note or rename an existing one. The action is assigned to the ID attribute of the confirm button and retreived in the code above to determine whether the user wants to rename the note using the "rename_note()" function or create a new one with the "create_note()" function.
The image above and the next three images show one of the most crucial parts of the JS code as it allows the user to click on a note in the notes list and open it. When a note is clicked on, its ID (determined by the file name) is retreived from its ID attribute, its title is assigned to a variable, and its "data-locked" and "data-shared" attribute are checked to see whether or not the note is a locked or shared one (by default, these values are set to false).
If the note is locked, then the "lock-button" is hidden and the "unlock-button" and "relock-button" are shown in the note locking submenu in the "more" menu. If the note isn't already open (determined by whether or not it has the "active" class) then the UI prompt is shown to let the user enter the password to temporarily unlock the note. The editor is then given the note's title, ID and password as attributes.
If the note isn't locked, then the same things as above occur but there's no need for a user prompt and the "open_note()" function is used without a password.
If the note is shared, then the editor's "data-si" ("si" stands for Share ID) attribute is given the note's share ID as a value.
The "open_user_confirmation()" shows a UI prompt that can be used for multiple things. It accepts title, description, data and action as parameters. The title should tell the user what the prompt is asking for, the description should go into more detail, the data could be any value that needs to be passed to the function and the action is what the user prompt is supposed to send the "process.php" script when the confirm button is clicked.
The "open_note_lock()" functions displays a pane that can be used to lock a note, change its password or unlock it, but it is also used to "reveal" it, which is what happens when the user enters the note's password after clicking on it. It doesn't affect the note's lock, but it does let the user view the note's content.
The "get_notes()" function sends an AJAX request to the API and retreives the user's notes, subsequently populating the "notes-list" element with the data it gets from the API. It also calls the "adjust_to_window_size()" function to make sure everything is displayed correctly on the user's screen.
The "lock_note()", "unlock_note()" and "relock_note()" functions all work very similarly. They all have a parameter for password(s) and send that data to the "process.php" script which performs the desired action. The "rename_note()" function gets the current note's password from the editor's "data-pw" attribute and sends the new title and the aforementioned password to the "process.php" script. It then refreshes the note list and calls the "adjust_note_facade()" which will be analyzed later.
The "create_note()" function accepts the title of the new note as a parameter and sends it to the PHP script and refreshes the note list upon the creation of the new note. The "open_note()" function accepts three parameters: file, which is the file name of the note that's being opened. Password, which is the password of the note being opened and finally, locked, which is whether or not the note being opened is even locked to begin with, though this is also double checked on the server side so it can't be bypassed. Once the note is opened, its content overwrites the HTML content of the "editor-content".
The "save_note()" function accepts "background" as a parameter, which dictates whether or not the user receives a notification telling them that the note is being saved. The function starts by getting the current note's file name, its content, and its password. It then sends this data to the "process.php" script and refreshes the note list once the note has been successfully saved. The "delete_note()" function works the same way but instead gets the note's file name and password as parameters, and has a third parameter which lets the user bypass having to enter a password of a locked note to delete it. This is used in the settings page for example to delete notes without having to know the password (to ensure notes don't get stuck in the user's account if they forget the password).
The "raw_data()" function just gets the current note's data in JSON format. If the note is locked, then its content will show up as an encrypted string.
The "note_sharing()" function has three parameters: the file name of the note, its password and whether or not the user wants the note to be public (as a boolean value). If the user wants to share a note, then the "publicize-note" action is sent to the "process.php" script along with the rest of the data. If the user wants to make a note private, then the action field would be set to "privatize-note".
The "close_note()" function resets the editor's attributes while readjusting its height and closing any currently open menus related to the note since it's being closed. The "copy_to_clipboard()" function accepts any text as a parameter, creates a temporary (and hidden) input field, selects the text in it, and then executes the "execCommand" that copies any selected text to the user's clipboard. It then removes the temporary input field.
All the functions above are just dynamic CSS adjustments that don't require much of an explanation.
The "apply_settings()" function essentially calls all the aforementioned CSS adjustment functions because most of them are affected by the user's own settings configuration and need to be called each time the user changes a setting (and consequently applies it).
The functions above are just some trivial ones, such as showing the user a visual character count when choosing a title for their note, or capitalizing the first letter of a string or getting the current UNIX timestamp.
The "create_tooltips()" function creates tooltips using "tippy.js" for the appropriate elements/buttons.
The code above is from the "process.php" script. It is responsible for creating a note. It starts by getting the user's desired title for the note from the $_POST array. If the title is empty, then the title is given a value of "Undefined Title" as a note can't exist with an empty title. The file name of the note is the current UNIX timestamp (to avoid two files having the same name, but it also specifies the creation date of the file) and an MD5 hash of a jumbled up version of the current UNIX time. The file has an extension of ".xnt" standing for X:/Notes. An associative array is then created with the following keys: time_created, time_modified, file_name, locked, shared, password, author, title, and content. This array is then encoded into JSON format and written to the file.
The code above allows the user to rename a note. It decodes the content of the note file from JSON format to an associative array. It then checks whether or not the password the user entered is correct if the note is a locked one, and then replaces the title value with the new title and sets the time_modified value to the current timestamp. Subsequently, it encodes the array into JSON format again and overwrites the file.
The "save-note" action in the "process.php" script starts by identifying which note is being saved and getting the data being saved from the $_POST array. It then decodes the note file's content from JSON format to an associative array. If the note is locked, then it also checks to see if the password the user entered is correct, and then includes the AES-256 library. If the password is correct, then it uses that password to encrypt the data and replace the old encrypted data with the new one while also updating the last_modified value. If the note isn't locked, then its content is simply overwritten without being encrypted first.
The "delete-note" action identifies the note being deleted and checks to see whether or not the bypass is active. If it is, then the file is immediately deleted. If the bypass isn't set to true, then the password entered by the user is first verified against the valid one, then the note is deleted.
The "lock-note", "unlock-note" and "relock-note" actions all work similarly. The "lock-note" action includes the AES library, identifies the note being modified, parses its JSON content into an array, gets the password the user wants to use and hashes it using BCrypt, and encrypts the note's content using AES-256. The hashed password and the encrypted content are then saved in the note file. Unlocking the note works the same way but instead sets the password value to nothing while decrypting the note's content. Changing the note's password works by simply hashing the new password and replacing the old one with the new one.
The "publicize-note" and "privatize-note" actions work the same way. They both check to see whether or not the user entered the password for the note correctly, and then set the note's "shared" value to true or false depending on what the user wants to do.
The "raw-data" action simply outputs the note file's content in JSON format after verifying the password the user entered is correct.
The "save-settings" action gets the user's settings configuration in JSON format and overwrites the current configuration file with it. The "reset-settings" action overwrites the current configuration file with a default configuration where the theme is a light one, the note icons are colored, the formatting buttons are square, the search box is visible, the separators are visible, notes are reopened automatically, tooltips are enabled, the settings page opens on the appearance pane by default and notifications are enabled.
The "change-username" and "change-password" actions work by overwriting the current username or password in the account configuration file with the new values if the user's password is entered correctly. For the username, the string is checked so it's only letters and numbers as special characters and symbols aren't allowed.
The "delete-all-notes" action requires that the entered password be correct. It then creates an array of all the note files using "glob()" and recursively deletes each note.
The code above is part of the API. The "get_notes()" function accepts "format" as a parameter which dictates how it returns its data. It starts by including the required SVG paths that create the icons used for the lock and globe icons. It then creates an array of every note file using "glob()" and then sorts this array by the date they were last modified at.
Afterwards, for each note file, it parses their JSON data, gets the note's title, file name, time modified, locked and shared status, and then assigns them the appropriate tags which can then be filtered on the main page. If the format parameter is set to HTML, then the API outputs each note as a fully formed "div" element with the correctly formatted date, title, tags and icons. If the format is set to JSON, then most of this information is put into an associative array that is then encoded into a JSON string and outputted.
The "open-note" action identifies which note is being opened and whether or not a password has been entered to open it. The note file's JSON data is parsed into an array, and if it's locked, then the password is verified and then used to decrypt the note's content, which is then outputted. If the note isn't locked, then its content is just outputted without the need to decrypt it. The "get-config" action outputs the configuration file's content in JSON format.
The code above is part of the "view.php" page that lets anyone view a shared note. The note's Share ID is first identified based on what is entered after the question mark in the URL (in the format "view.php?s-1551628794-b82c74ca62248ca40fab874f31784263"). The note file's JSON is then parsed. If the note isn't shared, then the user is told that they entered an invalid link or the note they're trying to access is a private one. If the note is shared, then the script checks whether or not the note is locked. If it's locked, the user is asked to enter its password. If it isn't locked, then the note's content is shown.
The HTML code (with inline PHP) of the note viewing page.
The image above shows what the view page looks like.
If the note is locked, then a password input field is shown along with a submit button. Clicking the submit button calls the "view_note()" function.
The "view_note()" function has two parameters: id, and password. The "id" is the Share ID of the note, and the password is its password. This data is sent to the API through an AJAX request and if the password is verified to be correct, then the note's content is shown to the user.
The "view-note" action is part of the API and doesn't require that the user be logged in, but it immediately checks to see if the note being accessed is shared to begin with. If it's not shared, no further action is taken besides showing an error. If it's shared, then the script checks to see if it's also locked. If it's locked, then the entered password is verified and used to decrypt the note's content, which is then outputted and shown to the user. If the note isn't locked, then its content is shown without needing to be decrypted.
The image above shows what X:/Notes looks like on desktop. The image is split in half, with one half having the light theme applied, and the other half the dark one. You can access a demo version of the site below on either a mobile device or a desktop browser. The code for this project is also on GitHub (the download button below will take you there).
For security reasons and to prevent abuse, I've disabled the back-end code that handles note creation, deletion and things that involve creating or modifying files in general. The API is still functional, so the notes that are displayed in the demo are actual notes. It's possible to mess around with the settings and generally test out the responsiveness of the website, but keep in mind any bugs you find will most likely not exist in the real version of the web app.
Login details for the demo:
Login details for the download: