├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.py ├── db.py ├── drop.sql ├── models ├── room_service.py ├── subject_service.py └── user_service.py ├── requirements.txt ├── schema.sql ├── static ├── 404.ico └── style.css ├── templates ├── base.html ├── create.html ├── index.html ├── register.html ├── room.html ├── search.html ├── subject.html └── subjects.html └── views ├── message_routes.py ├── room_routes.py ├── routes.py ├── subject_routes.py └── user_routes.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '38 7 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Githubs Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | 139 | # Cython debug symbols 140 | cython_debug/ 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat app 2 | 3 | [![License: CC0-1.0](https://licensebuttons.net/l/zero/1.0/80x15.png)](http://creativecommons.org/publicdomain/zero/1.0/) 4 | 5 | Website where users can send messages inside chat rooms, the rooms are are divided into subjects. Once logged in the users can create new subjects and rooms, created subjects can be set to private with a password log in. 6 | 7 | Writen in Python, using [Flask](https://flask.palletsprojects.com/en/1.1.x/) and [PostgreSQL](https://www.postgresql.org/). 8 | 9 | You can try out the website by registering or try the admin user: 10 | 11 | username: `Admin` password: `1234`. 12 | 13 | ## Features 14 | 15 | * Information is saved to a database 16 | 17 | * Registration and login. 18 | 19 | * User can create subjects and rooms. 20 | 21 | * User can send and delete his/her messages. 22 | 23 | * User can create a password protected private subjects. 24 | 25 | * Admin user. 26 | 27 | * Website is protected against SQL injections, XSS and CSRF attacks. 28 | 29 | ### Admin User 30 | 31 | * Has access to all rooms and subjects. 32 | 33 | * Can delete any room or message. 34 | 35 | * Can search all messages. 36 | 37 | ## Left Out 38 | 39 | * Can edit messages, rooms, userinfo and subjects. 40 | 41 | * Check inputs with regex. 42 | 43 | * Grid list contain more info. 44 | 45 | * User / Admin page. 46 | 47 | * Limit number of subjects and rooms per user. 48 | 49 | ## Command Prompts 50 | 51 | After navigating to the project folder, first steps when loading is to set up virtual enviroment to hold the project and loading dependencies. 52 | 53 | If the command `pip install -r requirements.txt` doesn't work correctly you will need to to manually download required modules using `pip install module-name`. 54 | 55 | You can check all used modules from the `requirements.txt` file. 56 | 57 | ### Enviroment 58 | 59 | To run the program localy you must create a `.env` file inside your project folder. The file will need to have the following information. 60 | 61 | * `DATABASE_URL=postgresql://'user':'password'localhost:'PORT'/'database-name'`. 62 | 63 | * `SECRET_KEY='generated_password'` - this information needs to be limited to local machine. 64 | 65 | 66 | ### Linux & macOS 67 | 68 | * `python3 -m venv venv` - Creates a local virtual envirnoment for the project with a name 'venv'. 69 | 70 | * `. venv/bin/activate` - Activates the virtual envirnoment. 71 | 72 | * **(venv)** `pip install -r requirements.txt` - Checks and downloads the project dependencies. 73 | 74 | * **(venv)** `flask run` - Runs the program on port `localhost:5000`. 75 | 76 | ### Windows 77 | 78 | * `py -3 -m venv venv` - Creates local python 3 virtual envirnoment folder named 'venv'. 79 | 80 | * `venv\Scripts\activate` - Activates the virtual envirnoment. 81 | 82 | * **(venv)** `pip install -r requirements.txt` - installs dependecies. 83 | 84 | * **(venv)** `flask run` - Starts the app on a development server (uses port 5000) defined in app.py. 85 | 86 | ### PostgreSQL 87 | 88 | If you have [PostgreSQL](https://www.postgresql.org/) installed you can run these commands, you will need to create a database inside your cluster. 89 | 90 | 91 | * `DATABASE_URL=postgresql://'user:password'@localhost:'PORT'/'database-name'` - template for .env configuration. 92 | 93 | * `psql -U 'user' -d 'database-name' < schema.sql` Creates SQL-tables. 94 | 95 | * `psql -U 'user/superuser' -d 'database-name' < drop.sql` Deletes all SQL-tables. 96 | 97 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 6 | app.config["TEMPLATES_AUTO_RELOAD"] = True 7 | 8 | from views import routes -------------------------------------------------------------------------------- /db.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from flask_sqlalchemy import SQLAlchemy 3 | from os import getenv 4 | 5 | app.config["SQLALCHEMY_DATABASE_URI"] = getenv("DATABASE_URL") 6 | app.secret_key = getenv("SECRET_KEY") 7 | db = SQLAlchemy(app) 8 | -------------------------------------------------------------------------------- /drop.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users CASCADE; 2 | DROP TABLE IF EXISTS rooms CASCADE; 3 | DROP TABLE IF EXISTS messages CASCADE; 4 | DROP TABLE IF EXISTS subjects CASCADE; 5 | DROP TABLE IF EXISTS subject_rights CASCADE; -------------------------------------------------------------------------------- /models/room_service.py: -------------------------------------------------------------------------------- 1 | from flask import session, flash 2 | from werkzeug.security import check_password_hash, generate_password_hash 3 | 4 | 5 | def create_room(user_id, room_name, subject_id, db): 6 | try: 7 | sql = "INSERT INTO rooms (room_name, user_id, subject_id, visible) values(:room_name, :user_id, :subject_id, 1)" 8 | db.session.execute( 9 | sql, {"room_name": room_name.strip(), "user_id": user_id, "subject_id": subject_id}) 10 | db.session.commit() 11 | return True 12 | except: 13 | return False 14 | 15 | 16 | def delete_room(room_id, db): 17 | if is_owner(session["id"], room_id, db) or session["admin"]: 18 | try: 19 | sql = "UPDATE rooms SET visible = 0 WHERE id = :id" 20 | db.session.execute(sql, {"id": room_id}) 21 | db.session.commit() 22 | return True 23 | 24 | except: 25 | return False 26 | else: 27 | return False 28 | 29 | 30 | def get_room(room_id, db): 31 | sql = "SELECT rooms.id AS room_id, room_name, username FROM rooms LEFT JOIN users ON user_id = users.id WHERE rooms.id=:id AND rooms.visible=1" 32 | result = db.session.execute(sql, {"id": room_id}) 33 | roomData = result.fetchone() 34 | 35 | return roomData 36 | 37 | 38 | def get_messages(room_id, db): 39 | sql = "SELECT content, username, messages.created_at AS datetime, messages.id AS message_id FROM messages LEFT JOIN users ON user_id = users.id LEFT JOIN rooms ON room_id = rooms.id WHERE rooms.id=:id AND messages.visible = 1 ORDER BY messages.created_at" 40 | result = db.session.execute(sql, {"id": room_id}) 41 | messages = result.fetchall() 42 | 43 | return messages 44 | 45 | 46 | def get_subject(room_id, db): 47 | sql = "SELECT subject_id FROM rooms WHERE id = :room_id" 48 | result = db.session.execute(sql, {"room_id": room_id}) 49 | subject_id = result.fetchone()[0] 50 | 51 | return subject_id 52 | 53 | 54 | def is_owner(user_id, room_id, db): 55 | if session["admin"]: 56 | print("Admin rights") 57 | return True 58 | 59 | else: 60 | sql = "SELECT COUNT(*) FROM rooms WHERE user_id = :user_id AND id = :room_id" 61 | result = db.session.execute( 62 | sql, {"user_id": user_id, "room_id": room_id}) 63 | is_owner = result.fetchone()[0] 64 | 65 | return is_owner > 0 66 | -------------------------------------------------------------------------------- /models/subject_service.py: -------------------------------------------------------------------------------- 1 | from flask import session, flash 2 | from werkzeug.security import check_password_hash, generate_password_hash 3 | 4 | 5 | def create_subject(user_id, subject_name, password, content, require, db): 6 | hash_value = generate_password_hash(password) 7 | 8 | if password == "": 9 | password = "1234" 10 | 11 | try: 12 | sql = """INSERT INTO subjects (subject_name, password, content, require_permission) 13 | VALUES (:subject_name, :password, :content, :require_permission)""" 14 | 15 | db.session.execute( 16 | sql, {"subject_name": subject_name, "password": hash_value, "content": content, "require_permission": require}) 17 | db.session.commit() 18 | except: 19 | return False 20 | else: 21 | add_right(session["id"], subject_name, db) 22 | return True 23 | 24 | 25 | def get_subjects(db): 26 | sql = "SELECT id, subject_name, require_permission FROM subjects" 27 | result = db.session.execute(sql) 28 | subjectsData = result.fetchall() 29 | 30 | return subjectsData 31 | 32 | 33 | def get_subject(subject_id, db): 34 | sql = "SELECT * FROM subjects WHERE id=:id" 35 | result = db.session.execute(sql, {"id": subject_id}) 36 | subject = result.fetchone() 37 | 38 | return subject 39 | 40 | 41 | def get_rooms(subject_id, db): 42 | sql = "SELECT id, room_name FROM rooms WHERE subject_id=:id AND visible=1" 43 | result = db.session.execute(sql, {"id": subject_id}) 44 | roomsData = result.fetchall() 45 | 46 | return roomsData 47 | 48 | 49 | def is_secret(subject_id, db): 50 | sql = "SELECT COUNT(*) FROM subjects WHERE id=:id AND require_permission=1" 51 | result = db.session.execute(sql, {"id": subject_id}) 52 | secret = result.fetchone()[0] 53 | 54 | return secret > 0 55 | 56 | 57 | def add_right(user_id, subject_name, db): 58 | try: 59 | sql = """INSERT INTO subject_rights (user_id, subject_id) 60 | VALUES (:user_id, (SELECT id FROM subjects WHERE subject_name = :subject_name))""" 61 | db.session.execute( 62 | sql, {"user_id": user_id, "subject_name": subject_name.strip()}) 63 | db.session.commit() 64 | except: 65 | print("Error adding rights") 66 | else: 67 | print("Rights added") 68 | 69 | 70 | def add_right_id(user_id, subject_id, db): 71 | try: 72 | sql = "INSERT INTO subject_rights(user_id, subject_id) VALUES(:user_id, :subject_id)" 73 | db.session.execute( 74 | sql, {"user_id": user_id, "subject_id": subject_id}) 75 | db.session.commit() 76 | except: 77 | print("Error adding rights") 78 | else: 79 | print("Rights added") 80 | 81 | 82 | def has_right(user_id, subject_id, db): 83 | if session["admin"]: 84 | print("Admin rights") 85 | return True 86 | else: 87 | if is_secret(subject_id, db): 88 | sql = "SELECT COUNT(*) FROM subject_rights WHERE user_id = :user_id AND subject_id = :subject_id" 89 | result = db.session.execute( 90 | sql, {"user_id": user_id, "subject_id": subject_id}) 91 | hasRight = result.fetchone()[0] 92 | 93 | if hasRight > 0: 94 | print("User has rights to subject_id", subject_id) 95 | return True 96 | else: 97 | print("User doesn't have rights to subject_id", subject_id) 98 | return False 99 | else: 100 | return True 101 | 102 | 103 | def check_password(subject_id, password, db): 104 | try: 105 | sql = "SELECT password, id FROM subjects WHERE id = :subject_id" 106 | result = db.session.execute(sql, {"subject_id": subject_id}) 107 | subjectData = result.fetchone() 108 | except: 109 | return False 110 | else: 111 | if subjectData == None: 112 | return False 113 | else: 114 | hash_value = subjectData[0] 115 | if check_password_hash(hash_value, password): 116 | return True 117 | else: 118 | return False 119 | -------------------------------------------------------------------------------- /models/user_service.py: -------------------------------------------------------------------------------- 1 | from flask import session, flash 2 | from werkzeug.security import check_password_hash, generate_password_hash 3 | from models import room_service, subject_service 4 | import os 5 | 6 | 7 | def login(username, password, db): 8 | try: 9 | sql = "SELECT password, id, role FROM users WHERE username=:username" 10 | result = db.session.execute(sql, {"username": username}) 11 | user = result.fetchone() 12 | except: 13 | return False 14 | else: 15 | if user == None: 16 | return False 17 | else: 18 | hash_value = user[0] 19 | if check_password_hash(hash_value, password): 20 | session["username"] = username 21 | session["id"] = user[1] 22 | session["csrf_token"] = os.urandom(16).hex() 23 | session["admin"] = False 24 | if user[2] == 0: 25 | print("Logged in as Admin") 26 | flash("Logged in as Admin", "message") 27 | session["admin"] = True 28 | return True 29 | else: 30 | return False 31 | 32 | 33 | def logout(): 34 | session.pop("username", None) 35 | session.pop("id", None) 36 | session.pop("csrf_token", None) 37 | session.pop("admin", False) 38 | 39 | 40 | def register(username, password, db): 41 | hash_value = generate_password_hash(password) 42 | try: 43 | sql = "INSERT INTO users (username,password, role) VALUES (:username,:password, 1)" 44 | db.session.execute(sql, {"username": username, "password": hash_value}) 45 | db.session.commit() 46 | except: 47 | return False 48 | else: 49 | return login(username, password, db) 50 | 51 | def get_id(username, db): 52 | sql = "SELECT id FROM users WHERE username=:username" 53 | result = db.session.execute(sql, {"username": username}) 54 | user_id = result.fetchone()[0] 55 | 56 | return user_id 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | autopep8==1.5.3 2 | click==7.1.2 3 | Flask==1.1.2 4 | Flask-SQLAlchemy==2.4.4 5 | gunicorn==20.0.4 6 | itsdangerous==1.1.0 7 | Jinja2==2.11.3 8 | MarkupSafe==1.1.1 9 | psycopg2==2.8.5 10 | pycodestyle==2.6.0 11 | python-dotenv==0.14.0 12 | SQLAlchemy==1.3.18 13 | toml==0.10.1 14 | waitress==2.1.2 15 | Werkzeug==1.0.1 -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users 2 | ( 3 | id SERIAL PRIMARY KEY, 4 | username varchar(21) NOT NULL, 5 | password TEXT NOT NULL, 6 | role INTEGER, 7 | CONSTRAINT unique_username UNIQUE(username) 8 | ); 9 | CREATE TABLE subjects 10 | ( 11 | id SERIAL PRIMARY KEY, 12 | subject_name TEXT NOT NULL, 13 | password TEXT, 14 | content TEXT, 15 | require_permission INTEGER, 16 | CONSTRAINT unique_subjectname UNIQUE(subject_name) 17 | 18 | ); 19 | CREATE TABLE rooms 20 | ( 21 | id SERIAL PRIMARY KEY, 22 | room_name TEXT NOT NULL, 23 | user_id INTEGER NOT NULL, 24 | subject_id INTEGER NOT NULL, 25 | visible INTEGER NOT NULL, 26 | CONSTRAINT fk_user 27 | FOREIGN KEY (user_id) 28 | REFERENCES users(id), 29 | CONSTRAINT fk_subject 30 | FOREIGN KEY (subject_id) 31 | REFERENCES subjects(id) 32 | ); 33 | CREATE TABLE messages 34 | ( 35 | id SERIAL PRIMARY KEY, 36 | user_id INTEGER NOT NULL, 37 | room_id INTEGER NOT NULL, 38 | content TEXT, 39 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 40 | visible INTEGER NOT NULL, 41 | CONSTRAINT fk_user 42 | FOREIGN KEY (user_id) 43 | REFERENCES users(id), 44 | CONSTRAINT fk_room 45 | FOREIGN KEY (room_id) 46 | REFERENCES rooms(id) 47 | ); 48 | CREATE TABLE subject_rights 49 | ( 50 | id SERIAL PRIMARY KEY, 51 | user_id INTEGER NOT NULL, 52 | subject_id INTEGER NOT NULL, 53 | CONSTRAINT fk_user 54 | FOREIGN KEY (user_id) 55 | REFERENCES users(id), 56 | CONSTRAINT fk_subject 57 | FOREIGN KEY (subject_id) 58 | REFERENCES subjects(id) 59 | ); 60 | 61 | -------------------------------------------------------------------------------- /static/404.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macwille/python-chat-app/379d18e077add9a74593ef14b30f6fb301c3cf02/static/404.ico -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-image: linear-gradient(to bottom, #ccc5b9, #444444); 3 | background-color: #ccc5b9; 4 | color: #252422; 5 | font-family: Helvetica, Verdana, sans-serif; 6 | text-transform: uppercase; 7 | max-width: 60em; 8 | margin: auto; 9 | } 10 | 11 | header { 12 | height: 8vh; 13 | background-size: auto; 14 | } 15 | 16 | .flash ul { 17 | display: inline-block; 18 | list-style-type: none; 19 | border-radius: 15px; 20 | padding: 10px; 21 | background-color: #eb5e28; 22 | } 23 | 24 | .flash ul li { 25 | color: white; 26 | } 27 | 28 | .mainTitle { 29 | background-image: linear-gradient(to bottom, #eb5e28, #eb5e28); 30 | font-weight: bold; 31 | padding: 1em 1em; 32 | font-size: 40px; 33 | box-shadow: 2px 2px 4px rgba(0, 0, 0, .25); 34 | } 35 | 36 | .mainTitle h1 { 37 | border-radius: 25px; 38 | background-color: #403d39; 39 | color: #fffcf2; 40 | text-align: center; 41 | padding: 20px; 42 | overflow: hidden; 43 | } 44 | 45 | .navbar { 46 | background: #403d39; 47 | box-shadow: 2px 2px 4px rgba(0, 0, 0, .25); 48 | } 49 | 50 | .navbar ul { 51 | margin: auto; 52 | padding-inline-end: 10px; 53 | padding-inline-start: 10px; 54 | } 55 | 56 | .navbar ul li { 57 | display: inline-block; 58 | margin: 16px 8px; 59 | color: white; 60 | } 61 | 62 | .navbar ul li:last-child { 63 | float: right; 64 | } 65 | 66 | .navbar ul li a { 67 | border-radius: 5px; 68 | text-decoration: none; 69 | font: 25px/1; 70 | font-weight: bold; 71 | color: #fffcf2; 72 | background-color: #eb5e28; 73 | padding: .2rem .5rem; 74 | border: 1px solid #fffcf2; 75 | } 76 | 77 | .navbar ul li a:hover { 78 | background-color: white; 79 | color: #eb5e28; 80 | } 81 | 82 | .content { 83 | font: 18px/1; 84 | position: relative; 85 | background-color: #fffcf2; 86 | padding: 20px; 87 | margin-top: 20px; 88 | margin-bottom: 60px; 89 | min-height: 80vh; 90 | box-shadow: 2px 2px 4px rgba(0, 0, 0, .25); 91 | } 92 | 93 | .pagetitle { 94 | margin: auto; 95 | } 96 | 97 | .grid-wrapper { 98 | display: grid; 99 | grid-gap: 10px; 100 | margin: 0 auto; 101 | padding-right: 40px; 102 | } 103 | 104 | .grid-list { 105 | display: grid; 106 | grid-template-columns: 1fr 1fr 1fr; 107 | grid-auto-rows: minmax(80px, auto); 108 | list-style-type: none; 109 | grid-gap: 20px; 110 | } 111 | 112 | .grid-list li { 113 | text-align: center; 114 | border: 2px solid #403d39; 115 | background-color: #eb5e28; 116 | border-radius: 8px; 117 | } 118 | 119 | .grid-list p { 120 | color: white; 121 | } 122 | 123 | .grid-list li:hover { 124 | background-color: white; 125 | } 126 | 127 | .grid-list li:hover p { 128 | color: #eb5e28; 129 | } 130 | 131 | .chat-wrapper { 132 | margin: 4px, 4px; 133 | padding: 4px; 134 | width: 100%; 135 | max-height: 800px; 136 | overflow-x: hidden; 137 | overflow-y: auto; 138 | text-align: justify; 139 | } 140 | 141 | .message-list { 142 | list-style-type: none; 143 | border: 2px solid #403d39; 144 | background-color: #f1f1f1; 145 | border-radius: 6px; 146 | } 147 | 148 | .message-list:nth-child(even) { 149 | background-color: white; 150 | } 151 | 152 | .message-paragraph { 153 | text-transform: none; 154 | } 155 | 156 | .message-paragraph b { 157 | text-transform: uppercase; 158 | color: #eb5e28; 159 | margin-right: 15px; 160 | } 161 | 162 | .message-list span { 163 | float: right; 164 | margin-right: 10px; 165 | } 166 | 167 | .message-list form input { 168 | float: right; 169 | margin-right: 10px; 170 | } 171 | 172 | .textarea-wrapper { 173 | display: flex; 174 | } 175 | 176 | .textarea-wrapper input[type="text"] { 177 | font-size: 24px; 178 | width: 100%; 179 | } 180 | 181 | .textarea-wrapper input[type="submit"] { 182 | margin: 8px; 183 | width: 8%; 184 | } 185 | 186 | a { 187 | text-decoration: none; 188 | color: #eb5e28; 189 | font: 18px/1; 190 | font-weight: bold; 191 | } 192 | 193 | a:hover { 194 | color: coral; 195 | } 196 | 197 | p { 198 | padding: auto; 199 | } 200 | 201 | form { 202 | margin-top: 20px; 203 | margin-bottom: 20px; 204 | } 205 | 206 | fieldset { 207 | padding: 10px; 208 | } 209 | 210 | input[type="text"] { 211 | margin: .4rem; 212 | padding: .1rem; 213 | background-color: white; 214 | font: 22px/1; 215 | } 216 | 217 | input[type="password"] { 218 | margin: .4rem; 219 | padding: .1rem; 220 | background-color: white; 221 | font: 22px/1; 222 | } 223 | 224 | input[type="submit"], button, input[type="reset"] { 225 | text-transform: uppercase; 226 | border-radius: 3px; 227 | background-color: #eb5e28; 228 | color: white; 229 | font: 12px/1; 230 | } 231 | 232 | input[type="submit"]:hover, button:hover, input[type="reset"]:hover { 233 | text-transform: uppercase; 234 | border-radius: 3px; 235 | background-color: white; 236 | color: #eb5e28; 237 | font: 12px/1; 238 | } 239 | 240 | textarea { 241 | margin: .4rem; 242 | padding: .1rem; 243 | background-color: white; 244 | font: 15px/1; 245 | } 246 | 247 | textarea { 248 | resize: none; 249 | font: 25px/1 Helvetica, Verdana, sans-serif; 250 | } 251 | 252 | hr { 253 | margin: 3vh 10vh; 254 | } 255 | 256 | .endtext { 257 | vertical-align: bottom; 258 | margin-bottom: 3vh; 259 | text-align: center; 260 | opacity: 40%; 261 | } 262 | 263 | .endtext p { 264 | text-transform: none; 265 | color: #eb5e28; 266 | } 267 | 268 | .endtext a { 269 | text-transform: uppercase; 270 | } 271 | 272 | ::-webkit-scrollbar { 273 | width: 10px; 274 | } 275 | 276 | ::-webkit-scrollbar-track { 277 | background: #f1f1f1; 278 | } 279 | 280 | ::-webkit-scrollbar-thumb { 281 | background: #888; 282 | } 283 | 284 | ::-webkit-scrollbar-thumb:hover { 285 | background: #eb5e28; 286 | } -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}{% endblock %} - Chat404 7 | 8 | 9 | 10 | 11 | 12 |
13 |

