├── Procfile
├── app
├── src
│ ├── components
│ │ ├── sidebar
│ │ │ ├── chat_rooms_list.jsx
│ │ │ └── users_list.jsx
│ │ ├── friends
│ │ │ ├── friends_index.jsx
│ │ │ └── friends_list.jsx
│ │ ├── nav-bar
│ │ │ ├── notifications.jsx
│ │ │ ├── options.jsx
│ │ │ ├── nav_bar.jsx
│ │ │ └── user_search.jsx
│ │ ├── user
│ │ │ ├── search
│ │ │ │ ├── users_search_index.jsx
│ │ │ │ └── users_search_results_page.jsx
│ │ │ ├── users_index.jsx
│ │ │ └── user_home_page.jsx
│ │ ├── main
│ │ │ ├── footer.jsx
│ │ │ └── chatterbox_app.jsx
│ │ ├── chat
│ │ │ └── chat_room.jsx
│ │ └── auth
│ │ │ ├── sign_up_page.jsx
│ │ │ ├── login_page.jsx
│ │ │ ├── login_form.jsx
│ │ │ └── sign_up_form.jsx
│ ├── constants
│ │ ├── search_constants.js
│ │ ├── current_user_constants.js
│ │ └── user_constants.js
│ ├── dispatcher
│ │ └── dispatcher.js
│ ├── utilities
│ │ ├── flash.js
│ │ └── auth.js
│ ├── actions
│ │ ├── search_actions.js
│ │ ├── current_user_actions.js
│ │ └── user_actions.js
│ ├── stores
│ │ ├── user_autocomplete_store.js
│ │ ├── current_user_store.js
│ │ └── logged_in_users_store.js
│ ├── apiutil
│ │ ├── api_session_util.js
│ │ └── api_user_util.js
│ └── main.js
├── static
│ ├── stylesheets
│ │ └── sass
│ │ │ ├── modules
│ │ │ ├── _mixins.scss
│ │ │ ├── _all.scss
│ │ │ ├── _clearfix.scss
│ │ │ └── _variables.scss
│ │ │ ├── partials
│ │ │ ├── _links.scss
│ │ │ ├── _icons.scss
│ │ │ ├── _headers.scss
│ │ │ ├── _base.scss
│ │ │ ├── _body.scss
│ │ │ ├── _input_box.scss
│ │ │ ├── _flash.scss
│ │ │ ├── _reset.scss
│ │ │ └── _buttons.scss
│ │ │ ├── components
│ │ │ ├── sidebar
│ │ │ │ ├── _users_index.scss
│ │ │ │ └── _users_list.scss
│ │ │ ├── _main_app.scss
│ │ │ ├── navbar
│ │ │ │ ├── _options.scss
│ │ │ │ ├── _navbar.scss
│ │ │ │ └── _user_search.scss
│ │ │ ├── _footer.scss
│ │ │ ├── _login_page.scss
│ │ │ └── _sign_up_page.scss
│ │ │ └── main.scss
│ ├── favicon.ico
│ ├── images
│ │ ├── icons
│ │ │ ├── ephraim.png
│ │ │ ├── github.png
│ │ │ └── linkedin.png
│ │ ├── avatar_placeholder.png
│ │ └── chatterbox_logo_angelic_version_by_spartasaurus.png
│ ├── page_not_found.html
│ └── index.html
├── api
│ ├── utilities
│ │ ├── __init__.py
│ │ └── controller_utilities.py
│ ├── controllers
│ │ ├── json
│ │ │ ├── __init__.py
│ │ │ └── user_controller.py
│ │ ├── __init__.py
│ │ ├── static_pages_controller.py
│ │ ├── application_controller.py
│ │ ├── socket_controller.py
│ │ └── session_controller.py
│ ├── models
│ │ ├── validations
│ │ │ ├── __init__.py
│ │ │ └── custom_validations.py
│ │ ├── __init__.py
│ │ ├── forms
│ │ │ ├── __init__.py
│ │ │ ├── login_form.py
│ │ │ └── registration_form.py
│ │ ├── message.py
│ │ ├── session.py
│ │ └── user.py
│ └── __init__.py
└── __init__.py
├── run_app
├── .gitignore
├── run_websockets
├── requirements.txt
├── webpack.config.js
├── package.json
└── readme.md
/Procfile:
--------------------------------------------------------------------------------
1 | ./run_app
2 |
--------------------------------------------------------------------------------
/app/src/components/sidebar/chat_rooms_list.jsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/modules/_mixins.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/api/utilities/__init__.py:
--------------------------------------------------------------------------------
1 | from controller_utilities import *
2 |
--------------------------------------------------------------------------------
/app/api/controllers/json/__init__.py:
--------------------------------------------------------------------------------
1 | from user_controller import *
2 |
--------------------------------------------------------------------------------
/app/api/models/validations/__init__.py:
--------------------------------------------------------------------------------
1 | from custom_validations import *
2 |
--------------------------------------------------------------------------------
/app/api/models/__init__.py:
--------------------------------------------------------------------------------
1 | from user import User
2 | from message import Message
3 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_links.scss:
--------------------------------------------------------------------------------
1 | a {
2 | cursor: pointer;
3 | }
4 |
--------------------------------------------------------------------------------
/app/api/__init__.py:
--------------------------------------------------------------------------------
1 | from app.api.controllers import *
2 | from app.api.models import *
3 |
--------------------------------------------------------------------------------
/app/api/models/forms/__init__.py:
--------------------------------------------------------------------------------
1 | from registration_form import *
2 | from login_form import *
3 |
--------------------------------------------------------------------------------
/app/src/constants/search_constants.js:
--------------------------------------------------------------------------------
1 | export default {
2 | RECEIVE_USERS: "RECEIVE_USERS"
3 | };
4 |
--------------------------------------------------------------------------------
/app/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/favicon.ico
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/modules/_all.scss:
--------------------------------------------------------------------------------
1 | @import "clearfix";
2 | @import "mixins";
3 | @import "variables";
4 |
--------------------------------------------------------------------------------
/run_app:
--------------------------------------------------------------------------------
1 | #!venv/bin/python
2 |
3 | from app import app
4 |
5 | if __name__ == '__main__':
6 | app.run(debug=True)
7 |
--------------------------------------------------------------------------------
/app/api/utilities/controller_utilities.py:
--------------------------------------------------------------------------------
1 | def build_user_response_object(user):
2 | return { "username": user.username }
3 |
--------------------------------------------------------------------------------
/app/static/images/icons/ephraim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/icons/ephraim.png
--------------------------------------------------------------------------------
/app/static/images/icons/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/icons/github.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | tmp/
3 | venv/
4 | manage.py
5 | .DS_Store
6 | node_modules
7 | .sass-cache
8 | *.log
9 | *.yaml
10 |
--------------------------------------------------------------------------------
/app/static/images/icons/linkedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/icons/linkedin.png
--------------------------------------------------------------------------------
/app/static/images/avatar_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/avatar_placeholder.png
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/sidebar/_users_index.scss:
--------------------------------------------------------------------------------
1 | .users-index {
2 | color: white;
3 | text-align: center;
4 | }
5 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/modules/_clearfix.scss:
--------------------------------------------------------------------------------
1 | .group:after {
2 | content: "";
3 | display: block;
4 | clear: both;
5 | }
6 |
--------------------------------------------------------------------------------
/run_websockets:
--------------------------------------------------------------------------------
1 | #!venv/bin/python
2 |
3 | from app import app, socketio
4 |
5 | if __name__ == '__main__':
6 | socketio.run(app)
7 |
--------------------------------------------------------------------------------
/app/src/dispatcher/dispatcher.js:
--------------------------------------------------------------------------------
1 | import { Dispatcher } from 'flux';
2 | const AppDispatcher = new Dispatcher();
3 | export default AppDispatcher;
4 |
--------------------------------------------------------------------------------
/app/src/constants/current_user_constants.js:
--------------------------------------------------------------------------------
1 | export default {
2 | RECEIVE_CURRENT_USER: "RECEIVE_CURRENT_USER",
3 | LOG_OUT_CURRENT_USER: "LOG_OUT_CURRENT_USER"
4 | };
5 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/_main_app.scss:
--------------------------------------------------------------------------------
1 | #wrapper {
2 | min-height: 100vh;
3 | position:relative;
4 | }
5 |
6 | #content {
7 | padding-bottom: 73px;
8 | }
9 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_icons.scss:
--------------------------------------------------------------------------------
1 | .social-media-icon {
2 | width: $icon-width;
3 | height: $icon-height;
4 | border-radius: $icon-border-radius;
5 | }
6 |
--------------------------------------------------------------------------------
/app/api/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | from application_controller import *
2 | from session_controller import *
3 | from static_pages_controller import *
4 | from socket_controller import *
5 |
--------------------------------------------------------------------------------
/app/static/images/chatterbox_logo_angelic_version_by_spartasaurus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/chatterbox_logo_angelic_version_by_spartasaurus.png
--------------------------------------------------------------------------------
/app/src/constants/user_constants.js:
--------------------------------------------------------------------------------
1 | export default {
2 | RECEIVE_USERS: "RECEIVE_USERS",
3 | RECEIVE_SINGLE_USER: "RECEIVE_SINGLE_USER",
4 | RECEIVE_LOGGED_IN_USERS: "RECEIVE_LOGGED_IN_USERS"
5 | };
6 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_headers.scss:
--------------------------------------------------------------------------------
1 | h1 {
2 | font-size: $h1-font-size;
3 | font-weight: $bold;
4 | }
5 |
6 | h2 {
7 | font-size: $h2-font-size;
8 | font-weight: $bold;
9 | }
10 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_base.scss:
--------------------------------------------------------------------------------
1 | @import "./body";
2 | @import "./buttons";
3 | @import "./headers";
4 | @import "./flash";
5 | @import "./icons";
6 | @import "./input_box";
7 | @import "./links";
8 |
--------------------------------------------------------------------------------
/app/static/page_not_found.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | You must be in the wrong place because... PAGE NOT FOUND.
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/sidebar/_users_list.scss:
--------------------------------------------------------------------------------
1 | .users-list {
2 | background: $users-list-background;
3 | height: $users-list-height;
4 | width: $users-list-width;
5 | opacity: $users-list-opacity;
6 | }
7 |
8 | @import 'users_index'
9 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_body.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: $base-font-family;
3 | font-weight: $base-font-weight;
4 | font-size: $base-font-size;
5 | line-height: $base-line-height;
6 | background: $body-bg;
7 | height: 100%;
8 | }
9 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/navbar/_options.scss:
--------------------------------------------------------------------------------
1 | .options {
2 | .options-list {
3 | display: none;
4 | position: absolute;
5 | cursor: pointer;
6 | }
7 | }
8 |
9 | .options:hover {
10 | .options-list {
11 | display: block;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/utilities/flash.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | let displayFlashMessage = function(message) {
4 | $('#flash').text(message);
5 |
6 | $('#flash').delay(500).fadeIn('normal', function() {
7 | $(this).delay(2500).fadeOut();
8 | });
9 | };
10 |
11 | export { displayFlashMessage };
12 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_input_box.scss:
--------------------------------------------------------------------------------
1 | input {
2 | padding: $input-box-padding;
3 | border-radius: $input-box-border-radius;
4 | font-size: $base-font-size;
5 | }
6 |
7 | input.invalid {
8 | border: $failed-validation-input-border;
9 | box-shadow: $failed-validation-box-shadow;
10 | }
11 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_flash.scss:
--------------------------------------------------------------------------------
1 | #flash {
2 | display: none;
3 | position: absolute;
4 | top: $flash-top;
5 | left: $flash-left;
6 | font-size: $flash-font-size;
7 | border: $flash-border;
8 | border-radius: $flash-border-radius;
9 | background: $flash-background-color;
10 | padding: $flash-padding;
11 | z-index: $flash-z-index;
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/components/friends/friends_index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class FriendsIndex extends React.Component {
4 | constructor (props, context) {
5 | super(props, context);
6 | }
7 |
8 | render () {
9 | return (
10 |
11 |
12 | );
13 | }
14 | }
15 |
16 | export default FriendsIndex;
17 |
--------------------------------------------------------------------------------
/app/src/components/nav-bar/notifications.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Notifications extends React.Component {
4 | constructor (props, context) {
5 | super(props, context);
6 | }
7 |
8 | render () {
9 | return (
10 | Notifications
11 |
12 | );
13 | }
14 | }
15 |
16 | export default Notifications;
17 |
--------------------------------------------------------------------------------
/app/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chatterbox
5 |
6 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/components/user/search/users_search_index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class UsersSearchIndex extends React.Component {
4 | constructor (props, context) {
5 | super(props, context);
6 | }
7 |
8 | render () {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export default UsersSearchIndex;
18 |
--------------------------------------------------------------------------------
/app/api/models/message.py:
--------------------------------------------------------------------------------
1 | from flask import url_for
2 | from app import db
3 | import datetime
4 |
5 | class Message(db.EmbeddedDocument):
6 | created_at = db.DateTimeField(default=datetime.datetime.now, required=True)
7 | sender = db.StringField(max_length=255, required=True)
8 | receiver = db.StringField(max_length=255, required=True)
9 | body = db.StringField(max_length=255, required=True)
10 |
--------------------------------------------------------------------------------
/app/src/actions/search_actions.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/dispatcher.js';
2 | import SearchConstants from "../constants/search_constants.js";
3 |
4 | export default new class {
5 | receiveUsers (users, isAutoCompleteSelection) {
6 | AppDispatcher.dispatch({
7 | actionType: SearchConstants.RECEIVE_USERS,
8 | users: users,
9 | isAutoCompleteSelection: isAutoCompleteSelection
10 | });
11 | }
12 | }();
13 |
--------------------------------------------------------------------------------
/app/src/components/friends/friends_list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FriendsIndex from "./friends_index.jsx";
3 |
4 | class FriendsList extends React.Component {
5 | constructor (props, context) {
6 | super(props, context);
7 | }
8 |
9 | render () {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 | }
17 |
18 | export default FriendsList;
19 |
--------------------------------------------------------------------------------
/app/src/components/user/users_index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class UsersIndex extends React.Component {
4 | constructor (props, context) {
5 | super(props, context);
6 | }
7 |
8 | render () {
9 | const users = this.props.users.map((user, idx) => { user });
10 |
11 | return (
12 |
15 | );
16 | }
17 | }
18 |
19 | export default UsersIndex;
20 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/main.scss:
--------------------------------------------------------------------------------
1 | // utilities (clearfix, mixins, variables)
2 | @import "./modules/all";
3 |
4 | // reset css
5 | @import "./partials/reset";
6 |
7 | // base
8 | @import "./partials/base";
9 |
10 | // components
11 | @import "./components/footer";
12 | @import "./components/sidebar/users_list";
13 | @import "./components/login_page";
14 | @import "./components/main_app";
15 | @import "./components/navbar/navbar";
16 | @import "./components/sign_up_page";
17 |
--------------------------------------------------------------------------------
/app/api/models/forms/login_form.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form, StringField, PasswordField, validators
2 | from app.api.models.validations import check_if_username_exists, validate_user_credentials
3 |
4 | class LoginForm(Form):
5 | username = StringField('username', [
6 | validators.Required(),
7 | check_if_username_exists
8 | ])
9 | password = PasswordField('password', [
10 | validators.Required(),
11 | validate_user_credentials
12 | ])
13 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | import eventlet
2 | from flask import Flask
3 | from flask.ext.mongoengine import MongoEngine
4 | from flask.ext.socketio import SocketIO
5 |
6 | app = Flask(__name__, static_url_path="", static_folder="static")
7 | socketio = SocketIO(app)
8 |
9 | app.config["MONGODB_SETTINGS"] = {'DB': "chatterbox_test"}
10 | app.config["SECRET_KEY"] = "whatitdo"
11 |
12 | db = MongoEngine(app)
13 |
14 | eventlet.monkey_patch()
15 |
16 | from app.api.controllers import *
17 | from app.api.controllers.json import *
18 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2015.11.20.1
2 | enum34==1.1.2
3 | eventlet==0.18.1
4 | Flask==0.10.1
5 | flask-mongoengine==0.7.4
6 | Flask-Script==2.0.5
7 | Flask-SocketIO==2.0
8 | Flask-WTF==0.12
9 | greenlet==0.4.9
10 | itsdangerous==0.24
11 | Jinja2==2.8
12 | MarkupSafe==0.23
13 | mongoengine==0.10.5
14 | pathlib==1.0.1
15 | py-bcrypt==0.4
16 | pymongo==3.2
17 | python-engineio==0.8.7
18 | python-socketio==1.0
19 | simplejson==3.8.1
20 | six==1.10.0
21 | urllib3==1.13.1
22 | Werkzeug==0.11.3
23 | wheel==0.24.0
24 | WTForms==2.1
25 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_reset.scss:
--------------------------------------------------------------------------------
1 | html, body, h1, h2, h3, div, footer, ul, li, a, figure, button, textarea, form, label {
2 | padding: 0;
3 | border: 0;
4 | margin: 0;
5 |
6 | font: inherit;
7 | vertical-align: inherit;
8 | text-align: inherit;
9 | text-decoration: inherit;
10 | color: inherit;
11 |
12 | background: transparent
13 | }
14 |
15 | ul {
16 | list-style: none;
17 | }
18 |
19 | input, textarea {
20 | outline: 0;
21 | }
22 |
23 | img {
24 | display: block;
25 | width: 100%;
26 | height: auto;
27 | }
28 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/navbar/_navbar.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | background: $header-background;
3 | border-bottom: $header-bottom-border;
4 |
5 | .nav-bar {
6 | width: $nav-bar-width;
7 | margin: auto;
8 | display: flex;
9 | align-items: center;
10 | justify-content: space-between;
11 |
12 | .logo {
13 | width: $logo-dimension;
14 | height: $logo-dimension;
15 | cursor: pointer;
16 | }
17 | }
18 | }
19 |
20 | .header.not-visible {
21 | display: none;
22 | }
23 |
24 | @import "user_search";
25 | @import "options";
26 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/partials/_buttons.scss:
--------------------------------------------------------------------------------
1 | button {
2 | padding: $button-padding;
3 | background: $button-background;
4 | font-size: $button-font-size;
5 | border: $button-border;
6 | border-radius: $button-border-radius;
7 | text-align: center;
8 | cursor: pointer;
9 | }
10 |
11 | button:focus {
12 | outline: 0;
13 | }
14 |
15 | button:active, button.disabled, button.pressed {
16 | text-shadow: 1px 1px 2px black;
17 | box-shadow: inset 0 0 0 1px #27496d, inset 0 5px 30px #193047;
18 | }
19 |
20 | button:hover {
21 | background: darken($button-background, 10%);
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/actions/current_user_actions.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/dispatcher.js';
2 | import CurrentUserConstants from "../constants/current_user_constants.js";
3 |
4 | export default new class {
5 | receiveCurrentUser (currentUser) {
6 | AppDispatcher.dispatch({
7 | actionType: CurrentUserConstants.RECEIVE_CURRENT_USER,
8 | currentUser: currentUser
9 | });
10 | }
11 |
12 | logoutCurrentUser (currentUser) {
13 | AppDispatcher.dispatch({
14 | actionType: CurrentUserConstants.LOG_OUT_CURRENT_USER,
15 | currentUser: currentUser
16 | });
17 | }
18 | }();
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: './app/src/main.js',
3 | output: {
4 | path: './app/static/javascripts',
5 | filename: 'bundle.js'
6 | },
7 | module: {
8 | loaders: [{
9 | test: /\.js$|\.jsx$/,
10 | exclude: /node_modules/,
11 | loader: 'babel',
12 | query: {
13 | presets: [ 'es2015', 'react', 'stage-0' ]
14 | }
15 | }, {
16 | test: /\.scss$/,
17 | exclude: /node_modules/,
18 | loaders: ["style", "css", "sass"]
19 | }]
20 | },
21 | devtool: 'source-maps',
22 | resolve: {
23 | extensions: ['', '.js', '.jsx']
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/app/api/models/forms/registration_form.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form, StringField, PasswordField, validators
2 | from app.api.models.validations import check_if_username_taken
3 |
4 | class RegistrationForm(Form):
5 | username = StringField('username', [
6 | validators.Required(),
7 | validators.Length(min=4, max=25),
8 | check_if_username_taken
9 | ])
10 | password = PasswordField('password', [
11 | validators.Required(),
12 | validators.EqualTo('confirm', message='Passwords must match'),
13 | validators.Length(min=4, max=25)
14 | ])
15 | confirm = PasswordField('confirm')
16 |
--------------------------------------------------------------------------------
/app/src/actions/user_actions.js:
--------------------------------------------------------------------------------
1 | import UserConstants from "../constants/user_constants.js";
2 | import AppDispatcher from '../dispatcher/dispatcher.js';
3 |
4 | export default new class {
5 | receiveUsers (users) {
6 | AppDispatcher.dispatch({
7 | actionType: UserConstants.RECEIVE_USERS,
8 | users: users
9 | });
10 | }
11 |
12 | receiveSingleUser (user) {
13 | AppDispatcher.dispatch({
14 | actionType: UserConstants.RECEIVE_SINGLE_USER,
15 | user: user
16 | });
17 | }
18 |
19 | receiveLoggedInUsers (users) {
20 | AppDispatcher.dispatch({
21 | actionType: UserConstants.RECEIVE_LOGGED_IN_USERS,
22 | users: users
23 | });
24 | }
25 |
26 | }();
27 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/_footer.scss:
--------------------------------------------------------------------------------
1 | #footer-wrapper {
2 | background: $footer-bg;
3 | border-top: $standard-border;
4 | height: $footer-height;
5 | position: absolute;
6 | width: 100%;
7 | bottom: 0;
8 | left: 0;
9 |
10 | .footer {
11 | width: $footer-width;
12 | margin: $footer-margin;
13 | padding: $footer-padding;
14 | font-size: $footer-font-size;
15 | color: $footer-font-color;
16 |
17 | .about {
18 | margin-top: $about-me-margin-top;
19 | opacity: $about-me-opacity;
20 | float: left;
21 | }
22 |
23 | .links {
24 | float: right;
25 |
26 | a {
27 | margin-left: $icon-list-margin;
28 | display: inline-block;
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/api/controllers/static_pages_controller.py:
--------------------------------------------------------------------------------
1 | from app import app
2 | from flask import send_from_directory
3 |
4 | @app.errorhandler(404)
5 | def page_not_found(error):
6 | return send_from_directory(app.static_folder, 'page_not_found.html'), 404
7 |
8 | @app.after_request
9 | def add_header(response):
10 | response.headers['X-UA-Compatible'] = 'IE=Edge,chrome=1'
11 | response.headers['Cache-Control'] = 'public, max-age=0'
12 | return response
13 |
14 | @app.route("/")
15 | @app.route("/users/new")
16 | @app.route("/users/edit")
17 | def index():
18 | return send_from_directory(app.static_folder, "index.html")
19 |
20 | @app.route("/users/")
21 | def show_user_profile(username):
22 | return send_from_directory(app.static_folder, "index.html")
23 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/_login_page.scss:
--------------------------------------------------------------------------------
1 | .login-page {
2 | h1, h2 { text-align: center; }
3 |
4 | .login-form {
5 | width: $login-form-width;
6 | margin: auto;
7 |
8 | .login-form-wrapper {
9 | button, label, input {
10 | margin: $login-form-item-margin;
11 | }
12 |
13 | a {
14 | text-align: center;
15 | }
16 |
17 | a:hover {
18 | color: blue;
19 | text-decoration: underline;
20 | }
21 |
22 | label {
23 | text-align: center;
24 | }
25 |
26 | input {
27 | width: 100%;
28 | }
29 |
30 | display: flex;
31 | flex-direction: column;
32 | justify-content: center;
33 | margin: $login-form-margins;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/api/controllers/application_controller.py:
--------------------------------------------------------------------------------
1 | from flask import session
2 | from app.api.models import User
3 | import pdb
4 |
5 | def current_user(cookie):
6 | if not session:
7 | return None
8 | else:
9 | for token in session:
10 | if cookie == token and User.find_by_session_token(token):
11 | return User.find_by_session_token(token)[0]
12 | return None
13 |
14 | def login(user):
15 | session[user.reset_session_token()] = user.username
16 |
17 | def logout(cookie):
18 | current_user(cookie).reset_session_token()
19 | session.pop(cookie, None)
20 |
21 | def logged_in_users():
22 | users = []
23 |
24 | for token in session:
25 | user = User.find_by_session_token(token)
26 | if user:
27 | users.append(user[0].username)
28 |
29 | return users
30 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/_sign_up_page.scss:
--------------------------------------------------------------------------------
1 | .sign-up-page {
2 | h1, h2 { text-align: center; }
3 |
4 | .sign-up-form {
5 | width: $sign-up-form-width;
6 | margin: auto;
7 |
8 | .sign-up-form-wrapper {
9 | * {
10 | margin: $sign-up-form-item-margin;
11 | }
12 |
13 | label, a {
14 | text-align: center;
15 | }
16 |
17 | a:hover {
18 | color: blue;
19 | text-decoration: underline;
20 | }
21 |
22 | input {
23 | width: 100%;
24 | }
25 |
26 | .form-avatar-preview {
27 | width: $avatar-preview-width;
28 | height: $avatar-preview-height;
29 | }
30 |
31 | display: flex;
32 | flex-direction: column;
33 | justify-content: center;
34 | margin: $sign-up-form-margins;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/api/models/validations/custom_validations.py:
--------------------------------------------------------------------------------
1 | from app.api.models import User
2 | from wtforms import ValidationError
3 | import pdb
4 |
5 | def check_if_username_taken(form, field):
6 | message = "Username already taken"
7 |
8 | if User.find_by_username(form.username.data).count() > 0:
9 | raise ValidationError(message)
10 |
11 | def check_if_username_exists(form, field):
12 | message = "Username not found"
13 |
14 | if User.find_by_username(form.username.data).count() == 0:
15 | raise ValidationError(message)
16 |
17 | def validate_user_credentials(form, field):
18 | if User.find_by_username(form.username.data).count() > 0:
19 | user = User.find_by_username(form.username.data)[0]
20 | else:
21 | return
22 |
23 | if not User.validate_user_credentials(user, form.password.data):
24 | message = "Invalid credentials"
25 | raise ValidationError(message)
26 |
--------------------------------------------------------------------------------
/app/src/components/nav-bar/options.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ApiSessionUtil from '../../apiutil/api_session_util.js';
3 |
4 | class Options extends React.Component {
5 | constructor (props, context) {
6 | super(props, context);
7 | this.logoutCurrentUser = this.logoutCurrentUser.bind(this);
8 | }
9 |
10 | logoutCurrentUser (e) {
11 | e.preventDefault();
12 |
13 | ApiSessionUtil.logout(this.props.logoutSuccess);
14 | }
15 | render () {
16 | let options = ["Update profile", "Logout"];
17 |
18 | let optionListItems = options.map( (option, idx) =>
19 | { option }
20 | );
21 |
22 | return (
23 | Options
24 |
25 | - Update Profile
26 | - Logout
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | export default Options;
34 |
--------------------------------------------------------------------------------
/app/src/utilities/auth.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | let failedAuthErrors = function (errors) {
4 | $(".submit").removeClass("disabled").prop("disabled", false);
5 |
6 | let [usernameErrors, passwordErrors] = [[], []];
7 |
8 | errors.forEach (function (err) {
9 | switch (err[0]) {
10 | case "username":
11 | usernameErrors.push(err[1][0]);
12 | $(".form-username-input").addClass("invalid");
13 | break;
14 | case "password":
15 | passwordErrors.push(err[1][0]);
16 | if (!$(".login-form-password-input").hasClass("invalid")) {
17 | $(".form-password-input").addClass("invalid");
18 | }
19 | break;
20 | }
21 | });
22 |
23 | return [usernameErrors, passwordErrors];
24 | };
25 |
26 | let removeInvalidClass = function (className) {
27 | if ($(`.${ className }`).hasClass("invalid")) {
28 | $(`.${ className }`).removeClass("invalid");
29 | }
30 | };
31 |
32 | export { failedAuthErrors, removeInvalidClass };
33 |
--------------------------------------------------------------------------------
/app/api/models/session.py:
--------------------------------------------------------------------------------
1 | from app import db
2 | import uuid
3 |
4 | class Session(db.Document):
5 | username = db.StringField(max_length=255, required=True)
6 | session_token = db.StringField(max_length=255, required=True)
7 |
8 | @classmethod
9 | def generate_session_token(cls):
10 | return str(uuid.uuid1())
11 |
12 | @classmethod
13 | def find_by_session_token(cls, session_token):
14 | return User.objects(session_token = session_token)
15 |
16 | @classmethod
17 | def destroy(cls, session):
18 | if session.delete():
19 | return True
20 | else:
21 | return False
22 |
23 | def reset_session_token(self):
24 | self.session_token = Session.generate_session_token()
25 | self.save()
26 | return self.session_token
27 |
28 | def __repr__(self):
29 | return ''.format(self.session_token, self.username)
30 |
31 | meta = {
32 | 'indexes': ['session_token']
33 | }
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatterbox",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "run_app",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "./run_app"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-core": "^6.3.26",
14 | "babel-loader": "^6.2.1",
15 | "babel-preset-es2015": "^6.3.13",
16 | "babel-preset-react": "^6.3.13",
17 | "babel-preset-stage-0": "^6.3.13",
18 | "css-loader": "^0.23.1",
19 | "eventemitter3": "^1.1.1",
20 | "file-loader": "^0.8.5",
21 | "flux": "^2.1.1",
22 | "history": "^1.17.0",
23 | "jquery": "^2.1.4",
24 | "node-sass": "^3.4.2",
25 | "react": "^0.14.5",
26 | "react-dom": "^0.14.5",
27 | "react-router": "^2.0.0-rc5",
28 | "sass-loader": "^3.1.2",
29 | "socket.io-client": "^1.4.5",
30 | "style-loader": "^0.13.0",
31 | "webpack": "^1.12.9"
32 | },
33 | "jshintConfig": {
34 | "esnext": true
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Chatterbox
2 |
3 | WIP - Live Site coming
4 |
5 | ## Summary
6 |
7 | Chatterbox is a slack-like chat app which uses the latest technologies such as WebSockets to enable the chat feature.
8 |
9 | ### Languages
10 |
11 | * Python
12 | * JavaScript
13 | * HTML
14 | * SASS/CSS
15 |
16 | ### Frameworks
17 |
18 | * Flask
19 | * React and Flux
20 |
21 | ### Libraries and Technologies
22 |
23 | * Node technologies
24 | + Webpack
25 | + jQuery
26 | + ReactRouter
27 | * Python technologies
28 | + MongoEngine
29 | + BCrypt
30 | + Flask-Script
31 | + Flask-WTForms
32 | + Flask-SocketIO
33 | * DB technologies
34 | + MongoDB
35 |
36 | ### App features
37 |
38 | - Incorporates latest JavaScript ES6 features and syntax
39 | - Webpack used to manage JavaScript module dependencies and to load ES6 and SASS files
40 | - Logging in establishes client to server WebSocket connection
41 | - Global chat room and private, peer to peer, chat rooms supported
42 | - Stores user and chat messages in MongoDB (user credentials hashed with BCrypt)
43 |
--------------------------------------------------------------------------------
/app/src/components/main/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Footer extends React.Component {
4 | constructor () {
5 | super();
6 | }
7 |
8 | render () {
9 | return (
10 |
11 |
12 |
13 |
30 |
31 | );
32 | }
33 | }
34 |
35 | export default Footer;
36 |
--------------------------------------------------------------------------------
/app/src/components/nav-bar/nav_bar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import UserSearch from './user_search.jsx';
3 | import Notifications from './notifications.jsx';
4 | import Options from './options.jsx';
5 | import { displayFlashMessage } from '../../utilities/flash.js';
6 |
7 | class NavBar extends React.Component {
8 | constructor (props, context) {
9 | super(props, context);
10 | this.logoutSuccess = this.logoutSuccess.bind(this);
11 | }
12 |
13 | logoutSuccess (message) {
14 | displayFlashMessage(message);
15 | }
16 |
17 | render () {
18 | return (
19 |
20 |
21 |

23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | export default NavBar;
33 |
--------------------------------------------------------------------------------
/app/src/components/chat/chat_room.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class ChatRoom extends React.Component {
4 | constructor (props, context) {
5 | super(props, contextTypes);
6 | this.changeMessage = this.changeMessage.bind(this);
7 | this.handleKeyPress = tihs.handleKeyPress.bind(this);
8 | this.handleMessageSubmission = this.handleMessageSubmission.bind(this);
9 | }
10 |
11 | handleKeyPress (e) {
12 | if (e.charCode === 13) { this.handleMessageSubmission(); }
13 | }
14 |
15 | handleMessageSubmission (e) {
16 | socket.emit('global chat request', { data: e.currentTarget.value });
17 | }
18 |
19 | render () {
20 | return (
21 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/api/controllers/socket_controller.py:
--------------------------------------------------------------------------------
1 | from flask import request
2 | # from flask import session, jsonify
3 | from flask.ext.socketio import emit, join_room, leave_room, close_room, rooms, disconnect
4 | from app import app, socketio
5 | from app.api.controllers import application_controller
6 | import pdb
7 |
8 | @socketio.on('private chat request', namespace='/chat')
9 | def private_message(message):
10 | emit('private chat response', {'data': message['data']})
11 |
12 | @socketio.on('global chat request', namespace='/chat')
13 | def global_message(message):
14 | emit('global chat response', {'data': message['data']}, broadcast=True)
15 |
16 | @socketio.on('connect', namespace='/chat')
17 | def connect():
18 | username = request.args.get('username')
19 | emit('a user connected', { 'broadcast': "{0} connected".format(username),
20 | 'username': username}, broadcast=True)
21 |
22 | @socketio.on('disconnect', namespace='/chat')
23 | def disconnect():
24 | username = request.args.get('username')
25 | emit('a user disconnected', { 'broadcast': "{0} disconnected".format(username),
26 | 'username': username}, broadcast=True)
27 | print('Client disconnected', request.sid)
28 |
--------------------------------------------------------------------------------
/app/src/stores/user_autocomplete_store.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/dispatcher.js';
2 | import SearchConstants from "../constants/search_constants.js";
3 | import EventEmitter from 'eventemitter3';
4 |
5 | const CHANGE_EVENT = "change";
6 |
7 | class UserSearchAutoCompleteStore extends EventEmitter {
8 | constructor () {
9 | super();
10 | this.users = [];
11 | }
12 |
13 | addChangeListener (callback) {
14 | this.on(CHANGE_EVENT, callback);
15 | }
16 |
17 | removeChangeListener (callback) {
18 | this.removeListener(CHANGE_EVENT, callback);
19 | }
20 |
21 | get () {
22 | return this.users.slice();
23 | }
24 |
25 | set (users) {
26 | this.users = users;
27 | }
28 | }
29 |
30 | const userSearchAutoCompleteStore = new UserSearchAutoCompleteStore();
31 |
32 | AppDispatcher.register(function (payload) {
33 | switch (payload.actionType) {
34 | case SearchConstants.RECEIVE_USERS:
35 | userSearchAutoCompleteStore.set(payload.users);
36 |
37 | if (!payload.isAutoCompleteSelection) {
38 | userSearchAutoCompleteStore.emit(CHANGE_EVENT);
39 | }
40 | break;
41 | }
42 | });
43 |
44 | export default userSearchAutoCompleteStore;
45 |
--------------------------------------------------------------------------------
/app/src/apiutil/api_session_util.js:
--------------------------------------------------------------------------------
1 | import $ from "jquery";
2 | import CurrentUserActions from "../actions/current_user_actions.js";
3 |
4 | class ApiSessionUtil {
5 | login(formData, success, failure) {
6 | const receiveCurrentUser = (data) => {
7 | CurrentUserActions.receiveCurrentUser(data.user);
8 | success(data.message, data.user.username);
9 | };
10 |
11 | const receiveErrors = (data) => {
12 | failure(data.responseJSON.errors);
13 | };
14 |
15 | $.ajax({
16 | url: "/session",
17 | method: "POST",
18 | processData: false,
19 | contentType: false,
20 | dataType: "json",
21 | data: formData
22 | }).done(receiveCurrentUser).fail(receiveErrors);
23 | }
24 |
25 | logout(success) {
26 | $.ajax({
27 | url: '/session',
28 | method: 'DELETE',
29 | dataType: 'json',
30 | success: (data) => {
31 | CurrentUserActions.logoutCurrentUser(data.user);
32 | success(data.message);
33 | }
34 | });
35 | }
36 |
37 | fetchCurrentUser() {
38 | const receiveCurrentUser = (data) => CurrentUserActions.receiveCurrentUser(data.user);
39 |
40 | $.get('/session', receiveCurrentUser);
41 | }
42 | }
43 |
44 | export default new ApiSessionUtil();
45 |
--------------------------------------------------------------------------------
/app/src/components/user/search/users_search_results_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import currentUserStore from '../../../stores/current_user_store.js';
3 | import NavBar from '../../nav-bar/nav_bar.jsx';
4 | import UsersSearchIndex from './users_search_index.jsx';
5 |
6 | class UsersSearchResultsPage extends React.Component {
7 | constructor (props, context) {
8 | super(props, context);
9 | this._ensureLoggedIn = this.__ensureLoggedIn.bind(this);
10 | }
11 |
12 | static contextTypes = {
13 | router: React.PropTypes.object.isRequired
14 | }
15 |
16 | componentWillMount () {
17 | this._ensureLoggedIn();
18 | }
19 |
20 | componentDidMount () {
21 | currentUserStore.addChangeListener(this._ensureLoggedIn);
22 | }
23 |
24 | componentWillUnmount () {
25 | currentUserStore.removeChangeListener(this._ensureLoggedIn);
26 | }
27 |
28 | __ensureLoggedIn () {
29 | if (!currentUserStore.isLoggedIn()) { this.context.router.push('/'); }
30 | }
31 |
32 | render () {
33 | return (
34 |
39 | );
40 | }
41 | }
42 |
43 | export default UsersSearchResultsPage;
44 |
--------------------------------------------------------------------------------
/app/src/components/sidebar/users_list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import UsersIndex from '../user/users_index.jsx';
3 | import ApiUserUtil from '../../apiutil/api_user_util.js';
4 | import currentUserStore from '../../stores/current_user_store.js';
5 | import loggedInUsersStore from '../../stores/logged_in_users_store.js';
6 |
7 | class UsersList extends React.Component {
8 | constructor (props, context) {
9 | super(props, context);
10 | this._onChange = this._onChange.bind(this);
11 | this.state = { loggedInUsers: loggedInUsersStore.get() };
12 | }
13 |
14 | componentWillMount () {
15 | const username = currentUserStore.get().username;
16 | ApiUserUtil.fetchUsers(username, "loggedinusers");
17 | }
18 |
19 | componentDidMount () {
20 | loggedInUsersStore.addChangeListener(this._onChange);
21 | }
22 |
23 | componentWillUnmount () {
24 | loggedInUsersStore.removeChangeListener(this._onChange);
25 | }
26 |
27 | _onChange () {
28 | this.setState({ loggedInUsers: loggedInUsersStore.get() });
29 | }
30 |
31 | render () {
32 | console.log(this.state.loggedInUsers);
33 | return (
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default UsersList;
42 |
--------------------------------------------------------------------------------
/app/src/main.js:
--------------------------------------------------------------------------------
1 | // main sass file
2 | require("../static/stylesheets/sass/main.scss");
3 |
4 | // core modules
5 | import $ from 'jquery';
6 | import React from 'react';
7 | import { render } from 'react-dom';
8 | import { Router, Route, IndexRoute, browserHistory } from 'react-router';
9 |
10 | // components
11 | import ChatterboxApp from './components/main/chatterbox_app.jsx';
12 | import Footer from './components/main/footer.jsx';
13 | import LoginPage from './components/auth/login_page.jsx';
14 | import SignUpPage from './components/auth/sign_up_page.jsx';
15 | import UserHomePage from './components/user/user_home_page.jsx';
16 | import UsersSearchResultsPage from './components/user/search/users_search_results_page.jsx';
17 |
18 | $(document).ready(function () {
19 | const routes = (
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 |
28 | render(,
29 | document.getElementById('content'));
30 | render(, document.getElementById('footer-wrapper'));
31 | });
32 |
--------------------------------------------------------------------------------
/app/src/apiutil/api_user_util.js:
--------------------------------------------------------------------------------
1 | import $ from "jquery";
2 | import CurrentUserActions from "../actions/current_user_actions.js";
3 | import UsersActions from "../actions/user_actions.js";
4 | import SearchActions from "../actions/search_actions.js";
5 |
6 | class ApiUserUtil {
7 | create (formData, success, failure) {
8 | const receiveCurrentUser = (data) => {
9 | CurrentUserActions.receiveCurrentUser(data.user);
10 | success(data.message, data.user.username);
11 | };
12 |
13 | const receiveError = (data) => failure(data.responseJSON.errors);
14 |
15 | $.ajax({
16 | url: "/users/api",
17 | method: "POST",
18 | processData: false,
19 | contentType: false,
20 | dataType: "json",
21 | data: formData
22 | }).done(receiveCurrentUser).fail(receiveError);
23 | }
24 |
25 | fetchUsers (username, mode) {
26 | const urlUserAutoComplete = "/users/api/" + username + "/" + mode;
27 |
28 | const isAutoCompleteSelection = mode === "autocomplete-selection" ? true : false;
29 |
30 | const receiveUsers = mode === "loggedinusers" ? (data) => UsersActions.receiveLoggedInUsers(data.users)
31 | : (data) => SearchActions.receiveUsers(data.users, isAutoCompleteSelection);
32 |
33 | $.get(urlUserAutoComplete, receiveUsers);
34 | }
35 | }
36 |
37 | export default new ApiUserUtil();
38 |
--------------------------------------------------------------------------------
/app/src/stores/current_user_store.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/dispatcher.js';
2 | import CurrentUserConstants from "../constants/current_user_constants.js";
3 | import loggedInUsersStore from "./logged_in_users_store.js";
4 | import EventEmitter from 'eventemitter3';
5 |
6 | const CHANGE_EVENT = "change";
7 |
8 | class CurrentUserStore extends EventEmitter {
9 | constructor () {
10 | super();
11 | this.currentUser = {};
12 | }
13 |
14 | addChangeListener (callback) {
15 | this.on(CHANGE_EVENT, callback);
16 | }
17 |
18 | removeChangeListener (callback) {
19 | this.removeListener(CHANGE_EVENT, callback);
20 | }
21 |
22 | get () {
23 | return Object.assign({}, this.currentUser);
24 | }
25 |
26 | isLoggedIn () {
27 | const result = Object.keys(this.currentUser).length > 0 ? true : false;
28 | return result;
29 | }
30 |
31 | set (user) {
32 | this.currentUser = user;
33 | }
34 | }
35 |
36 | const currentUserStore = new CurrentUserStore();
37 |
38 | AppDispatcher.register(function (payload) {
39 | switch (payload.actionType) {
40 | case CurrentUserConstants.RECEIVE_CURRENT_USER:
41 | currentUserStore.set(payload.currentUser);
42 | currentUserStore.emit(CHANGE_EVENT);
43 | loggedInUsersStore.add(payload.currentUser.username);
44 | break;
45 | case CurrentUserConstants.LOG_OUT_CURRENT_USER:
46 | currentUserStore.set({});
47 | currentUserStore.emit(CHANGE_EVENT);
48 | break;
49 | }
50 | });
51 |
52 | export default currentUserStore;
53 |
--------------------------------------------------------------------------------
/app/src/stores/logged_in_users_store.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/dispatcher.js';
2 | import UserConstants from "../constants/user_constants.js";
3 | import EventEmitter from 'eventemitter3';
4 |
5 | const CHANGE_EVENT = "change";
6 |
7 | class LoggedInUsersStore extends EventEmitter {
8 | constructor () {
9 | super();
10 | this.loggedInUsers = [];
11 | }
12 |
13 | addChangeListener (callback) {
14 | this.on(CHANGE_EVENT, callback);
15 | }
16 |
17 | removeChangeListener (callback) {
18 | this.removeListener(CHANGE_EVENT, callback);
19 | }
20 |
21 | get () {
22 | return this.loggedInUsers.slice();
23 | }
24 |
25 | add (username) {
26 | const findUser = this.loggedInUsers.find((el) => el === username);
27 |
28 | if (typeof findUser === 'undefined') { this.loggedInUsers.push(username); }
29 |
30 | this.emit(CHANGE_EVENT);
31 | }
32 |
33 | remove (username) {
34 | const userIdx = this.loggedInUsers.indexOf(username);
35 |
36 | if (userIdx !== -1) { this.loggedInUsers.splice(userIdx, 1);}
37 |
38 | this.emit(CHANGE_EVENT);
39 | }
40 |
41 | set (users) {
42 | debugger;
43 | this.loggedInUsers = users;
44 | }
45 | }
46 |
47 | const loggedInUsersStore = new LoggedInUsersStore();
48 |
49 | AppDispatcher.register(function (payload) {
50 | switch (payload.actionType) {
51 | case UserConstants.RECEIVE_LOGGED_IN_USERS:
52 | loggedInUsersStore.set(payload.users);
53 | loggedInUsersStore.emit(CHANGE_EVENT);
54 | break;
55 | }
56 | });
57 |
58 | export default loggedInUsersStore;
59 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/components/navbar/_user_search.scss:
--------------------------------------------------------------------------------
1 | .user-search {
2 | position: relative;
3 |
4 | input {
5 | margin: $user-search-input-margin;
6 | width: $user-search-input-width;
7 | }
8 |
9 | .user-search-submit {
10 | border: 0;
11 | transition: all .2s ease-in-out;
12 | }
13 |
14 | .user-search-submit:hover {
15 | transform: scale(1.2);
16 | }
17 |
18 | .user-search-autocomplete-list {
19 | display: none;
20 | position: absolute;
21 | width: $user-autocomplete-list-width;
22 | left: $user-autocomplete-list-left;
23 | background: $user-autocomplete-list-background;
24 | border-radius: $user-autocomplete-list-border-radius;
25 | border-left: $user-autocomplete-list-border-left;
26 | border-right: $user-autocomplete-list-border-right;
27 | border-bottom: $user-autocomplete-list-border-bottom;
28 |
29 | .user-search-autocomplete-list-item {
30 | border-top: $user-autocomplete-list-item-border-top;
31 | padding: $user-autocomplete-list-item-padding;
32 | display: block;
33 | }
34 |
35 | .user-search-autocomplete-list-item.selected {
36 | border: $selected-border;
37 | }
38 |
39 | .user-search-autocomplete-list-item.selected:first-child {
40 | border-top-left-radius: $base-border-radius;
41 | border-top-right-radius: $base-border-radius;
42 | }
43 |
44 | .user-search-autocomplete-list-item.selected:last-child {
45 | border-bottom-left-radius: $base-border-radius;
46 | border-bottom-right-radius: $base-border-radius;
47 | }
48 | }
49 |
50 | .user-search-autocomplete-list.show {
51 | display: block;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/api/models/user.py:
--------------------------------------------------------------------------------
1 | from flask import url_for
2 | from app import db
3 | import bcrypt
4 | import uuid
5 |
6 | class User(db.Document):
7 | username = db.StringField(max_length=25, required=True)
8 | password_digest = db.StringField(max_length=255, required=True)
9 | session_token = db.StringField(max_length=255, required=True)
10 |
11 | @classmethod
12 | def generate_session_token(cls):
13 | return str(uuid.uuid1())
14 |
15 | @classmethod
16 | def validate_user_credentials(cls, user, password):
17 | return user.password_digest == bcrypt.hashpw(password, user.password_digest)
18 |
19 | @classmethod
20 | def find_by_username(cls, username):
21 | return User.objects(username = username)
22 |
23 | @classmethod
24 | def find_by_session_token(cls, session_token):
25 | return User.objects(session_token = session_token)
26 |
27 | @classmethod
28 | def destroy(cls, user):
29 | if user.delete():
30 | return True
31 | else:
32 | return False
33 |
34 | def generate_password_digest(self, password):
35 | salt = bcrypt.gensalt()
36 | hash = bcrypt.hashpw(password, salt)
37 | self.password_digest = hash
38 |
39 | def reset_session_token(self):
40 | self.session_token = User.generate_session_token()
41 | self.save()
42 | return self.session_token
43 |
44 | def get_absolute_url(self):
45 | return url_for('user', kwargs={"username": self.username})
46 |
47 | def __repr__(self):
48 | return ''.format(self.username)
49 |
50 | meta = {
51 | 'allow_inheritance': True,
52 | 'indexes': ['username', 'session_token'],
53 | 'ordering': ['+username']
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/components/auth/sign_up_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SignUpForm from './sign_up_form.jsx';
3 | import { displayFlashMessage } from '../../utilities/flash.js';
4 | import { failedAuthErrors } from '../../utilities/auth.js';
5 |
6 | class SignUpPage extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.successfulSignUp = this.successfulSignUp.bind(this);
10 | this.failedSignUp = this.failedSignUp.bind(this);
11 | this.deleteUsernameErrors = this.deleteUsernameErrors.bind(this);
12 | this.deletePasswordErrors = this.deletePasswordErrors.bind(this);
13 | this.state={ usernameErrors:[], passwordErrors:[] };
14 | }
15 |
16 | static contextTypes = {
17 | router: React.PropTypes.object.isRequired
18 | }
19 |
20 | successfulSignUp (message, username) {
21 | this.context.router.push('/users/' + username);
22 |
23 | displayFlashMessage(message);
24 | }
25 |
26 | failedSignUp (errors) {
27 | let [usernameErrors, passwordErrors] = failedAuthErrors(errors);
28 |
29 | this.setState({ usernameErrors, passwordErrors });
30 | }
31 |
32 | deleteUsernameErrors () {
33 | this.setState({ usernameErrors: [] })
34 | }
35 |
36 | deletePasswordErrors () {
37 | this.setState({ passwordErrors: [] })
38 | }
39 |
40 | render () {
41 | return (
42 |
43 |
Welcome to Chatterbox!
44 | Create a new user to get going!
45 |
51 |
52 | );
53 | }
54 | }
55 |
56 | export default SignUpPage;
57 |
--------------------------------------------------------------------------------
/app/api/controllers/session_controller.py:
--------------------------------------------------------------------------------
1 | from app import app
2 | from flask import request, session, jsonify
3 | from app.api.controllers import application_controller
4 | from app.api.models import User
5 | from app.api.models.forms import LoginForm
6 | from app.api.utilities import *
7 | import pdb
8 |
9 | @app.route("/session", methods=["GET", "POST", "DELETE"])
10 | def handle_session_api_request():
11 | if request.method == "GET":
12 | return __fetch_session()
13 | elif request.method == "POST":
14 | return __create_session()
15 | elif request.method == "DELETE":
16 | return __destroy_session()
17 |
18 | def __fetch_session():
19 | cookie = request.cookies.get('chatterbox')
20 |
21 | user = application_controller.current_user(cookie)
22 |
23 | if user:
24 | user_response = build_user_response_object(user)
25 | return jsonify(user=user_response)
26 | else:
27 | return jsonify(user={})
28 |
29 | def __create_session():
30 | form = LoginForm(request.form)
31 |
32 | if form.validate():
33 | user = User.find_by_username(form.username.data)[0]
34 | print user.session_token
35 | application_controller.login(user)
36 |
37 | user_response = build_user_response_object(user)
38 |
39 | response = jsonify(user=user_response,
40 | message = "Login successful! Welcome {0}!".format(user.username))
41 | response.set_cookie('chatterbox', user.session_token)
42 |
43 | return response
44 | else:
45 | return jsonify(errors=form.errors.items()), 401
46 |
47 | def __destroy_session():
48 | cookie = request.cookies.get('chatterbox')
49 |
50 | user = application_controller.current_user(cookie)
51 |
52 | application_controller.logout(cookie)
53 |
54 | response = jsonify(user=user, message="Goodbye {0}!".format(user.username))
55 | response.set_cookie('chatterbox', '', expires=0)
56 |
57 | return response
58 |
--------------------------------------------------------------------------------
/app/src/components/main/chatterbox_app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NavBar from '../nav-bar/nav_bar.jsx';
3 | import currentUserStore from '../../stores/current_user_store.js';
4 | import ApiSessionUtil from '../../apiutil/api_session_util.js';
5 |
6 | class ChatterboxApp extends React.Component {
7 | constructor (props, context) {
8 | super(props, context);
9 | this.getStateFromStore = this.getStateFromStore.bind(this);
10 | this.navigateToSearchResultsPage = this.navigateToSearchResultsPage.bind(this);
11 | this.navigateToUserHomePage = this.navigateToUserHomePage.bind(this);
12 | this._onChange = this._onChange.bind(this);
13 | this.state = { header: this.getStateFromStore() };
14 | }
15 |
16 | static contextTypes = {
17 | router: React.PropTypes.object.isRequired
18 | }
19 |
20 | componentWillMount () {
21 | ApiSessionUtil.fetchCurrentUser();
22 | }
23 |
24 | componentDidMount () {
25 | currentUserStore.addChangeListener(this._onChange);
26 | }
27 |
28 | componentWillUnmount () {
29 | currentUserStore.removeChangeListener(this._onChange);
30 | }
31 |
32 | getStateFromStore () {
33 | let result = currentUserStore.isLoggedIn() ? true : false;
34 | return result;
35 | }
36 |
37 | navigateToUserHomePage () {
38 | this.context.router.push('/users/' + currentUserStore.get().username);
39 | }
40 |
41 | navigateToSearchResultsPage (username) {
42 | this.context.router.push({
43 | pathname: '/users/search',
44 | query: { username: username }
45 | });
46 | }
47 |
48 | _onChange () {
49 | this.setState({ header: this.getStateFromStore() });
50 | }
51 |
52 | render () {
53 | const headerClass = this.state.header ? "header" : "header not-visible";
54 |
55 | return (
56 |
57 |
61 | { this.props.children }
62 |
63 | );
64 | }
65 | }
66 |
67 | export default ChatterboxApp;
68 |
--------------------------------------------------------------------------------
/app/src/components/user/user_home_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import io from 'socket.io-client';
4 | import NavBar from '../nav-bar/nav_bar.jsx';
5 | import UsersList from '../sidebar/users_list.jsx';
6 | import currentUserStore from '../../stores/current_user_store.js';
7 | import loggedInUsersStore from '../../stores/logged_in_users_store.js';
8 |
9 | class UserHomePage extends React.Component {
10 | constructor (props, context) {
11 | super(props, context);
12 | this.addWebSocketListeners = this.addWebSocketListeners.bind(this);
13 | this._ensureLoggedIn = this._ensureLoggedIn.bind(this);
14 | this.socket = io('http://' + document.domain + ':' + location.port + '/chat',
15 | { query:
16 | { username: currentUserStore.get().username }
17 | });
18 | }
19 |
20 | static contextTypes = {
21 | router: React.PropTypes.object.isRequired
22 | }
23 |
24 | componentWillMount () {
25 | this._ensureLoggedIn();
26 | }
27 |
28 | componentDidMount () {
29 | this.addWebSocketListeners();
30 | currentUserStore.addChangeListener(this._ensureLoggedIn);
31 | }
32 |
33 | componentWillUnmount () {
34 | this.socket.emit('disconnect', { query: { username: currentUserStore.get().username } });
35 | this.socket.off();
36 | this.socket.close();
37 | currentUserStore.removeChangeListener(this._ensureLoggedIn);
38 | }
39 |
40 | addWebSocketListeners () {
41 | this.socket.on('a user connected', (data) => {
42 | console.log(data.broadcast);
43 | loggedInUsersStore.add(data.username)
44 | });
45 |
46 | this.socket.on('a user disconnected', (data) => {
47 | console.log(data.broadcast);
48 | loggedInUsersStore.remove(data.username)
49 | });
50 |
51 | this.socket.on('global chat response', (data) => {
52 | debugger;
53 | });
54 | }
55 |
56 | _ensureLoggedIn () {
57 | if (!currentUserStore.isLoggedIn()) { this.context.router.push('/'); }
58 | }
59 |
60 | render () {
61 | return (
62 |
63 |
64 |
65 | );
66 | }
67 | }
68 |
69 | export default UserHomePage;
70 |
--------------------------------------------------------------------------------
/app/src/components/auth/login_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LoginForm from './login_form.jsx';
3 | import currentUserStore from '../../stores/current_user_store.js';
4 | import { displayFlashMessage } from '../../utilities/flash.js';
5 | import { failedAuthErrors } from '../../utilities/auth.js';
6 |
7 | class LoginPage extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.successfulLogin = this.successfulLogin.bind(this);
11 | this.failedLogin = this.failedLogin.bind(this);
12 | this.deleteUsernameErrors = this.deleteUsernameErrors.bind(this);
13 | this.deletePasswordErrors = this.deletePasswordErrors.bind(this);
14 | this.__checkIfLoggedIn = this.__checkIfLoggedIn.bind(this);
15 | this.state={ usernameErrors:[], passwordErrors:[] };
16 | }
17 |
18 | static contextTypes = {
19 | router: React.PropTypes.object.isRequired
20 | }
21 |
22 | componentWillMount () {
23 | this.__checkIfLoggedIn();
24 | }
25 |
26 | componentDidMount () {
27 | currentUserStore.addChangeListener(this.__checkIfLoggedIn);
28 | }
29 |
30 | componentWillUnmount () {
31 | currentUserStore.removeChangeListener(this.__checkIfLoggedIn);
32 | }
33 |
34 | successfulLogin (message, username) {
35 | this.context.router.push('/users/' + username);
36 |
37 | displayFlashMessage(message);
38 | }
39 |
40 | failedLogin (errors) {
41 | const [usernameErrors, passwordErrors] = failedAuthErrors(errors);
42 |
43 | this.setState({ usernameErrors, passwordErrors });
44 | }
45 |
46 | deleteUsernameErrors () {
47 | this.setState({ usernameErrors: [] })
48 | }
49 |
50 | deletePasswordErrors () {
51 | this.setState({ passwordErrors: [] })
52 | }
53 |
54 | __checkIfLoggedIn() {
55 | if (currentUserStore.isLoggedIn()) {
56 | this.context.router.push('/users/' + currentUserStore.get().username);
57 | }
58 | }
59 |
60 | render () {
61 | return (
62 |
63 |
Welcome to Chatterbox!
64 | Please login to continue.
65 |
71 |
72 | );
73 | }
74 | }
75 |
76 | export default LoginPage;
77 |
--------------------------------------------------------------------------------
/app/static/stylesheets/sass/modules/_variables.scss:
--------------------------------------------------------------------------------
1 | /* font weights */
2 | $light: 100;
3 | $regular: 400;
4 | $bold: 700;
5 |
6 | /* base background */
7 | $body-bg: #eee;
8 |
9 | /* base font */
10 | $base-font-family: sans-serif;
11 | $base-font-weight: $regular;
12 | $base-font-size: 14px;
13 | $base-line-height: 1.4;
14 |
15 | /* icons */
16 | $icon-width: 32px;
17 | $icon-height: 32px;
18 | $icon-border-radius: 10px;
19 |
20 | /* borders */
21 | $standard-border: 1px solid #ccc;
22 | $selected-border: 1px solid blue;
23 | $base-border-radius: 10px;
24 |
25 | /* buttons */
26 | $button-padding: 3px;
27 | $button-font-size: 16px;
28 | $button-border: 1px solid darkgrey;
29 | $button-border-radius: 10px;
30 | $button-background: lightblue;
31 |
32 | /* headers */
33 | $h1-font-size: 36px;
34 | $h2-font-size: 24px;
35 |
36 | /* input boxes */
37 | $input-box-padding: 5px 2.5px;
38 | $input-box-border-radius: 10px;
39 | $failed-validation-input-border: 2px solid red;
40 | $failed-validation-box-shadow: 0 0 10px red;
41 |
42 | /* flash messages */
43 | $flash-top: 15vh;
44 | $flash-left: 40vw;
45 | $flash-font-size: 18px;
46 | $flash-border: $standard-border;
47 | $flash-border-radius: $base-border-radius;
48 | $flash-background-color: yellow;
49 | $flash-padding: 5px;
50 | $flash-z-index: 1;
51 |
52 | /* footer */
53 | $footer-height: 72px;
54 | $footer-font-size: 16px;
55 | $footer-font-color: #fff;
56 | $footer-width: 70vw;
57 | $footer-margin: auto;
58 | $footer-padding: 17px 0;
59 | $footer-bg: #ffab62;
60 | $about-me-opacity: .7;
61 | $about-me-margin-top: 5px;
62 | $icon-list-margin: 10px;
63 |
64 | /* login page */
65 | $login-form-item-margin: 5px 0;
66 | $login-form-width: 200px;
67 | $login-form-margins: 100px 0;
68 | $login-input-box-width: 100%;
69 |
70 | /* sign up page */
71 | $sign-up-form-item-margin: 5px 0;
72 | $sign-up-form-width: 200px;
73 | $sign-up-form-margins: 50px 0;
74 | $sign-up-input-box-width: 100%;
75 | $avatar-preview-width: 200px;
76 | $avatar-preview-height: 200px;
77 |
78 | /* navigation bar */
79 | $header-background: lightblue;
80 | $header-bottom-border: $standard-border;
81 | $nav-bar-width: 70vw;
82 | $logo-dimension: 75px;
83 |
84 | /* user search bar */
85 | $user-search-input-margin: 0 1vw;
86 | $user-search-input-width: 20vw;
87 |
88 | /* user autocomplete list */
89 | $user-autocomplete-list-left: calc(61px + 1vw);
90 | $user-autocomplete-list-background: white;
91 | $user-autocomplete-list-width: $user-search-input-width;
92 | $user-autocomplete-list-border-radius: $base-border-radius;
93 | $user-autocomplete-list-border-left: $standard-border;
94 | $user-autocomplete-list-border-right: $standard-border;
95 | $user-autocomplete-list-border-bottom: $standard-border;
96 | $user-autocomplete-list-item-border-top: $standard-border;
97 | $user-autocomplete-list-item-padding: 5px;
98 |
99 | /* side bar */
100 |
101 | /* users list */
102 | $users-list-background: black;
103 | $users-list-height: calc(100vh - 76px - 73px);
104 | $users-list-width: 15vw;
105 | $users-list-opacity: .6;
106 |
--------------------------------------------------------------------------------
/app/src/components/auth/login_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import $ from 'jquery';
4 | import ApiSessionUtil from '../../apiutil/api_session_util.js';
5 | import { removeInvalidClass } from '../../utilities/auth.js';
6 |
7 | class LoginForm extends React.Component {
8 | constructor(props, context) {
9 | super(props, context);
10 | this.handleLoginSubmission = this.handleLoginSubmission.bind(this);
11 | this.handleKeyPress = this.handleKeyPress.bind(this);
12 | this.logIntoDemoAccount = this.logIntoDemoAccount.bind(this);
13 | this.changeUsername = this.changeUsername.bind(this);
14 | this.changePassword = this.changePassword.bind(this);
15 | this.state = {
16 | username: "",
17 | password: "",
18 | };
19 | }
20 |
21 | handleLoginSubmission (e) {
22 | if (e) { e.preventDefault(); }
23 |
24 | $(".submit").addClass("disabled").prop("disabled", true);
25 |
26 | const formData = new FormData();
27 |
28 | formData.append("username", this.state.username);
29 | formData.append("password", this.state.password);
30 |
31 | ApiSessionUtil.login(formData, this.props.success, this.props.failure);
32 | }
33 |
34 | handleKeyPress (e) {
35 | if (e.charCode === 13) { this.handleLoginSubmission(); }
36 | }
37 |
38 | logIntoDemoAccount (e) {
39 | e.preventDefault();
40 | }
41 |
42 | changeUsername (e) {
43 | removeInvalidClass("form-username-input");
44 |
45 | this.props.deleteUsernameErrors();
46 |
47 | this.setState({ username: e.currentTarget.value });
48 | }
49 |
50 | changePassword (e) {
51 | removeInvalidClass("form-password-input");
52 |
53 | this.props.deletePasswordErrors();
54 |
55 | this.setState({ password: e.currentTarget.value });
56 | }
57 |
58 | render() {
59 | const usernameErrors = this.props.usernameErrors.map( (err, idx) =>
60 | { err }
61 | );
62 |
63 | const passwordErrors = this.props.passwordErrors.map( (err, idx) =>
64 | { err }
65 | );
66 |
67 | return (
68 |
99 | );
100 | }
101 | }
102 |
103 | export default LoginForm;
104 |
--------------------------------------------------------------------------------
/app/api/controllers/json/user_controller.py:
--------------------------------------------------------------------------------
1 | from app import app
2 | from flask import request, session, jsonify
3 | from app.api.models import User
4 | from app.api.models.forms import RegistrationForm
5 | from app.api.controllers import application_controller
6 | from app.api.utilities import *
7 | import pdb
8 |
9 | @app.route("/users/api//", methods=["GET", "PUT", "DELETE"])
10 | def handle_user_api_request(username, mode):
11 | if request.method == "GET":
12 | return __fetch_users(username, mode)
13 | elif request.method == "PUT":
14 | return __update_user(username)
15 | elif request.method == "DELETE":
16 | return __destroy_user(username)
17 |
18 | @app.route("/users/api", methods=["POST"])
19 | def create_user():
20 | form = RegistrationForm(request.form)
21 |
22 | if form.validate():
23 | new_user = User(username = form.username.data)
24 | new_user.generate_password_digest(form.password.data)
25 | new_user.reset_session_token()
26 |
27 | if new_user.save():
28 | application_controller.login(new_user)
29 |
30 | user_response = build_user_response_object(new_user)
31 |
32 | return jsonify(user = user_response,
33 | message = "User creation successful! Welcome {0}!".format(new_user.username))
34 | else:
35 | return jsonify(error="Could not create user."), 401
36 | else:
37 | return jsonify(errors=form.errors.items()), 400
38 |
39 | def __fetch_users(username, mode):
40 | if mode == "autocomplete-selection" or mode == "autocomplete-input":
41 | users = User.objects.filter(username__icontains=username).only('username')[:5]
42 | else:
43 | users = application_controller.logged_in_users()
44 |
45 | return jsonify(users = users)
46 |
47 | def __update_user(username):
48 | password = request.form['password']
49 | option = request.form['option']
50 |
51 | user = User.find_by_username(username)[0]
52 |
53 | if user and User.validate_user_credentials(user, password):
54 | updated_user = __updated_user(user, option)
55 |
56 | user.username = new_username
57 | user.generate_password_digest(new_password)
58 |
59 | if updated_user.save():
60 | message = __generate_update_msg(option)
61 | return jsonify(username=username, message=message)
62 | else:
63 | return jsonify(error="Credentials are valid but could not update user.")
64 | else:
65 | return jsonify(error="Could not validate user credentials.")
66 |
67 | def __destroy_user(username):
68 | password = request.form['password']
69 |
70 | user = User.find_by_username(username)[0]
71 |
72 | if user and User.validate_user_credentials(user, password):
73 | if User.destroy(user):
74 | return jsonify(message="User {0} successfully deleted!".format(user.username))
75 | else:
76 | return jsonify(error="Credentials are valid but could not delete user.")
77 | else:
78 | return jsonify(error="Could not validate user credentials.")
79 |
80 | def __updated_user(user, option):
81 | if option == "change username":
82 | updated_username = request.form['new_username']
83 | user.username = updated_username
84 | return user
85 | else:
86 | updated_password = request.form['new_password']
87 | user.generate_password_digest(updated_password)
88 | return user
89 |
90 | def __generate_update_msg(option):
91 | if option == "change username":
92 | return "Username updated successfully!"
93 | else:
94 | return "Password updated successfully!"
95 |
--------------------------------------------------------------------------------
/app/src/components/auth/sign_up_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import $ from 'jquery';
4 | import ApiUserUtil from '../../apiutil/api_user_util.js';
5 | import { removeInvalidClass } from '../../utilities/auth.js';
6 |
7 | class SignUpForm extends React.Component {
8 | constructor(props, context) {
9 | super(props);
10 | this.handleSignUpSubmission = this.handleSignUpSubmission.bind(this);
11 | this.handleKeyPress = this.handleKeyPress.bind(this);
12 | this.changeUsername = this.changeUsername.bind(this);
13 | this.changePassword = this.changePassword.bind(this);
14 | this.changePasswordConf = this.changePasswordConf.bind(this);
15 | this.changeFile = this.changeFile.bind(this);
16 | this.state = {
17 | username: "",
18 | password: "",
19 | passwordConf: "",
20 | imageUrl: "/images/avatar_placeholder.png",
21 | imageFile: null
22 | };
23 | }
24 |
25 | handleSignUpSubmission (e) {
26 | if (e) { e.preventDefault(); }
27 |
28 | $(".submit").addClass("disabled").prop("disabled", true);
29 |
30 | const formData = new FormData();
31 |
32 | formData.append("username", this.state.username);
33 | formData.append("password", this.state.password);
34 | formData.append("confirm", this.state.passwordConf);
35 | formData.append("avatar", this.state.imageFile);
36 |
37 | ApiUserUtil.create(formData, this.props.success, this.props.failure);
38 | }
39 |
40 | handleKeyPress (e) {
41 | if (e.charCode === 13) { this.handleSignUpSubmission(); }
42 | }
43 |
44 | changeUsername (e) {
45 | removeInvalidClass("form-username-input");
46 |
47 | this.props.deleteUsernameErrors();
48 |
49 | this.setState({ username: e.currentTarget.value });
50 | }
51 |
52 | changePassword (e) {
53 | removeInvalidClass("form-password-input");
54 |
55 | this.props.deletePasswordErrors();
56 |
57 | this.setState({ password: e.currentTarget.value });
58 | }
59 |
60 | changePasswordConf (e) {
61 | removeInvalidClass("form-password-input");
62 |
63 | this.props.deletePasswordErrors();
64 |
65 | this.setState({ passwordConf: e.currentTarget.value });
66 | }
67 |
68 | changeFile (e) {
69 | const reader = new FileReader();
70 | const file = e.currentTarget.files[0];
71 |
72 | reader.onloadend = () =>
73 | this.setState({ imageUrl: reader.result, imageFile: file });
74 |
75 | file ? reader.readAsDataURL(file) : this.setState({ imageUrl: "", imageFile: null });
76 | }
77 |
78 | render() {
79 | const usernameErrors = this.props.usernameErrors.map( (err, idx) =>
80 | { err }
81 | );
82 |
83 | const passwordErrors = this.props.passwordErrors.map( (err, idx) =>
84 | { err }
85 | );
86 | return (
87 |
132 | );
133 | }
134 | }
135 |
136 | export default SignUpForm;
137 |
--------------------------------------------------------------------------------
/app/src/components/nav-bar/user_search.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import ApiUserUtil from '../../apiutil/api_user_util.js';
4 | import userSearchAutoCompleteStore from '../../stores/user_autocomplete_store.js';
5 |
6 | class UserSearch extends React.Component {
7 | constructor (props, context) {
8 | super(props, context);
9 | this.handleUserSearchInput = this.handleUserSearchInput.bind(this);
10 | this.handleUserSearchAutoComplete = this.handleUserSearchAutoComplete.bind(this);
11 | this.handleUserSearchSubmission = this.handleUserSearchSubmission.bind(this);
12 | this.handleKeyDown = this.handleKeyDown.bind(this);
13 | this.usernameInBounds = this.usernameInBounds.bind(this);
14 | this.selectUser = this.selectUser.bind(this);
15 | this.handleUserSearchInputFocus = this.handleUserSearchInputFocus.bind(this);
16 | this.addDOMListeners = this.addDOMListeners.bind(this);
17 | this.removeDOMListeners = this.removeDOMListeners.bind(this);
18 | this.moveUp = this.moveUp.bind(this);
19 | this.moveDown = this.moveDown.bind(this);
20 | this.highlightUser = this.highlightUser.bind(this);
21 | this.unhighlightUser = this.unhighlightUser.bind(this);
22 | this.__onChange = this.__onChange.bind(this);
23 | this.state = { username: "", showUserSearchAutoCompleteList: false };
24 | }
25 |
26 | componentDidMount () {
27 | this.addDOMListeners();
28 | userSearchAutoCompleteStore.addChangeListener(this.__onChange);
29 | }
30 |
31 | componentWillUnmount () {
32 | this.removeDOMListeners();
33 | userSearchAutoCompleteStore.removeChangeListener(this.__onChange);
34 | }
35 |
36 | componentWillReceiveProps (nextProps) {
37 | this.setState({ username: "", showUserSearchAutoCompleteList: false });
38 | }
39 |
40 | addDOMListeners () {
41 | $("#content").on("click", (e) => {
42 | $(".selected").removeClass("selected");
43 | this.setState({showUserSearchAutoCompleteList: false});
44 | });
45 |
46 | $(".user-search-bar").on("click", (e) => e.stopPropagation() );
47 | }
48 |
49 | removeDOMListeners () {
50 | $("#content").off();
51 | $(".user-search-bar").off();
52 | }
53 |
54 | handleUserSearchInput (e, mode) {
55 | const username = e.currentTarget.value || e.currentTarget.textContent;
56 |
57 | if (typeof mode === "undefined") { mode = "autocomplete-input"; }
58 | this.setState({ username });
59 |
60 | this.handleUserSearchAutoComplete(username, mode);
61 | }
62 |
63 | handleUserSearchAutoComplete (username, mode) {
64 | if (this.usernameInBounds(username)) {
65 | debugger;
66 | ApiUserUtil.fetchUsers(username, mode);
67 | } else {
68 | this.setState({ showUserSearchAutoCompleteList: false });
69 | }
70 | }
71 |
72 | handleUserSearchSubmission () {
73 | $(".user-search-submit").removeClass("pressed");
74 | this.props.successfulUserSearch(this.state.username);
75 | }
76 |
77 | selectUser (e) {
78 | this.handleUserSearchInput(e, "autocomplete-selection");
79 | }
80 |
81 | usernameInBounds (username) {
82 | const isInBounds = username.length >= 4 && username.length <= 25 ? true : false;
83 |
84 | return isInBounds;
85 | }
86 |
87 | handleKeyDown (e) {
88 | switch (e.keyCode) {
89 | // Enter
90 | case 13:
91 | if ($(".user-search-autocomplete-list-item.selected").length > 0) {
92 | $(".user-search-autocomplete-list-item.selected")[0].click();
93 | } else if ($(".user-search-bar").is(":focus")) {
94 | $(".user-search-submit").addClass("pressed");
95 | this.handleUserSearchSubmission();
96 | }
97 | break;
98 | // Arrow Up Key
99 | case 38:
100 | e.preventDefault();
101 | this.moveUp();
102 | break;
103 | // Arrow Down Key
104 | case 40:
105 | e.preventDefault();
106 | this.moveDown();
107 | break;
108 | }
109 | }
110 |
111 | moveUp () {
112 | if ($(".selected").prev(".user-search-autocomplete-list-item").length > 0) {
113 | $(".selected")
114 | .removeClass("selected")
115 | .prev(".user-search-autocomplete-list-item")
116 | .addClass("selected")
117 | .focus();
118 | } else {
119 | $(".selected").removeClass("selected");
120 | $(".user-search-autocomplete-list-item")
121 | .last()
122 | .addClass("selected")
123 | .focus();
124 | }
125 | }
126 |
127 | moveDown () {
128 | if ($(".selected").next(".user-search-autocomplete-list-item").length > 0) {
129 | $(".selected")
130 | .removeClass("selected")
131 | .next(".user-search-autocomplete-list-item")
132 | .addClass("selected")
133 | .focus();
134 | } else {
135 | $(".selected").removeClass("selected");
136 | $(".user-search-autocomplete-list-item")
137 | .first()
138 | .addClass("selected")
139 | .focus();
140 | }
141 | }
142 |
143 | highlightUser (e) {
144 | $(e.currentTarget).addClass("selected");
145 | }
146 |
147 | unhighlightUser (e) {
148 | $(e.currentTarget).removeClass("selected");
149 | }
150 |
151 | handleUserSearchInputFocus (e) {
152 | if (typeof e === "undefined") {
153 | this.setState({ showUserSearchAutoCompleteList: false });
154 | } else {
155 | if (this.usernameInBounds(this.state.username)) {
156 | this.setState({ showUserSearchAutoCompleteList: true });
157 | }
158 | }
159 | }
160 |
161 | __onChange () {
162 | this.setState({ showUserSearchAutoCompleteList: true });
163 | }
164 |
165 | render () {
166 | const users = userSearchAutoCompleteStore.get();
167 |
168 | const userSearchAutoCompleteItems = users.map( (user, idx) => {
169 | return
174 | { user.username }
175 | ;
176 | });
177 |
178 | let userSearchAutoCompleteListClass = "user-search-autocomplete-list ";
179 |
180 | if (this.state.showUserSearchAutoCompleteList) {
181 | userSearchAutoCompleteListClass += "show";
182 | }
183 |
184 | return (
185 |
186 |
187 |
188 |
195 |
196 |
198 |
199 |
200 | { userSearchAutoCompleteItems }
201 |
202 |
203 | );
204 | }
205 | }
206 |
207 | export default UserSearch;
208 |
--------------------------------------------------------------------------------