CHAT404

14 |
15 | 16 | 32 | 33 |
34 |
35 | {% if session.admin %} 36 | Admin session 🔓 37 | {% endif %} 38 | {% block header %}{% endblock %} 39 | {% with messages = get_flashed_messages() %} 40 | {% if messages %} 41 |
42 |
    43 | {% for message in messages %} 44 |
  • {{ message }}
  • 45 | {% endfor %} 46 |
47 |
48 | {% endif %} 49 | {% endwith %} 50 |
51 |
52 | 53 | {% block content %}{% endblock %} 54 | 55 |
56 | 57 |
58 |
59 |
60 |

Github repository of this website can be found here 61 |

62 |
63 |
64 | 65 | -------------------------------------------------------------------------------- /templates/create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} Create {% endblock %} 3 | 4 | {% block header %} 5 |

Create

6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 | {% if session.username %} 11 | 12 | {% if id > 0 %} 13 | 14 |

Room

15 |
16 | 17 |
18 | New Room 19 |

Name:
20 |

21 | 22 | 23 |
24 |
25 | 26 | 27 | {% else %} 28 | 29 |

Subject

30 |
31 | 32 |
33 | New Subject 34 |

Name:
35 |

36 |

Description:
37 |

38 |

Requires Password:
39 | 43 |

47 |

Password:
48 |

49 | 50 |
51 |
52 | 53 | {% endif %} 54 | 55 | 56 | {% else %} 57 | 58 |

Log in to view the rooms

59 |
60 | Log in 61 |
62 | or 63 |
64 | Register 65 | 66 | {% endif %} 67 | 68 | {% endblock %} -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} Home {% endblock %} 3 | 4 | {% block header %} 5 | 6 |

Welcome to Chat404

7 | {{ message }} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 13 | {% if session.username %} 14 | 15 |

You are logged in as '{{ session.username }}'

16 | To chat go to Rooms 17 |

or

18 | Log out 19 | 20 | 21 | {% else %} 22 | 23 |

Log in to chat!

24 | 25 |
26 |
27 | Log in 28 |

Username:
29 |

30 |

Password:
31 |

32 |
33 |

New user?
34 | Register here 35 |

36 |
37 |
38 | 39 | {% endif %} 40 | 41 | {% endblock %} -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 |

{% block title %}Register{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content%} 8 | 9 | {% if session.username %} 10 | 11 |

You are logged in as '{{ session.username }}'

12 |
13 | Log out 14 | 15 | 16 | {% else %} 17 | 18 |
19 | 20 |
21 | Register 22 |

Username:
23 |

24 |

Password:
25 |


26 | 27 | 28 |
29 |
30 | 31 | {% endif %} 32 | 33 | {% endblock %} -------------------------------------------------------------------------------- /templates/room.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 |

{% block title %} {{ name }} {% endblock %}

5 |

{{ content }}

6 | 7 | {% if owner == session.username or session.admin %} 8 |

9 | You can remove messages of this room
10 | or 11 | Delete the room 12 |

13 | 14 | 15 | 16 | 17 |
18 |

19 | 20 | {% else %} 21 | 22 |

Owner: {{ owner }}

23 | 24 | {% endif %} 25 | 26 | {% endblock %} 27 | 28 | {% block content%} 29 | 30 | {% if session.username %} 31 | 32 | 33 | 34 | {% if messages is defined %} 35 |

Messages

36 |
37 | {% for message in messages %} 38 | 39 | {% if message[1] == session.username or session.username == owner or session.admin %} 40 | 41 | 55 | 56 | 57 | {% else %} 58 | 66 | {% endif %} 67 | 68 | {% endfor %} 69 |
70 | 71 | 72 | {% else %} 73 | 74 |
75 |

No messages yet

76 |
77 |
78 | 79 | {% endif %} 80 | 81 | 82 | 83 |
84 | 85 | 86 | 87 |
88 | 89 | 90 |
91 | 92 |
93 | 94 | 95 | {% else %} 96 | 97 |

You must be logged in to send messages to this room.

98 |
99 | Log in 100 |
101 | or 102 |
103 | Register 104 | 105 | {% endif %} 106 | 107 | {% endblock %} -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 |
5 |

{% block title %} Search {% endblock %}

6 |
7 | {% endblock %} 8 | 9 | {% block content%} 10 | 11 | {% if session.username %} 12 |
13 | 14 | {% if query is defined %} 15 | 16 | {% if results|length == 1 %} 17 |

Found {{ results|length }} match for '{{ query }}'

18 | 19 | {% else %} 20 |

found {{ results|length }} matches for '{{ query }}'

21 | {% endif %} 22 | 23 | {% endif %} 24 | 25 | {% if results is defined %} 26 |
27 | {% for result in results %} 28 | 29 | {% if result[0] == session.username or session.admin %} 30 |
31 |

32 | 33 | 34 | 35 | 36 | {{result[0]}} 37 | ({{result[1]}}): 38 | 39 |

{{ result[2]}}
40 |

41 |
42 | 43 | 44 | {% else %} 45 | 46 |

{{result[0]}} ({{result[1]}}): 47 |

{{ result[2]}}
48 |

49 | 50 | {% endif %} 51 | 52 | {% endfor %} 53 | 54 |
55 | 56 | {% endif %} 57 | 58 |
59 | 60 |
61 | 62 | 63 |
64 |
65 |
66 | 67 | 68 | {% else %} 69 | 70 |
71 |

You must be logged in to use the search function

72 | Log in 73 |
74 | or 75 |
76 | Register 77 |
78 | 79 | {% endif %} 80 | 81 | {% endblock %} -------------------------------------------------------------------------------- /templates/subject.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 | 5 |

{% block title %} {{ subject_name }} {% endblock %}

6 |

'{{ content }}'

7 | 8 | 9 | {% endblock %} 10 | 11 | {% block content%} 12 | 13 | {% if session.username %} 14 | 15 | 16 | {% if require == 1 and hasRight != True and session.admin != True %} 17 | 18 |
19 |

This area is restricted by a password

20 |
21 | 22 |
23 | Login to {{ subject_name }} 24 | 25 |

Password:
26 |

27 | 28 |
29 |
30 |
31 | 32 | 33 | {% else %} 34 | 35 |

You can create a new room here

36 | 37 | 38 |
39 | 40 | {% if rooms is defined and rooms|length > 0 %} 41 | 42 |

Enter a room to chat

43 | 44 | 55 | {% else %} 56 |

{{ subject_name }} doesn't have any rooms yet

57 | {% endif %} 58 |
59 | 60 | 61 | {% endif%} 62 | 63 | 64 | {% else %} 65 | 66 |

67 | Log in to view the rooms 68 |
69 | Log in 70 |
71 | or 72 |
73 | Register 74 |

75 | 76 | {% endif %} 77 | 78 | {% endblock %} -------------------------------------------------------------------------------- /templates/subjects.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} Rooms {% endblock %} 4 | 5 | {% block header %} 6 | 7 |

Subjects

8 | 9 | {% endblock %} 10 | 11 | {% block content%} 12 | 13 | {% if session.username %} 14 | 15 |

You can create a new subject Here

16 | 17 |
18 | 19 | {% if subjects is defined %} 20 | 21 |

Select a subject

22 | 23 | 47 | 48 | {% endif %} 49 | 50 |
51 | 52 | 53 | {% else %} 54 | 55 |
56 |

You must be logged in to view rooms

57 | Log in 58 |
59 | or 60 |
61 | Register 62 |
63 | 64 | {% endif %} 65 | 66 | {% endblock %} -------------------------------------------------------------------------------- /views/message_routes.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from views import routes 3 | from flask import Flask, url_for, flash, redirect, render_template, request, session 4 | from models import room_service, subject_service 5 | from db import db 6 | 7 | 8 | @app.route("/send", methods=["POST"]) 9 | def send(): 10 | routes.check_token() 11 | content = request.form["content"] 12 | room_id = request.form["room_id"] 13 | user_id = session["id"] 14 | subect_id = room_service.get_subject(room_id, db) 15 | 16 | if routes.word_too_long(content): 17 | flash("Message had a word that was too long", "message") 18 | return redirect(url_for("room", id=room_id)) 19 | 20 | elif subject_service.has_right(user_id, subect_id, db): 21 | try: 22 | sql = """INSERT INTO messages (user_id, room_id, content, created_at, visible) 23 | VALUES (:user_id, :room_id, :content, NOW(), 1)""" 24 | db.session.execute( 25 | sql, {"user_id": user_id, "room_id": room_id, "content": content.strip()}) 26 | db.session.commit() 27 | except: 28 | return redirect("subjects") 29 | else: 30 | return redirect(url_for("room", id=room_id)) 31 | else: 32 | return redirect(url_for("subjects")) 33 | 34 | 35 | @ app.route("/delete_message", methods=["GET", "POST"]) 36 | def delete_message(): 37 | routes.check_token() 38 | user_id = session["id"] 39 | message_id = request.form["message_id"] 40 | page = request.referrer 41 | try: 42 | if session["admin"]: 43 | sql = "UPDATE messages SET visible = 0 WHERE id = :id" 44 | db.session.execute(sql, {"id": message_id}) 45 | db.session.commit() 46 | else: 47 | sql = "UPDATE messages SET visible = 0 WHERE id = :id AND user_id = :user_id" 48 | db.session.execute(sql, {"id": message_id, "user_id": user_id}) 49 | db.session.commit() 50 | except: 51 | flash("Error deleteting message", "error") 52 | return redirect(url_for("subjects")) 53 | else: 54 | flash("Message deleted", "message") 55 | return redirect(page) 56 | 57 | 58 | @app.route("/result", methods=["GET"]) 59 | def result(): 60 | if not session["username"]: 61 | return redirect(url_for(routes.search)) 62 | 63 | username = session["username"] 64 | query = request.args["query"] 65 | 66 | sql = """SELECT username, room_name, content, messages.id AS messages_id FROM messages 67 | LEFT JOIN users ON user_id = users.id LEFT JOIN rooms on room_id = rooms.id 68 | WHERE lower(content) LIKE :query AND messages.visible = 1""" 69 | 70 | sql_not_admin = " AND username = :username" 71 | 72 | if session["admin"]: 73 | sql_query = db.session.execute( 74 | sql, {"query": "%"+query+"%"}) 75 | else: 76 | sql2 = sql + sql_not_admin 77 | sql_query = db.session.execute( 78 | sql2, {"query": "%"+query.lower()+"%", "username": username}) 79 | 80 | results = sql_query.fetchall() 81 | 82 | if not results: 83 | flash("No results", "message") 84 | return redirect(url_for("search")) 85 | else: 86 | return render_template("search.html", results=results, query=query) 87 | -------------------------------------------------------------------------------- /views/room_routes.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from views import routes 3 | from flask import Flask, url_for, flash, redirect, render_template, request, session 4 | from models import room_service, subject_service 5 | from db import db 6 | 7 | 8 | @app.route("/create_room", methods=["POST", "GET"]) 9 | def create_room(): 10 | routes.check_token() 11 | user_id = session["id"] 12 | room_name = request.form["room_name"] 13 | subject_id = request.form["subject_id"] 14 | 15 | if subject_service.has_right(user_id, subject_id, db): 16 | 17 | if room_service.create_room(user_id, room_name, subject_id, db): 18 | flash("Room created", "message") 19 | return redirect(url_for("subject", id=subject_id)) 20 | 21 | else: 22 | flash("Problem creating room", "error") 23 | return redirect(url_for("subject", id=subject_id)) 24 | else: 25 | flash("Problem creating room", "error") 26 | return redirect(url_for("subject", id=subject_id)) 27 | 28 | 29 | @app.route("/room/id=") 30 | def room(id): 31 | user_id = session["id"] 32 | subject_id = room_service.get_subject(id, db) 33 | 34 | if subject_service.has_right(user_id, subject_id, db): 35 | 36 | roomData = room_service.get_room(id, db) 37 | if roomData: 38 | room_id = roomData[0] 39 | name = roomData[1] 40 | username = roomData[2] 41 | messages = room_service.get_messages(room_id, db) 42 | if messages: 43 | return render_template("room.html", id=room_id, name=name, owner=username, messages=messages) 44 | else: 45 | return render_template("room.html", id=room_id, name=name, owner=username) 46 | else: 47 | flash("Room not found", "error") 48 | return redirect(url_for("subjects")) 49 | else: 50 | return redirect(url_for("subject", id=subject_id)) 51 | 52 | 53 | @app.route("/delete_room", methods=["GET", "POST"]) 54 | def delete_room(): 55 | routes.check_token() 56 | user_id = session["id"] 57 | room_id = request.form["room_id"] 58 | if room_service.is_owner(user_id, room_id, db): 59 | if room_service.delete_room(room_id, db): 60 | flash("Room deleted", "message") 61 | return redirect(url_for("subjects")) 62 | else: 63 | flash("Error deleting room", "error") 64 | return redirect(url_for("subjects")) 65 | else: 66 | flash("Error deleting room", "error") 67 | return redirect(url_for("subjects")) 68 | -------------------------------------------------------------------------------- /views/routes.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from views import subject_routes, room_routes, user_routes, message_routes 3 | from models import user_service 4 | from flask import Flask, flash, render_template, request, session, abort 5 | from db import db 6 | 7 | 8 | # Rest of the routes are imported from /views 9 | 10 | @app.route("/") 11 | def index(): 12 | return render_template("index.html") 13 | 14 | 15 | @app.route("/create/id=") 16 | def create(id): 17 | return render_template("create.html", id=id) 18 | 19 | 20 | @ app.route("/search") 21 | def search(): 22 | return render_template("search.html") 23 | 24 | 25 | # Utilties 26 | def check_token(): 27 | token = session["csrf_token"] 28 | form_token = request.form["csrf_token"] 29 | if token != form_token: 30 | print("Failed token") 31 | abort(403) 32 | else: 33 | print("Token checked") 34 | 35 | 36 | def word_too_long(string): 37 | lengths = [len(x) for x in string.split()] 38 | if any(l > 30 for l in lengths): 39 | return True 40 | else: 41 | return False 42 | 43 | 44 | @app.errorhandler(403) 45 | def resource_not_found(e): 46 | flash("Forbidden 403", "error") 47 | return render_template("index.html") 48 | 49 | 50 | @app.errorhandler(500) 51 | def server_error(e): 52 | flash("Server encountered an internal error", "error") 53 | return render_template("index.html") 54 | 55 | 56 | @app.errorhandler(Exception) 57 | def error_dump(error): 58 | print(error) 59 | flash("Unexpected Error", "error") 60 | return render_template("index.html") 61 | -------------------------------------------------------------------------------- /views/subject_routes.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from views import routes 3 | from flask import Flask, url_for, flash, redirect, render_template, request, session, abort 4 | from werkzeug.security import check_password_hash, generate_password_hash 5 | from models import subject_service 6 | from db import db 7 | 8 | 9 | @app.route("/subjects") 10 | def subjects(): 11 | subjects_data = subject_service.get_subjects(db) 12 | if subjects_data: 13 | return render_template("subjects.html", subjects=subjects_data) 14 | else: 15 | return render_template("subjects.html") 16 | 17 | 18 | @app.route("/create_subject", methods=["POST", "GET"]) 19 | def create_subject(): 20 | routes.check_token() 21 | page = request.referrer 22 | user_id = session["id"] 23 | subject_name = request.form["subject_name"] 24 | password = request.form["password"] 25 | content = request.form["content"] 26 | require = request.form["require"] 27 | 28 | print(require) 29 | print(password) 30 | 31 | if routes.word_too_long(content): 32 | flash("Description had a word that was too long", "error") 33 | return redirect(page) 34 | 35 | if int(require) == 1 and password == "": 36 | flash("You must give a password", "error") 37 | return redirect(page) 38 | 39 | if subject_service.create_subject(user_id, subject_name, password, content, require, db): 40 | flash("Subject created", "message") 41 | return redirect(url_for("subjects")) 42 | 43 | else: 44 | flash("Error creating subject - name might be taken", "error") 45 | return redirect(page) 46 | 47 | 48 | @ app.route("/subject_login", methods=["POST"]) 49 | def subject_login(): 50 | routes.check_token() 51 | subject_id = request.form["id"] 52 | password = request.form["password"] 53 | 54 | if subject_service.check_password(subject_id, password, db): 55 | subject_service.add_right_id(session["id"], subject_id, db) 56 | flash("Access granted", "message") 57 | return redirect(url_for("subject", id=subject_id)) 58 | else: 59 | flash("Access denied", "error") 60 | return redirect(url_for("subject", id=subject_id)) 61 | 62 | 63 | @ app.route("/subject/id=") 64 | def subject(id): 65 | subject_data = subject_service.get_subject(id, db) 66 | if subject_data: 67 | subject_id = subject_data[0] 68 | subject_name = subject_data[1] 69 | secret = subject_data[2] 70 | content = subject_data[3] 71 | require = subject_data[4] 72 | rooms = subject_service.get_rooms(subject_id, db) 73 | hasRight = subject_service.has_right(session["id"], subject_id, db) 74 | 75 | if rooms: 76 | return render_template("subject.html", subject_name=subject_name, rooms=rooms, id=subject_id, content=content, require=require, hasRight=hasRight) 77 | else: 78 | return render_template("subject.html", subject_name=subject_name, id=subject_id, content=content, require=require, hasRight=hasRight) 79 | else: 80 | return redirect(url_for("subjects")) 81 | -------------------------------------------------------------------------------- /views/user_routes.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from flask import Flask, url_for, flash, redirect, render_template, request 3 | from models import user_service 4 | from db import db 5 | 6 | 7 | @app.route("/login", methods=["POST", "GET"]) 8 | def login(): 9 | username = request.form["username"] 10 | password = request.form["password"] 11 | if user_service.login(username, password, db): 12 | flash("Welcome back", "message") 13 | return redirect(url_for("index")) 14 | else: 15 | flash("Error logging in", "error") 16 | return redirect(url_for("index")) 17 | 18 | 19 | @app.route("/logout") 20 | def logout(): 21 | user_service.logout() 22 | flash("Logged out", "message") 23 | return redirect(url_for("index")) 24 | 25 | 26 | @app.route("/register") 27 | def register(): 28 | return render_template("register.html") 29 | 30 | 31 | @app.route("/new_user", methods=["POST", "GET"]) 32 | def new_user(): 33 | username = request.form["username"] 34 | password = request.form["password"] 35 | if user_service.register(username, password, db): 36 | flash("Registration Successful", "message") 37 | return redirect(url_for("index")) 38 | else: 39 | flash("Registration Failed", "message") 40 | return redirect(url_for("register")) 41 | --------------------------------------------------------------------------------