├── user-auth
├── .flaskenv
├── dev-requirements.txt
├── migrations
│ ├── README
│ ├── script.py.mako
│ ├── versions
│ │ ├── 20210722_173642_.py
│ │ ├── 20210816_151855_.py
│ │ └── 20210712_174530_.py
│ ├── alembic.ini
│ └── env.py
├── react-app
│ ├── src
│ │ ├── index.css
│ │ ├── components
│ │ │ ├── splash
│ │ │ │ ├── splashImage.jpg
│ │ │ │ ├── Splash.jsx
│ │ │ │ └── splash.css
│ │ │ ├── auth
│ │ │ │ ├── logoutButton.css
│ │ │ │ ├── ProtectedRoute.js
│ │ │ │ ├── LogoutButton.js
│ │ │ │ ├── loginForm.css
│ │ │ │ ├── signUpForm.css
│ │ │ │ ├── LoginForm.js
│ │ │ │ └── SignUpForm.js
│ │ │ ├── footer
│ │ │ │ ├── footer.css
│ │ │ │ └── Footer.jsx
│ │ │ ├── title
│ │ │ │ ├── title.css
│ │ │ │ └── Title.jsx
│ │ │ ├── interests
│ │ │ │ ├── DeleteInterest.jsx
│ │ │ │ ├── interests.css
│ │ │ │ ├── Interests.jsx
│ │ │ │ └── UpdateInterests.jsx
│ │ │ ├── gifs
│ │ │ │ ├── Gif.jsx
│ │ │ │ └── gifs.css
│ │ │ ├── navbar.css
│ │ │ ├── Google Analytics
│ │ │ │ └── GoogleAnalytics.js
│ │ │ ├── User.js
│ │ │ ├── articles
│ │ │ │ ├── Headline.jsx
│ │ │ │ ├── Article.jsx
│ │ │ │ └── articles.css
│ │ │ ├── default
│ │ │ │ └── Default.jsx
│ │ │ ├── saved
│ │ │ │ ├── RemoveFromSaved.jsx
│ │ │ │ ├── saved.css
│ │ │ │ ├── SavedList.jsx
│ │ │ │ └── AddToSaved.jsx
│ │ │ ├── content
│ │ │ │ └── Content.jsx
│ │ │ ├── weather
│ │ │ │ ├── getDays.js
│ │ │ │ ├── weather.css
│ │ │ │ └── Weather.jsx
│ │ │ ├── content.css
│ │ │ └── NavBar.jsx
│ │ ├── index.js
│ │ ├── store
│ │ │ ├── default.js
│ │ │ ├── index.js
│ │ │ ├── weather.js
│ │ │ ├── articles.js
│ │ │ ├── session.js
│ │ │ ├── saved.js
│ │ │ └── interests.js
│ │ ├── services
│ │ │ └── auth.js
│ │ └── App.js
│ ├── .env.example
│ ├── public
│ │ ├── favicon.ico
│ │ └── index.html
│ ├── .gitignore
│ ├── README.md
│ └── package.json
├── app
│ ├── models
│ │ ├── db.py
│ │ ├── __init__.py
│ │ ├── tag.py
│ │ ├── interest.py
│ │ ├── userTag.py
│ │ ├── homeData.py
│ │ ├── saved.py
│ │ └── user.py
│ ├── forms
│ │ ├── __init__.py
│ │ ├── tag_form.py
│ │ ├── interest_form.py
│ │ ├── update_interest_form.py
│ │ ├── saved_form.py
│ │ ├── signup_form.py
│ │ └── login_form.py
│ ├── config.py
│ ├── api
│ │ ├── default_routes.py
│ │ ├── tag_routes.py
│ │ ├── user_routes.py
│ │ ├── weather_routes.py
│ │ ├── saved_routes.py
│ │ ├── article_routes.py
│ │ ├── interest_routes.py
│ │ └── auth_routes.py
│ ├── seeds
│ │ ├── __init__.py
│ │ ├── users.py
│ │ ├── tags.py
│ │ └── default.py
│ └── __init__.py
├── .dockerignore
├── architecture.pdf
├── .env.example
├── .vscode
│ └── settings.json
├── Dockerfile
├── Pipfile
├── requirements.txt
├── redux-steps.md
├── .gitignore
└── Pipfile.lock
├── README.md
├── .vscode
└── settings.json
├── news-connector db schema (3).png
└── SETUP INSTRUCTIONS.md
/user-auth/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP=app
--------------------------------------------------------------------------------
/user-auth/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | psycopg2-binary==2.8.6
2 |
--------------------------------------------------------------------------------
/user-auth/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/user-auth/react-app/src/index.css:
--------------------------------------------------------------------------------
1 | /* TODO Add site wide styles */
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # news-connector
2 |
3 | https://news-connector.herokuapp.com/
4 |
--------------------------------------------------------------------------------
/user-auth/react-app/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_URL=http://localhost:5000
2 |
--------------------------------------------------------------------------------
/user-auth/app/models/db.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | db = SQLAlchemy()
3 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": "/Users/mikehat/.pyenv/versions/3.9.4/bin/python"
3 | }
--------------------------------------------------------------------------------
/user-auth/.dockerignore:
--------------------------------------------------------------------------------
1 | react-app/node_modules
2 | .venv
3 | Pipfile
4 | Pipfile.lock
5 | .env
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/user-auth/architecture.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/John-Witter/news-connector/HEAD/user-auth/architecture.pdf
--------------------------------------------------------------------------------
/news-connector db schema (3).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/John-Witter/news-connector/HEAD/news-connector db schema (3).png
--------------------------------------------------------------------------------
/user-auth/react-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/John-Witter/news-connector/HEAD/user-auth/react-app/public/favicon.ico
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/splash/splashImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/John-Witter/news-connector/HEAD/user-auth/react-app/src/components/splash/splashImage.jpg
--------------------------------------------------------------------------------
/user-auth/app/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 | from .user import User
3 | from.tag import Tag
4 | from .saved import Saved
5 | from .interest import Interest
6 | from .homeData import HomeData
--------------------------------------------------------------------------------
/user-auth/.env.example:
--------------------------------------------------------------------------------
1 | FLASK_APP=app
2 | FLASK_ENV=development
3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230
4 | DATABASE_URL=postgresql://starter_app_dev@localhost/starter_app
5 |
--------------------------------------------------------------------------------
/user-auth/app/forms/__init__.py:
--------------------------------------------------------------------------------
1 | from .login_form import LoginForm
2 | from .signup_form import SignUpForm
3 | from .saved_form import SavedForm
4 | from .tag_form import TagForm
5 | from .interest_form import InterestForm
--------------------------------------------------------------------------------
/user-auth/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": ".venv/bin/python",
3 | "python.linting.pylintEnabled": false,
4 | "python.linting.enabled": true,
5 | "python.linting.pycodestyleEnabled": true
6 | }
--------------------------------------------------------------------------------
/user-auth/app/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class Config:
5 | SECRET_KEY = os.environ.get('SECRET_KEY')
6 | SQLALCHEMY_TRACK_MODIFICATIONS = False
7 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
8 | SQLALCHEMY_ECHO = True
9 |
--------------------------------------------------------------------------------
/user-auth/app/forms/tag_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, IntegerField
3 | from wtforms.validators import DataRequired
4 |
5 |
6 | class TagForm(FlaskForm):
7 | title = StringField('title', [DataRequired()])
8 |
--------------------------------------------------------------------------------
/user-auth/app/forms/interest_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, IntegerField
3 | from wtforms.validators import DataRequired
4 |
5 | class InterestForm(FlaskForm):
6 | userId = IntegerField('userId', [DataRequired()])
7 | title = StringField('title', [DataRequired()])
--------------------------------------------------------------------------------
/user-auth/app/api/default_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 | from app.models import HomeData
3 |
4 | default_routes = Blueprint('homeData', __name__)
5 |
6 | @default_routes.route('/')
7 | def get_defaults():
8 | defaults = HomeData.query.all()
9 | return {"defaults": [item.to_dict() for item in defaults]}
10 |
--------------------------------------------------------------------------------
/user-auth/app/models/tag.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 | class Tag(db.Model):
4 | __tablename__ = 'tags'
5 |
6 | id = db.Column(db.Integer, primary_key=True)
7 | title = db.Column(db.String, nullable=False, unique=True)
8 |
9 | def to_dict(self):
10 | return {
11 | "id": self.id,
12 | "title": self.title
13 | }
--------------------------------------------------------------------------------
/user-auth/app/api/tag_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 | from app.models import db, Tag
3 | from app.forms import TagForm
4 |
5 | tag_routes = Blueprint('tags', __name__)
6 |
7 |
8 | # return all tags in the db
9 | @tag_routes.route('/', methods=["GET"])
10 | def get_tags():
11 | db_tags = Tag.query.all()
12 | return {"tags": [tags.to_dict() for tags in db_tags]}
--------------------------------------------------------------------------------
/user-auth/app/forms/update_interest_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, IntegerField
3 | from wtforms.validators import DataRequired
4 |
5 |
6 | class UpdateInterestForm(FlaskForm):
7 | userId = IntegerField('userId', [DataRequired()])
8 | interestId = IntegerField('interestId', [DataRequired()])
9 | title = StringField('title', [DataRequired()])
10 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/auth/logoutButton.css:
--------------------------------------------------------------------------------
1 | .logoutBtn {
2 | /* background-color: #e3e2d7; */
3 | color: #416894;
4 | font-size: 1rem;
5 | border: none;
6 | padding: 0;
7 | margin: 0;
8 | font-family: sans-serif;
9 | align-self: flex-start;
10 | }
11 |
12 | .logoutBtn:hover {
13 | cursor: pointer;
14 | text-decoration: underline;
15 | color: #4b256b;
16 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/splash/Splash.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './splash.css'
3 |
4 | const Splash = () => {
5 | return (
6 |
7 |
Connect to Your Interests
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | export default Splash
16 |
--------------------------------------------------------------------------------
/user-auth/react-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/auth/ProtectedRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Route, Redirect } from 'react-router-dom';
4 |
5 | const ProtectedRoute = props => {
6 | const user = useSelector(state => state.session.user)
7 | return (
8 |
9 | {(user)? props.children : }
10 |
11 | )
12 | };
13 |
14 |
15 | export default ProtectedRoute;
16 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/footer/footer.css:
--------------------------------------------------------------------------------
1 | .footer{
2 |
3 | }
4 |
5 | .footerUL {
6 | display: flex;
7 | justify-content: space-around;
8 | align-items: center;
9 | }
10 |
11 | .footerLI {
12 | list-style: none;
13 | }
14 |
15 | .linkText {
16 | color: #416894;
17 | letter-spacing: .03125rem;
18 | font-weight: 500;
19 | font-size: 1rem;
20 | }
21 |
22 | .footerLI:hover {
23 | cursor: pointer;
24 | text-decoration: underline;
25 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/title/title.css:
--------------------------------------------------------------------------------
1 | .titleContainer {
2 | width: 100%;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | margin: 0 auto;
7 | }
8 |
9 | .title-link {
10 | margin: 0 auto;
11 | }
12 |
13 | .title {
14 | color: #7E3EB3;
15 | font-family: cursive;
16 | }
17 |
18 | .title:hover {
19 | /* font-weight: bolder; */
20 | color: #4b256b;
21 | }
22 |
23 | a {
24 | font-size: 1.5rem;
25 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux'
4 | import './components/content.css'
5 | import App from './App';
6 | import configureStore from './store';
7 |
8 | const store = configureStore();
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | );
18 |
--------------------------------------------------------------------------------
/user-auth/app/models/interest.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 | class Interest(db.Model):
4 | __tablename__ = 'interests'
5 |
6 | id = db.Column(db.Integer, primary_key=True)
7 | title = db.Column(db.String, nullable=False, unique=True)
8 | userId = db.Column(db.Integer, db.ForeignKey('users.id'),nullable=False)
9 |
10 | def to_dict(self):
11 | return {
12 | 'id': self.id,
13 | 'title': self.title,
14 | 'userId': self.userId
15 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/auth/LogoutButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import { logout } from "../../store/session";
4 | import './logoutButton.css'
5 |
6 | const LogoutButton = () => {
7 | const dispatch = useDispatch();
8 | const onLogout = async (e) => {
9 | await dispatch(logout());
10 | };
11 |
12 | return ;
13 | };
14 |
15 | export default LogoutButton;
16 |
--------------------------------------------------------------------------------
/user-auth/app/api/user_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify
2 | from flask_login import login_required
3 | from app.models import User
4 |
5 | user_routes = Blueprint('users', __name__)
6 |
7 |
8 | @user_routes.route('/')
9 | @login_required
10 | def users():
11 | users = User.query.all()
12 | return {"users": [user.to_dict() for user in users]}
13 |
14 |
15 | @user_routes.route('/')
16 | @login_required
17 | def user(id):
18 | user = User.query.get(id)
19 | return user.to_dict()
20 |
--------------------------------------------------------------------------------
/user-auth/app/forms/saved_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, IntegerField
3 | from wtforms.validators import DataRequired
4 |
5 | class SavedForm(FlaskForm):
6 | userId = IntegerField('userId', [DataRequired()])
7 | itemURL = StringField('itemURL', [DataRequired()])
8 | imageURL = StringField('imageURL', [DataRequired()])
9 | title = StringField('title', [DataRequired()])
10 | description = StringField('description')
11 | contentSource = StringField('contentSource')
12 | publishedTime = StringField('publishedTime')
--------------------------------------------------------------------------------
/user-auth/react-app/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | Your React App will live here. While is development, run this application from this location using `npm start`.
4 |
5 |
6 | No environment variables are needed to run this application in development, but be sure to set the REACT_APP_BASE_URL environment variable in heroku!
7 |
8 | This app will be automatically built when you deploy to heroku, please see the `heroku-postbuild` script in your `express.js` applications `package.json` to see how this works.
9 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/title/Title.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { NavLink } from 'react-router-dom'
3 | import LogoutButton from '../auth/LogoutButton'
4 | import './title.css'
5 |
6 | const Title = () => {
7 | return (
8 |
9 |
10 | news-connector
11 |
12 | {/* < LogoutButton /> */}
13 |
14 | )
15 | }
16 |
17 | export default Title
18 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/interests/DeleteInterest.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from "react-redux";
3 | import { removeInterest } from "../../store/interests";
4 |
5 | const DeleteInterest = ({ userId, interest}) => {
6 | const dispatch = useDispatch()
7 | const handleDelete = () => {
8 | dispatch(removeInterest(userId, interest.id, interest.title))
9 | }
10 | return (
11 |
14 | )
15 | }
16 |
17 | export default DeleteInterest
18 |
--------------------------------------------------------------------------------
/user-auth/app/models/userTag.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 | # from .user import User
3 | from .tag import Tag
4 |
5 | class UserTag(db.Model):
6 | __tablename__ = 'userTags'
7 | id = db.Column(db.Integer, primary_key=True)
8 | userId = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
9 | tagId = db.Column(db.Integer, db.ForeignKey('tags.id'), nullable=False)
10 |
11 |
12 | def to_dict(self):
13 | return {
14 | "id": self.id,
15 | "userId": self.userId,
16 | "tag": Tag.query.filter(Tag.id == self.tagId).first().to_dict(),
17 | }
18 |
--------------------------------------------------------------------------------
/user-auth/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/user-auth/app/models/homeData.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class HomeData(db.Model):
5 | __tablename__ = 'homeData'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | itemURL = db.Column(db.String, nullable=False, unique=True)
9 | imageURL = db.Column(db.String, unique=True)
10 | title = db.Column(db.String, nullable=False)
11 | description = db.Column(db.String)
12 |
13 | def to_dict(self):
14 | return {
15 | "id": self.id,
16 | "itemURL": self.itemURL,
17 | "imageURL": self.imageURL,
18 | "title": self.title,
19 | "description": self.description
20 | }
21 |
--------------------------------------------------------------------------------
/user-auth/app/seeds/__init__.py:
--------------------------------------------------------------------------------
1 | from flask.cli import AppGroup
2 | from .users import seed_users, undo_users
3 | from .tags import seed_tags, undo_tags
4 | from .default import seed_default
5 |
6 | # Creates a seed group to hold our commands
7 | # So we can type `flask seed --help`
8 | seed_commands = AppGroup('seed')
9 |
10 | # Creates the `flask seed all` command
11 | @seed_commands.command('all')
12 | def seed():
13 | seed_users()
14 | seed_default()
15 | # Add other seed functions here
16 | seed_tags()
17 |
18 | # Creates the `flask seed undo` command
19 | @seed_commands.command('undo')
20 | def undo():
21 | undo_users()
22 | # Add other undo functions here
23 | undo_tags()
--------------------------------------------------------------------------------
/user-auth/app/seeds/users.py:
--------------------------------------------------------------------------------
1 | from werkzeug.security import generate_password_hash
2 | from app.models import db, User
3 |
4 | # Adds a demo user, you can add other users here if you want
5 | def seed_users():
6 |
7 | demo = User(username='Demo', email='demo@aa.io',
8 | password='password')
9 |
10 | db.session.add(demo)
11 |
12 | db.session.commit()
13 |
14 | # Uses a raw SQL query to TRUNCATE the users table.
15 | # SQLAlchemy doesn't have a built in function to do this
16 | # TRUNCATE Removes all the data from the table, and resets
17 | # the auto incrementing primary key
18 | def undo_users():
19 | db.session.execute('TRUNCATE users RESTART IDENTITY CASCADE;')
20 | db.session.commit()
21 |
--------------------------------------------------------------------------------
/user-auth/app/forms/signup_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from app.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | print("Checking if user exits", field.data)
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if user:
12 | raise ValidationError("User is already registered.")
13 |
14 |
15 | class SignUpForm(FlaskForm):
16 | username = StringField('username', validators=[DataRequired()])
17 | email = StringField('email', validators=[DataRequired(), user_exists])
18 | password = StringField('password', validators=[DataRequired()])
19 |
--------------------------------------------------------------------------------
/user-auth/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12 AS build-stage
2 |
3 | WORKDIR /react-app
4 | COPY react-app/. .
5 |
6 | # You have to set this because it should be set during build time.
7 | ENV REACT_APP_BASE_URL="https://news-connector.herokuapp.com/"
8 |
9 | # Build our React App
10 | RUN npm install
11 | RUN npm run build
12 |
13 | FROM python:3.8
14 |
15 | # Setup Flask environment
16 | ENV FLASK_APP=app
17 | ENV FLASK_ENV=production
18 | ENV SQLALCHEMY_ECHO=True
19 |
20 | EXPOSE 8000
21 |
22 | WORKDIR /var/www
23 | COPY . .
24 | COPY --from=build-stage /react-app/build/* app/static/
25 |
26 | # Install Python Dependencies
27 | RUN pip install -r requirements.txt
28 | RUN pip install psycopg2
29 |
30 | # Run flask environment
31 | CMD gunicorn app:app
32 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/auth/loginForm.css:
--------------------------------------------------------------------------------
1 | .login-outermost-container {
2 | height: 75vh;
3 | width: 100vw;
4 | margin-top: 2rem;
5 | }
6 |
7 | .login-form {
8 | height: 100%;
9 | /* padding-left: 10px; */
10 | width: 100%;
11 | display: flex;
12 | flex-direction: column;
13 | align-items: center;
14 | }
15 |
16 | .login-input-container {
17 | width: 310px;
18 | display: flex;
19 | }
20 |
21 | .login-input-label {
22 | margin-right: 41px;
23 | }
24 |
25 | .login-input {
26 | color: #000;
27 | }
28 |
29 |
30 | .password-input-container {
31 | width: 300px;
32 | display: flex;
33 | }
34 |
35 | .password-input-label {
36 | margin-right: 10px;
37 | }
38 |
39 | .password-input {
40 | margin-right: 15px;
41 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/splash/splash.css:
--------------------------------------------------------------------------------
1 | .mainImageContainer {
2 | display: flex;
3 | align-content: center;
4 | flex-direction: column;
5 | width: 100vw;
6 | }
7 |
8 | .tagLine {
9 | display: block;
10 | align-self: center;
11 | margin-top: -1rem;
12 | margin-bottom: 2rem;
13 | font-weight: 540;
14 | font-size: 1.125rem;
15 | }
16 |
17 | .mainImage {
18 | background: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3)), url("./splashImage.jpg");
19 | /* background-image: url("https://images.unsplash.com/photo-1490775949603-0e355e8e01ba?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1047&q=80"); */
20 | background-size: contain;
21 | height: 33rem;
22 | width: 49rem;
23 | margin: 0 auto;
24 |
25 | }
--------------------------------------------------------------------------------
/user-auth/migrations/versions/20210722_173642_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: c5ab4546c252
4 | Revises: 486b230f1f36
5 | Create Date: 2021-07-22 17:36:42.126805
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'c5ab4546c252'
14 | down_revision = '6735414ea296'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('saved', sa.Column('contentSource', sa.String(), nullable=True))
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.drop_column('saved', 'contentSource')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/user-auth/migrations/versions/20210816_151855_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 9fd082dd664d
4 | Revises: c5ab4546c252
5 | Create Date: 2021-08-16 15:18:55.490223
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '9fd082dd664d'
14 | down_revision = 'c5ab4546c252'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('saved', sa.Column('publishedTime', sa.String(), nullable=True))
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.drop_column('saved', 'publishedTime')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/auth/signUpForm.css:
--------------------------------------------------------------------------------
1 | .signup-outermost-container {
2 | height: 75vh;
3 | width: 100vw;
4 | margin-top: 2rem;
5 | }
6 |
7 | .signup-form {
8 | height: 100%;
9 | /* padding-left: 10px; */
10 | width: 100%;
11 | display: flex;
12 | flex-direction: column;
13 | align-items: center;
14 | }
15 |
16 | .name-input-container {
17 | width: 310px;
18 | }
19 |
20 | .name-label {
21 | margin-right: 59px;
22 | }
23 |
24 | .email-input-container {
25 | width: 310px;
26 | }
27 |
28 | .email-label {
29 | margin-right: 101px;
30 | }
31 |
32 | .password-input-container {
33 | width: 310px;
34 | }
35 |
36 | .password-label {
37 | margin-right: 70px;
38 | }
39 |
40 | .repeat-password-input-container {
41 | width: 310px;
42 | }
43 |
44 | .repeat-password-label {
45 | margin-right: 10px;
46 | }
--------------------------------------------------------------------------------
/user-auth/app/api/weather_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 | import requests
3 | import os
4 |
5 | weather_routes = Blueprint('weather', __name__)
6 |
7 |
8 | @weather_routes.route('/', methods=['GET'])
9 | def get_weather(loc):
10 | api_key = os.environ.get('WEATHER_API')
11 |
12 | # https://ipapi.co/api/?python#location-of-clients-ip
13 | try:
14 | # response = requests.get('https://ipapi.co/json/').json()
15 | # lat = response['latitude']
16 | # lon = response['longitude']
17 | strToArr = loc.split('+')
18 | url = f'http://api.openweathermap.org/data/2.5/onecall?lat={strToArr[1]}&lon={strToArr[2]}&appid={api_key}&units=imperial'
19 |
20 | res = requests.get(url)
21 |
22 | return {'weather': res.json(), 'location': strToArr}
23 |
24 | except:
25 | return {'error': res.json()}
26 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/gifs/Gif.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AddToSaved from '../saved/AddToSaved'
3 | import './gifs.css'
4 | import '../content.css'
5 |
6 | const Gif = ({ gif }) => {
7 | gif['itemURL'] = gif.url
8 | gif['imageURL'] = gif.images.original.url
9 | gif['contentSource'] = 'Giphy'
10 |
11 | return (
12 |
22 | )
23 | }
24 |
25 | export default Gif
26 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/store/default.js:
--------------------------------------------------------------------------------
1 | // constants
2 | const READ_DEFAULT = 'default/READ_DEFAULT'
3 |
4 | // actions
5 | const readDefault = (articles) => ({
6 | type: READ_DEFAULT,
7 | articles
8 | })
9 |
10 | // thunks
11 | export const loadDefault = () => async (dispatch) => {
12 | const res = await fetch ('/api/homeData/')
13 |
14 | if (res.ok) {
15 | const data = await res.json()
16 | dispatch(readDefault(data))
17 | }
18 | }
19 |
20 | // reducer
21 | export default function DefaultReducer (state={}, action) {
22 | let newState = {...state}
23 |
24 | switch (action.type) {
25 | case READ_DEFAULT:
26 | action.articles.defaults.forEach(article => {
27 | newState[article.id] = article
28 | })
29 | return newState
30 | default:
31 | return state
32 | }
33 | }
--------------------------------------------------------------------------------
/user-auth/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | click = "==7.1.2"
8 | gunicorn = "==20.0.4"
9 | itsdangerous = "==1.1.0"
10 | python-dotenv = "==0.14.0"
11 | six = "==1.15.0"
12 | Flask = "==1.1.2"
13 | Flask-Cors = "==3.0.8"
14 | Flask-SQLAlchemy = "==2.4.4"
15 | Flask-WTF = "==0.14.3"
16 | Jinja2 = "==2.11.2"
17 | MarkupSafe = "==1.1.1"
18 | SQLAlchemy = "==1.3.19"
19 | Werkzeug = "==1.0.1"
20 | WTForms = "==2.3.3"
21 | Flask-JWT-Extended = "==3.24.1"
22 | email-validator = "*"
23 | Flask-Migrate = "==2.5.3"
24 | Flask-Login = "==0.5.0"
25 | alembic = "==1.4.3"
26 | python-dateutil = "==2.8.1"
27 | python-editor = "==1.0.4"
28 | Mako = "==1.1.3"
29 | PyJWT = "==1.7.1"
30 | requests = "*"
31 |
32 | [dev-packages]
33 | psycopg2-binary = "==2.8.6"
34 | autopep8 = "*"
35 | pylint = "*"
36 |
37 | [requires]
38 | python_version = "3.9"
39 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/navbar.css:
--------------------------------------------------------------------------------
1 | /*
2 | navbar links
3 | interests
4 | */
5 |
6 | .navbar {
7 | display: flex;
8 | flex-direction: row;
9 | justify-content: space-around;
10 | align-items: center;
11 | }
12 |
13 | .navbar li {
14 | list-style: none;
15 | font-family: sans-serif;
16 | letter-spacing: .03125rem;
17 | }
18 |
19 | .navbar li a {
20 | font-size: 1rem;
21 | }
22 |
23 | .authLink {
24 | letter-spacing: .03125rem;
25 | color: #416894;
26 | }
27 |
28 | .demoBtn {
29 | color: #416894;
30 | font-size: 1rem;
31 | }
32 |
33 | .navbar li:hover {
34 | cursor: pointer;
35 | text-decoration: underline;
36 | /* font-weight: bolder;
37 | color: #4b256b; */
38 | }
39 |
40 | .demoBtn:hover {
41 | cursor: pointer;
42 | text-decoration: underline;
43 | /* font-weight: bolder;
44 | color: #4b256b; */
45 | }
46 |
47 | /* .interests {
48 |
49 | } */
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/Google Analytics/GoogleAnalytics.js:
--------------------------------------------------------------------------------
1 | // Google Analytics
2 | // Initialize ReactGA with standardImplementation: true option.
3 | // https://www.npmjs.com/package/react-ga?activeTab=readme
4 | // https://malith-dev.medium.com/track-users-in-your-react-app-with-google-analytics-6364ebfcbae8
5 |
6 | import ReactGA from "react-ga";
7 |
8 | // method to inititialize ReactGA
9 | export const initGA = (trackingID) => {
10 | ReactGA.initialize("UA-213353187-1");
11 | };
12 |
13 | // Page View
14 | export const PageView = () => {
15 | ReactGA.pageview(window.location.pathname + window.location.search);
16 | };
17 |
18 | /**
19 | * Event - Add custom tracking event.
20 | * @param {string} category
21 | * @param {string} action
22 | * @param {string} label
23 | */
24 | export const Event = (category, action, label) => {
25 | ReactGA.event({
26 | category: category,
27 | action: action,
28 | label: label
29 | });
30 | };
--------------------------------------------------------------------------------
/user-auth/app/models/saved.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 | class Saved(db.Model):
4 | __tablename__ = 'saved'
5 |
6 | id = db.Column(db.Integer, primary_key=True)
7 | userId = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
8 | itemURL = db.Column(db.String, nullable=False, unique=True)
9 | imageURL = db.Column(db.String, nullable=False)
10 | title = db.Column(db.String, nullable=False)
11 | description = db.Column(db.String)
12 | contentSource = db.Column(db.String)
13 | publishedTime = db.Column(db.String)
14 |
15 | def to_dict(self):
16 | return {
17 | "id": self.id,
18 | "userId": self.userId,
19 | "itemURL": self.itemURL,
20 | "imageURL": self.imageURL,
21 | "title": self.title,
22 | "description": self.description,
23 | "contentSource": self.contentSource,
24 | "publishedTime": self.publishedTime
25 | }
26 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/User.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 |
4 | function User() {
5 | const [user, setUser] = useState({});
6 | // Notice we use useParams here instead of getting the params
7 | // From props.
8 | const { userId } = useParams();
9 |
10 | useEffect(() => {
11 | if (!userId) {
12 | return
13 | }
14 | (async () => {
15 | const response = await fetch(`/api/users/${userId}`);
16 | const user = await response.json();
17 | setUser(user);
18 | })();
19 | }, [userId]);
20 |
21 | if (!user) {
22 | return null;
23 | }
24 |
25 | return (
26 |
27 | -
28 | User Id {userId}
29 |
30 | -
31 | Username {user.username}
32 |
33 | -
34 | Email {user.email}
35 |
36 |
37 | );
38 | }
39 | export default User;
40 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/gifs/gifs.css:
--------------------------------------------------------------------------------
1 | h2 {
2 | /* font-size: 16px;
3 | color: #7e3eb3; */
4 | }
5 |
6 | img {
7 | max-width: 66%;
8 | }
9 |
10 | a {
11 | text-decoration: none;
12 | display: flex;
13 | flex-direction: column;
14 | }
15 |
16 | .gifTitle {
17 | text-decoration: none;
18 | color: #416894;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | text-align: left;
23 | font-size: 1rem;
24 | padding: 0 2rem;
25 | }
26 |
27 | .gifSource {
28 | text-decoration: none;
29 | color: #416894;
30 | display: flex;
31 | flex-direction: column;
32 | align-items: flex-start;
33 | text-align: left;
34 | font-size: .75rem;
35 | padding: 0 2rem;
36 | padding-top: .5rem;
37 | font-weight: 300;
38 | }
39 |
40 | .gifTitle:hover {
41 | /* font-size: larger; */
42 | /* color: #8a51ba; */
43 | text-decoration: underline;
44 | }
45 |
46 | .gifSource:hover {
47 | text-decoration: underline;
48 | }
--------------------------------------------------------------------------------
/user-auth/app/seeds/tags.py:
--------------------------------------------------------------------------------
1 | from app.models import db, Tag
2 |
3 | # Tags are only seeded / users can't add tags
4 | def seed_tags():
5 |
6 | business = Tag(title='business')
7 | entertainment = Tag(title='entertainment')
8 | general = Tag(title='general')
9 | health = Tag(title='health')
10 | science = Tag(title='science')
11 | sports = Tag(title='sports')
12 | technology = Tag(title='technology')
13 |
14 | db.session.add(business)
15 | db.session.add(entertainment)
16 | db.session.add(general)
17 | db.session.add(health)
18 | db.session.add(science)
19 | db.session.add(sports)
20 | db.session.add(technology)
21 | db.session.commit()
22 |
23 | # Uses a raw SQL query to TRUNCATE the users table.
24 | # SQLAlchemy doesn't have a built in function to do this
25 | # TRUNCATE Removes all the data from the table, and resets
26 | # the auto incrementing primary key
27 |
28 | def undo_tags():
29 | db.session.execute('TRUNCATE users RESTART IDENTITY CASCADE;')
30 | db.session.commit()
31 |
--------------------------------------------------------------------------------
/user-auth/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/user-auth/app/forms/login_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from app.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | print("Checking if user exists", field.data)
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if not user:
12 | raise ValidationError("Email provided not found.")
13 |
14 |
15 | def password_matches(form, field):
16 | print("Checking if password matches")
17 | password = field.data
18 | email = form.data['email']
19 | user = User.query.filter(User.email == email).first()
20 | if not user:
21 | raise ValidationError("No such user exists.")
22 | if not user.check_password(password):
23 | raise ValidationError("Password was incorrect.")
24 |
25 |
26 | class LoginForm(FlaskForm):
27 | email = StringField('email', validators=[DataRequired(), user_exists])
28 | password = StringField('password', validators=[
29 | DataRequired(), password_matches])
30 |
--------------------------------------------------------------------------------
/user-auth/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # These requirements were autogenerated by pipenv
3 | # To regenerate from the project's Pipfile, run:
4 | #
5 | # pipenv lock --requirements
6 | #
7 |
8 | -i https://pypi.org/simple
9 | alembic==1.4.3
10 | certifi==2021.5.30
11 | chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
12 | click==7.1.2
13 | dnspython==2.1.0; python_version >= '3.6'
14 | email-validator==1.1.3
15 | flask-cors==3.0.8
16 | flask-jwt-extended==3.24.1
17 | flask-login==0.5.0
18 | flask-migrate==2.5.3
19 | flask-sqlalchemy==2.4.4
20 | flask-wtf==0.14.3
21 | flask==1.1.2
22 | gunicorn==20.0.4
23 | idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
24 | itsdangerous==1.1.0
25 | jinja2==2.11.2
26 | mako==1.1.3
27 | markupsafe==1.1.1
28 | pyjwt==1.7.1
29 | python-dateutil==2.8.1
30 | python-dotenv==0.14.0
31 | python-editor==1.0.4
32 | requests==2.25.1
33 | six==1.15.0
34 | sqlalchemy==1.3.19
35 | urllib3==1.26.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
36 | werkzeug==1.0.1
37 | wtforms==2.3.3
38 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/articles/Headline.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AddToSaved from '../saved/AddToSaved'
3 | import './articles.css'
4 | import '../content.css'
5 |
6 | const Headline = ({ article }) => {
7 | article['itemURL'] = article['url']
8 | article['imageURL'] = article['urlToImage']
9 | article['contentSource'] = article.source.name
10 |
11 | return (
12 |
13 |
17 |
18 |
19 |
20 |
21 | {article.description}
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | export default Headline
29 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/articles/Article.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AddToSaved from '../saved/AddToSaved'
3 | import './articles.css'
4 | import '../content.css'
5 |
6 |
7 | const Article = ({ article }) => {
8 |
9 | article['itemURL'] = article['url']
10 | article['imageURL'] = article['urlToImage']
11 | article['contentSource'] = article.source.name
12 |
13 | return (
14 |
15 |
19 |
20 |
21 |
22 |
23 | {article.description}
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default Article
31 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from "redux";
2 | import thunk from "redux-thunk";
3 | import session from "./session"
4 | import InterestReducer from "./interests";
5 | import ArticleReducer from "./articles";
6 | import SavedReducer from "./saved";
7 | import DefaultReducer from "./default";
8 | import WeatherReducer from "./weather";
9 |
10 | const rootReducer = combineReducers({
11 | session,
12 | interest: InterestReducer,
13 | article: ArticleReducer,
14 | saved: SavedReducer,
15 | default: DefaultReducer,
16 | weather: WeatherReducer
17 | });
18 |
19 |
20 | let enhancer;
21 |
22 | if (process.env.NODE_ENV === 'production') {
23 | enhancer = applyMiddleware(thunk);
24 | } else {
25 | const logger = require('redux-logger').default;
26 | const composeEnhancers =
27 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
28 | enhancer = composeEnhancers(applyMiddleware(thunk, logger));
29 | }
30 |
31 | const configureStore = (preloadedState) => {
32 | return createStore(rootReducer, preloadedState, enhancer);
33 | };
34 |
35 | export default configureStore;
36 |
--------------------------------------------------------------------------------
/user-auth/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "http-proxy-middleware": "^1.0.5",
10 | "react": "^17.0.0",
11 | "react-dom": "^17.0.0",
12 | "react-ga": "^3.3.0",
13 | "react-redux": "^7.2.4",
14 | "react-router-dom": "^5.2.0",
15 | "react-scripts": "3.4.3",
16 | "redux": "^4.1.0",
17 | "redux-logger": "^3.0.6",
18 | "redux-thunk": "^2.3.0"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | },
41 | "proxy": "http://localhost:5000"
42 | }
43 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/default/Default.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { loadDefault } from '../../store/default'
4 | import '../content.css'
5 | import '../articles/articles.css'
6 |
7 | const Default = () => {
8 | const dispatch = useDispatch()
9 | const defaults = useSelector(state => state.default)
10 |
11 | useEffect(() => {
12 | dispatch(loadDefault())
13 | }, [dispatch])
14 |
15 | return (
16 |
17 | {defaults && Object.values(defaults).map(item => {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | {item.description && (
{item.description}
)}
25 |
26 | )
27 | })}
28 |
29 | )
30 | }
31 |
32 | export default Default
33 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/services/auth.js:
--------------------------------------------------------------------------------
1 | export const authenticate = async() => {
2 | const response = await fetch('/api/auth/',{
3 | headers: {
4 | 'Content-Type': 'application/json'
5 | }
6 | });
7 | return await response.json();
8 | }
9 |
10 | export const login = async (email, password) => {
11 | const response = await fetch('/api/auth/login', {
12 | method: 'POST',
13 | headers: {
14 | 'Content-Type': 'application/json'
15 | },
16 | body: JSON.stringify({
17 | email,
18 | password
19 | })
20 | });
21 | return await response.json();
22 | }
23 |
24 | export const logout = async () => {
25 | const response = await fetch("/api/auth/logout", {
26 | headers: {
27 | "Content-Type": "application/json",
28 | }
29 | });
30 | return await response.json();
31 | };
32 |
33 |
34 | export const signUp = async (username, email, password) => {
35 | const response = await fetch("/api/auth/signup", {
36 | method: "POST",
37 | headers: {
38 | "Content-Type": "application/json",
39 | },
40 | body: JSON.stringify({
41 | username,
42 | email,
43 | password,
44 | }),
45 | });
46 | return await response.json();
47 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/saved/RemoveFromSaved.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { removeFromSaved } from '../../store/saved'
4 | import '../content.css'
5 | import './saved.css'
6 |
7 | const RemoveFromSaved = ({ item }) => {
8 | const dispatch = useDispatch()
9 | const user = useSelector(state => state.session.user)
10 | const userId = user['id']
11 | const savedArticles = useSelector(state => state.saved)
12 | const [savedText, setSavedText] = useState('Remove from Saved')
13 |
14 | const handleRemoveFromSaved = () => {
15 | const { title, itemURL, imageURL, description } = item
16 |
17 | if (savedText === 'Remove from Saved') {
18 | dispatch(removeFromSaved(userId, itemURL, imageURL, title, description))
19 | }
20 | }
21 |
22 | return (
23 |
24 |
handleRemoveFromSaved()}
27 | >
28 | {savedText}
29 |
30 |
31 | )
32 | }
33 |
34 | export default RemoveFromSaved
35 |
--------------------------------------------------------------------------------
/user-auth/app/models/user.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.orm import backref, relationship
2 | from .db import db
3 | from .saved import Saved
4 | from .userTag import UserTag
5 | from werkzeug.security import generate_password_hash, check_password_hash
6 | from flask_login import UserMixin
7 |
8 |
9 |
10 | class User(db.Model, UserMixin):
11 | __tablename__ = 'users'
12 |
13 | id = db.Column(db.Integer, primary_key=True)
14 | username = db.Column(db.String(40), nullable=False, unique=True)
15 | email = db.Column(db.String(255), nullable=False, unique=True)
16 | hashed_password = db.Column(db.String(255), nullable=False)
17 |
18 | saved = db.relationship("Saved", backref="User")
19 | tags = db.relationship('UserTag', backref='User')
20 |
21 | @property
22 | def password(self):
23 | return self.hashed_password
24 |
25 | @password.setter
26 | def password(self, password):
27 | self.hashed_password = generate_password_hash(password)
28 |
29 | def check_password(self, password):
30 | return check_password_hash(self.password, password)
31 |
32 | def to_dict(self):
33 | return {
34 | "id": self.id,
35 | "username": self.username,
36 | "email": self.email
37 | }
38 |
--------------------------------------------------------------------------------
/user-auth/react-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | news-connector
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import '../content.css'
3 | import './footer.css'
4 |
5 | const Footer = () => {
6 | return (
7 |
33 | )
34 | }
35 |
36 | export default Footer
37 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/store/weather.js:
--------------------------------------------------------------------------------
1 | // constants
2 | const GET_WEATHER = "weather/GET_WEATHER";
3 |
4 | // actions
5 | const loadWeather = (weather) => ({
6 | type: GET_WEATHER,
7 | weather,
8 | });
9 |
10 | // thunks
11 | export const getWeather = () => async (dispatch) => {
12 | try {
13 | const location = await fetch("https://ipapi.co/json/");
14 | const locData = await location.json();
15 |
16 | const { city, latitude, longitude } = locData;
17 | const loc = city + "+" + latitude + "+" + longitude;
18 | const res = await fetch(`/api/weather/${loc}`);
19 |
20 | if (res.ok) {
21 | const data = await res.json();
22 | dispatch(loadWeather(data));
23 | }
24 | } catch (error) {
25 | // if ip location is not found
26 | // set location to Miami
27 |
28 | const defaultLocation = 'Miami' + "+" + 25.7381 + "+" + -80.312;
29 | const defaultWeatherRes = await fetch(
30 | `/api/weather/${defaultLocation}`
31 | );
32 |
33 | const defaultWeather = await defaultWeatherRes.json();
34 |
35 | dispatch(loadWeather(defaultWeather));
36 | }
37 | };
38 |
39 | // reducer
40 | export default function WeatherReducer(state = {}, action) {
41 | switch (action.type) {
42 | case GET_WEATHER:
43 | return action.weather;
44 | default:
45 | return state;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/interests/interests.css:
--------------------------------------------------------------------------------
1 | .deleteBtn {
2 | /* display: inline-block; */
3 | margin: 0 5px;
4 | max-height: 21px;
5 | }
6 |
7 | .editBtn {
8 | margin: 0 5px;
9 | max-height: 21px;
10 | }
11 |
12 | .interests {
13 | display: flex;
14 | flex-direction: column;
15 | margin-bottom: 1rem;
16 | }
17 |
18 | .interestsContainer {
19 | /* display: flex; */
20 | margin: 0 auto;
21 | }
22 |
23 | .inputHTMLForm {
24 | /* padding-left: 15px; */
25 | }
26 |
27 | .interestListItem {
28 | text-align: left;
29 | display: flex;
30 | }
31 |
32 | .interestTitle {
33 | display: inline;
34 | /* text-align: left; */
35 | margin-right: .8rem;
36 | /* justify-self: ; */
37 | width: 119.76px;
38 | }
39 |
40 | .titleInput {
41 | /* color: blue; */
42 | }
43 |
44 | .interestFormListContainer {
45 | color: #222;
46 | display: inline;
47 | }
48 |
49 | .interestForm {
50 | display: flex;
51 | flex-direction: row;
52 | list-style: none;
53 | justify-content: flex-end;
54 | margin-bottom: .25rem;
55 | }
56 |
57 | .viewInterests {
58 | border-bottom: 1px solid #c3c1b4;
59 | padding-bottom: .5rem;
60 | color: #416894;
61 | align-self: center;
62 | }
63 |
64 | .viewInterests:hover {
65 | /* font-weight: bolder;
66 | color: #4b256b; */
67 | }
68 |
69 | .viewInterests:hover {
70 | cursor: pointer;
71 | text-decoration: underline;
72 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/store/articles.js:
--------------------------------------------------------------------------------
1 | // constants
2 | const READ_ARTICLES = 'articles/LOAD_ARTICLES'
3 |
4 |
5 | // actions
6 | const readArticles = (articles) => ({
7 | type: READ_ARTICLES,
8 | articles
9 | })
10 |
11 |
12 | // thunks
13 | export const loadArticles = (userId) => async (dispatch) => {
14 | const res = await fetch('/api/articles/')
15 |
16 | if (res.ok) {
17 | const data = await res.json()
18 | dispatch(readArticles(data))
19 | return data
20 | }
21 | }
22 |
23 |
24 | // reducer
25 | export default function ArticleReducer (state={}, action) {
26 | let newState = {...state}
27 |
28 | switch (action.type) {
29 | case READ_ARTICLES:
30 | // news api articles
31 | let articles = action.articles.articles.articles
32 | let gifs = action.articles.gifs
33 | let gifs1 = []
34 | for (let i = 0; i < gifs.length; i++) {
35 | let iGifs = gifs[i].data
36 | for (let j = 0; j < iGifs.length; j++) {
37 | let jGifs = iGifs[j]
38 | gifs1.push(jGifs)
39 | }
40 | }
41 | // console.log('!!!!!!READ_ARTICLES action', action)
42 | newState['articles'] = articles
43 | newState['gifs'] = gifs1
44 | newState['headlines'] = action.articles.headlines
45 | return newState
46 | default:
47 | return state
48 | }
49 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/saved/saved.css:
--------------------------------------------------------------------------------
1 | .addToSavedContainer {
2 | color: #000;
3 | margin-bottom: -.5rem;
4 | }
5 |
6 | .addToSavedText {
7 | border: .0625rem solid #656560;
8 | color: #416894;
9 | padding: .125rem;
10 | margin-top: .5rem;
11 | background-color: #C5C4BC;
12 | border-radius: .125rem;
13 | font-size: .75rem;
14 | }
15 |
16 | .addToSavedText:hover {
17 | cursor: pointer;
18 | /* font-weight: bold; */
19 | color: #f1f0eb;
20 | background-color: #416894;
21 | }
22 |
23 | .savedText {
24 | border-bottom: 1px solid #c3c1b4;
25 | padding-bottom: .5rem
26 | }
27 |
28 | .savedTitle {
29 | text-decoration: none;
30 | /* color: #7e3eb3; */
31 | color: #4974a5;
32 | display: flex;
33 | flex-direction: column;
34 | align-items: center;
35 | text-align: left;
36 | font-size: 1rem;
37 | padding: 0 2rem;
38 | }
39 |
40 | .savedSource {
41 | text-decoration: none;
42 | /* color: #7e3eb3; */
43 | color: #4974a5;
44 | display: flex;
45 | flex-direction: column;
46 | align-items: flex-start;
47 | text-align: left;
48 | font-size: .75rem;
49 | padding: 0 2rem;
50 | padding-top: .5rem;
51 | font-weight: 300;
52 | }
53 |
54 | .savedImage {
55 | max-width: 100%;
56 | border-radius: .25rem;
57 | align-items: center;
58 | text-align: center;
59 | }
60 |
61 | .savedTitle:hover {
62 | text-decoration: underline;
63 | }
64 |
65 | .savedSource:hover {
66 | text-decoration: underline;
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/articles/articles.css:
--------------------------------------------------------------------------------
1 | /* h2 {
2 | font-size: 16px;
3 | color: #7e3eb3;
4 | } */
5 |
6 | .articleTitle{
7 | /* font-size: 1rem; */
8 | }
9 |
10 |
11 | .contentImage {
12 | max-width: 100%;
13 | border-radius: .25rem;
14 | align-items: center;
15 | text-align: center;
16 | }
17 |
18 | .articleTitle {
19 | text-decoration: none;
20 | /* color: #7e3eb3; */
21 | color: #416894;
22 | display: flex;
23 | flex-direction: column;
24 | align-items: center;
25 | text-align: left;
26 | font-size: 1rem;
27 | padding-left: 2rem;
28 | letter-spacing: .03125rem;
29 | }
30 |
31 | .articleSource {
32 | text-decoration: none;
33 | /* color: #7e3eb3; */
34 | color: #416894;
35 | /* display: flex;
36 | flex-direction: column;
37 | align-items: flex-start;
38 | text-align: left; */
39 | font-size: .75rem;
40 | padding-left: 2rem;
41 | letter-spacing: .03125rem;
42 | font-weight: 300;
43 | padding-top: .5rem;
44 | }
45 |
46 | p {
47 | font-weight: bold;
48 | }
49 |
50 | .description {
51 | font-weight: 500;
52 | letter-spacing: .015625rem;
53 | word-wrap: break-word;
54 | text-align: left;
55 | padding: 1rem 0;
56 | max-width: 80%;
57 | font-size: .8rem;
58 | margin-bottom: -1rem;
59 | }
60 |
61 | .articleTitle:hover {
62 | /* font-weight: bolder; */
63 | /* color: #4b256b; */
64 | text-decoration: underline;
65 | }
66 |
67 | .articleSource:hover{
68 | text-decoration: underline;
69 | }
--------------------------------------------------------------------------------
/user-auth/redux-steps.md:
--------------------------------------------------------------------------------
1 | # Walkthrough & redux refactor
2 | 1. Delete the .git folder and push to my empty repo
3 | - if you have files in your repo already, just clone the repo and copy all of the files except the .git folder into your existing repo.
4 | 2. Go through steps in README
5 |
6 |
7 |
8 |
9 | ## Redux Refactor
10 | 1. install redux, redux-thunk, react-redux, redux-logger
11 | 2. set up your store
12 | - make store folder
13 | - in index.js, create your root reducer and set up redux middleware
14 | - export your configure store function
15 | 3. create your store
16 | - top level index.js file, create store and wrap the provider around your app
17 | 4. create a reducer for auth
18 | 5. create constants
19 | 6. create action-creators
20 | 7. create thunks (replace each of the functions in the "services" folder with a thunk version)
21 | 8. replace all my state based logic for auth with the use of my redux store
22 | - anytime i use a value from a useState, i will want to use useSelector
23 | - anytime i use the function to change the state, i will want to use a dispatch
24 |
25 | ```javascript
26 | // if i am refactoring some piece of state to use redux...
27 | const [ myState, setMyState ] = useState(null);
28 |
29 | // any time i see that i'm using the "myState" value, i want to use a selector instead to get the value from my redux store
30 | const myState = useSelector( state => state.stateSlice.myState )
31 |
32 | // any time i see that i'm using "setMyState" to change the value of myState
33 | // I will need to dispatch a redux thunk or action creator instead
34 | dispatch(updateMyState(newState));
35 |
36 | // anytime i see that i am ONLY using the value to pass it as a prop
37 | // i should just remove it from that component all together
38 | ```
39 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/saved/SavedList.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect} from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { loadSavedArticles } from '../../store/saved'
4 | import RemoveFromSaved from './RemoveFromSaved'
5 | import '../content.css'
6 | import '../articles/articles.css'
7 |
8 | const SavedList = () => {
9 | const dispatch = useDispatch()
10 | const user = useSelector(state => state.session.user)
11 | const savedArticles = useSelector(state => state.saved)
12 |
13 | useEffect(() => {
14 | if(!user) return
15 | dispatch(loadSavedArticles())
16 | }, [dispatch])
17 |
18 | if (!user) {
19 | return null
20 | }
21 |
22 | return (
23 |
24 |
Saved
25 | {savedArticles && Object.values(savedArticles).map(item => {
26 | return (
27 |
28 |
34 |
35 |
36 |
37 | {item.description && (
{item.description}
)}
38 |
39 |
40 | )
41 | })}
42 |
43 | )
44 | }
45 |
46 | export default SavedList
47 |
--------------------------------------------------------------------------------
/user-auth/app/api/saved_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request
2 | from flask_login import current_user
3 | from app.models import db, Saved
4 | from app.forms import SavedForm
5 |
6 | saved_routes = Blueprint('saved', __name__)
7 |
8 |
9 | #return all articles that a given user has saved
10 | @saved_routes.route('/', methods=["GET"])
11 | def get_saved_articles():
12 | saved_articles = Saved.query.filter_by(userId=current_user.id).all()
13 | return {"saved":[article.to_dict() for article in saved_articles]}
14 |
15 |
16 | # adds an article to the current user's saved articles
17 | @saved_routes.route('/', methods=["POST"])
18 | def post_to_saved():
19 | form = SavedForm()
20 | form['csrf_token'].data = request.cookies['csrf_token']
21 | if form.validate_on_submit():
22 | new_article = Saved(
23 | userId = form.data['userId'],
24 | itemURL = form.data['itemURL'],
25 | imageURL = form.data['imageURL'],
26 | title = form.data['title'],
27 | description = form.data['description'],
28 | contentSource = form.data['contentSource'],
29 | publishedTime = form.data['publishedTime']
30 | )
31 | db.session.add(new_article)
32 | db.session.commit()
33 | return new_article.to_dict()
34 |
35 |
36 | # returns single saved article for the given article id
37 | @saved_routes.route('/', methods=["GET"])
38 | def get_saved(id):
39 | saved = Saved.query.filter_by(id = id).first()
40 |
41 | return {"saved": saved.to_dict()}
42 |
43 |
44 | # deletes an article from the user's saved articles list
45 | @saved_routes.route('/', methods=['DELETE'])
46 | def delete_from_saved():
47 | form = SavedForm()
48 | itemURL = form.data['itemURL']
49 |
50 | article = Saved.query.filter_by(itemURL = itemURL).first()
51 |
52 | db.session.delete(article)
53 | db.session.commit()
54 |
55 | return article.to_dict()
56 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { BrowserRouter, Route, Switch } from "react-router-dom";
3 | import { useDispatch } from "react-redux";
4 | import LoginForm from "./components/auth/LoginForm";
5 | import SignUpForm from "./components/auth/SignUpForm";
6 | import NavBar from "./components/NavBar";
7 | import ProtectedRoute from "./components/auth/ProtectedRoute";
8 | import Content from "./components/content/Content";
9 | import Interests from "./components/interests/Interests";
10 | import Title from "./components/title/Title";
11 | import SavedList from "./components/saved/SavedList";
12 | import Weather from "./components/weather/Weather";
13 | import { authenticate } from "./store/session";
14 | import Footer from "./components/footer/Footer";
15 | import {PageView, initGA} from '../src/components/Google Analytics/GoogleAnalytics.js'
16 |
17 | function App() {
18 | const dispatch = useDispatch();
19 | const [loaded, setLoaded] = useState(false);
20 |
21 | useEffect(() => {
22 | (async () => {
23 | await dispatch(authenticate());
24 | setLoaded(true);
25 | })();
26 |
27 | // initialize ReactGA
28 | initGA("UA-213353187-1");
29 | PageView();
30 | }, []);
31 |
32 | if (!loaded) {
33 | return null;
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | export default App;
59 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/saved/AddToSaved.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { addToSaved, removeFromSaved } from '../../store/saved';
4 | import '../content.css'
5 | import './saved.css'
6 |
7 | const AddToSaved = ({ item }) => {
8 | const dispatch = useDispatch()
9 | const user = useSelector(state => state.session.user)
10 | let userId = user['id']
11 | const savedArticles = useSelector(state => state.saved)
12 | const [savedText, setSavedText] = useState('Add to Saved')
13 |
14 |
15 | useEffect(() => {
16 | //if item is in store, set text to Remove from Saved
17 | Object.values(savedArticles).forEach(article => {
18 | if (article['itemURL'] === item['url']) {
19 | setSavedText('Remove from Saved')
20 | }
21 | })
22 | }, [])
23 |
24 | const handleAddToSaved = () => {
25 | //todo
26 | //check if article is already saved first
27 | let { title, itemURL, imageURL, description, contentSource, publishedAt=null } = item
28 | if (savedText === 'Remove from Saved') {
29 | dispatch(removeFromSaved(userId, itemURL, imageURL, title, description, contentSource))
30 | setSavedText('Add to Saved')
31 | return
32 | }
33 |
34 | if (savedText === 'Add to Saved') {
35 | dispatch(addToSaved(userId, itemURL, imageURL, title, description, contentSource, publishedAt))
36 | setSavedText('Remove from Saved')
37 | return
38 | }
39 |
40 | return
41 | // change text to display if item is saved or not
42 | // savedText === 'Add to Saved' ? setSavedText('Remove from Saved') : setSavedText('Add to Saved')
43 | }
44 | return (
45 |
46 |
handleAddToSaved()}
49 | >
50 | {savedText}
51 |
52 |
53 | )
54 | }
55 |
56 | export default AddToSaved
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/content/Content.jsx:
--------------------------------------------------------------------------------
1 | // this is the
2 |
3 | import React, { useEffect } from 'react'
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { loadArticles } from "../../store/articles";
6 | import Article from '../articles/Article';
7 | import Gif from '../gifs/Gif';
8 | import Default from '../default/Default';
9 | import Splash from '../splash/Splash';
10 |
11 | import '../content.css'
12 | import Headline from '../articles/Headline';
13 |
14 | const Content = () => {
15 | const dispatch = useDispatch()
16 | const user = useSelector(state => state.session.user)
17 | const content = useSelector(state => state.article)
18 | const articles = content.articles
19 | const headlines = content.headlines
20 | const gifs = content.gifs
21 | let userId
22 |
23 | if (user) {
24 | userId = user['id']
25 | }
26 |
27 | useEffect(() => {
28 | if (!userId) return
29 | dispatch(loadArticles(userId))
30 | }, [dispatch])
31 |
32 | if (!user) {
33 | // return
34 | // instead of returning a garbage splash page, make it nice like
35 | return
36 | }
37 |
38 | return (
39 |
40 | {/* render headlines first */}
41 | {user && headlines && headlines.map((article) => {
42 | return (
43 |
44 |
45 |
46 | )
47 | })}
48 | {user && articles && articles.map((article, idx) => {
49 | return (
50 |
51 |
52 | {idx % 2 === 0 && gifs[idx / 2] &&
}
53 |
54 | )
55 | })}
56 |
57 |
58 | )
59 | }
60 |
61 | export default Content
62 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/auth/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Redirect } from "react-router-dom";
4 | import { login } from "../../store/session";
5 | import './loginForm.css'
6 |
7 | const LoginForm = () => {
8 | const dispatch = useDispatch();
9 | const user = useSelector(state => state.session.user)
10 | const [errors, setErrors] = useState([]);
11 | const [email, setEmail] = useState("");
12 | const [password, setPassword] = useState("");
13 |
14 | const onLogin = async (e) => {
15 | e.preventDefault();
16 | const data = await dispatch(login(email, password));
17 | if (data.errors) {
18 | setErrors(data.errors);
19 | }
20 | };
21 |
22 | const updateEmail = (e) => {
23 | setEmail(e.target.value);
24 | };
25 |
26 | const updatePassword = (e) => {
27 | setPassword(e.target.value);
28 | };
29 |
30 | if (user) {
31 | return ;
32 | }
33 |
34 | return (
35 |
69 | );
70 | };
71 |
72 | export default LoginForm;
73 |
--------------------------------------------------------------------------------
/user-auth/app/api/article_routes.py:
--------------------------------------------------------------------------------
1 | from re import T
2 | from flask import Blueprint, jsonify, request
3 | from flask_login import current_user
4 | import requests, json, random, os
5 | from app.models import db, Interest, Tag
6 |
7 | article_routes = Blueprint('articles', __name__)
8 |
9 | @article_routes.route('/', methods=['GET'])
10 | def get_articles():
11 | # res = requests.get(
12 | # 'https://newsapi.org/v2/everything?q=Apple&from=2021-06-26&sortBy=popularity&apiKey=13bc774f3bb545d8935600ca47e4cfcf')
13 |
14 | # get titles from user's interests
15 | interestsFromDB = Interest.query.filter_by(userId = current_user.id).all()
16 | interests = [title.to_dict()['title'] for title in interestsFromDB]
17 | random.shuffle(interests)
18 |
19 | # get titles from tags
20 | tagsFromDB = Tag.query.all()
21 | tags = [tag.to_dict()['title'] for tag in tagsFromDB]
22 | random.shuffle(tags)
23 |
24 | # merge titles from tags and interests into one list
25 | titles = interests + tags
26 | random.shuffle(titles)
27 |
28 | # 100 requests per day available
29 | news_api_key = os.environ.get('NEWS_API')
30 |
31 | news_url = ('https://newsapi.org/v2/everything?q=' + ' OR '.join(titles)
32 | ) + '&language=en' + '&apiKey=' + news_api_key + '&pageSize=100'
33 |
34 | news_res = requests.get(news_url)
35 | articles = news_res.json()['articles']
36 | random.shuffle(articles)
37 |
38 | # get headlines
39 | headline_url = 'https://newsapi.org/v2/top-headlines?country=us' + '&language=en' + '&apiKey=' + news_api_key + '&pageSize=100'
40 | headline_res = requests.get(headline_url)
41 | headlines = headline_res.json()['articles']
42 |
43 | # Note: All API Keys start as beta keys, which are rate limited(42 reads per hour and 1000 searches/API calls per day.)
44 | giphy_api_key = os.environ.get('GIPHY_API')
45 |
46 | gifs = []
47 | for i in range(len(interests)):
48 | giphy_url = 'https://api.giphy.com/v1/gifs/search?api_key=' + giphy_api_key + '&q=' + interests[i] + '&limit=5&offset=0&rating=g&lang=en'
49 | giphy_res = requests.get(giphy_url)
50 | gifs.append(giphy_res.json())
51 |
52 |
53 | articles = {'articles': articles}
54 | return {'gifs': gifs, 'articles': articles, 'headlines': headlines}
55 |
--------------------------------------------------------------------------------
/user-auth/app/api/interest_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request
2 | from flask_login import current_user
3 | from app.models import db, Interest
4 | from app.forms import InterestForm
5 | from app.forms.update_interest_form import UpdateInterestForm
6 |
7 | interest_routes = Blueprint('interests', __name__)
8 |
9 | # return all of the user's user-created interests
10 |
11 |
12 | @interest_routes.route('/', methods=['GET'])
13 | def get_interests():
14 | interests = Interest.query.filter_by(userId=current_user.id).all()
15 | return {"interests": [titles.to_dict() for titles in interests]}
16 |
17 |
18 | # return the interest with the given id
19 | @interest_routes.route('/', methods=["GET"])
20 | def get_one_interest(id):
21 | interest = Interest.query.filter_by(id=id).first()
22 | return interest.to_dict()
23 |
24 |
25 | # add and interest to view within the newsfeed
26 | @interest_routes.route('/', methods=["POST"])
27 | def post_interests():
28 | form = InterestForm()
29 | form['csrf_token'].data = request.cookies['csrf_token']
30 | if form.validate_on_submit():
31 | new_interest = Interest(
32 | userId=form.data['userId'],
33 | title=form.data['title']
34 | )
35 | db.session.add(new_interest)
36 | db.session.commit()
37 |
38 | return new_interest.to_dict()
39 |
40 |
41 | # update the interest with the given id
42 | @interest_routes.route('/', methods=['PUT'])
43 | def update_interest():
44 | form = UpdateInterestForm()
45 | form['csrf_token'].data = request.cookies['csrf_token']
46 | if form.validate_on_submit():
47 | interestId = form.data["interestId"]
48 | interest = Interest.query.filter_by(id=interestId).\
49 | update(dict({'title': form.data['title']}))
50 | updated_interest = Interest.query.filter_by(id=interestId).first()
51 | db.session.commit()
52 | return updated_interest.to_dict()
53 |
54 |
55 | # delete the interest with the given id from the user's interests
56 | @interest_routes.route('/', methods=['DELETE'])
57 | def delete_interest():
58 | form = UpdateInterestForm()
59 | interestId = form.data["interestId"]
60 | interest = Interest.query.filter_by(id=interestId).first()
61 | db.session.delete(interest)
62 | db.session.commit()
63 | return interest.to_dict()
64 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/store/session.js:
--------------------------------------------------------------------------------
1 | // constants
2 | const SET_USER = "session/SET_USER"
3 | const REMOVE_USER = "session/REMOVE_USER"
4 |
5 | // action creators
6 | const setUser = (user) => ({
7 | type: SET_USER,
8 | payload: user
9 | })
10 |
11 | const removeUser = () => ({
12 | type: REMOVE_USER,
13 | })
14 |
15 | // thunks
16 |
17 | export const authenticate = () => async (dispatch) => {
18 | const response = await fetch('/api/auth/', {
19 | headers: {
20 | 'Content-Type': 'application/json'
21 | }
22 | });
23 | const data = await response.json();
24 | if (data.errors) {
25 | return;
26 | }
27 | dispatch(setUser(data))
28 | }
29 |
30 | export const login = (email, password) => async (dispatch) => {
31 | const response = await fetch('/api/auth/login', {
32 | method: 'POST',
33 | headers: {
34 | 'Content-Type': 'application/json'
35 | },
36 | body: JSON.stringify({
37 | email,
38 | password
39 | })
40 | });
41 | const data = await response.json();
42 | if (data.errors) {
43 | return data;
44 | }
45 | dispatch(setUser(data))
46 | return {}
47 | }
48 |
49 | export const logout = () => async (dispatch) => {
50 | const response = await fetch("/api/auth/logout", {
51 | headers: {
52 | "Content-Type": "application/json",
53 | }
54 | });
55 | const data = await response.json();
56 | dispatch(removeUser());
57 | };
58 |
59 |
60 | export const signUp = (username, email, password) => async (dispatch) => {
61 | const response = await fetch("/api/auth/signup", {
62 | method: "POST",
63 | headers: {
64 | "Content-Type": "application/json",
65 | },
66 | body: JSON.stringify({
67 | username,
68 | email,
69 | password,
70 | }),
71 | });
72 | const data = await response.json();
73 | if (data.errors) {
74 | return
75 | }
76 | dispatch(setUser(data))
77 | }
78 |
79 |
80 |
81 | const initialState = {user: null}
82 |
83 | export default function reducer(state = initialState, action) {
84 | switch (action.type) {
85 | case SET_USER:
86 | return {user: action.payload}
87 | case REMOVE_USER:
88 | return {user: null}
89 | default:
90 | return state;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/user-auth/app/api/auth_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, session, request
2 | from app.models import User, db
3 | from app.forms import LoginForm
4 | from app.forms import SignUpForm
5 | from flask_login import current_user, login_user, logout_user, login_required
6 |
7 | auth_routes = Blueprint('auth', __name__)
8 |
9 |
10 | def validation_errors_to_error_messages(validation_errors):
11 | """
12 | Simple function that turns the WTForms validation errors into a simple list
13 | """
14 | errorMessages = []
15 | for field in validation_errors:
16 | for error in validation_errors[field]:
17 | errorMessages.append(f"{field} : {error}")
18 | return errorMessages
19 |
20 |
21 | @auth_routes.route('/')
22 | def authenticate():
23 | """
24 | Authenticates a user.
25 | """
26 | if current_user.is_authenticated:
27 | return current_user.to_dict()
28 | return {'errors': ['Unauthorized']}
29 |
30 |
31 | @auth_routes.route('/login', methods=['POST'])
32 | def login():
33 | """
34 | Logs a user in
35 | """
36 | form = LoginForm()
37 | print(request.get_json())
38 | # Get the csrf_token from the request cookie and put it into the
39 | # form manually to validate_on_submit can be used
40 | form['csrf_token'].data = request.cookies['csrf_token']
41 | if form.validate_on_submit():
42 | # Add the user to the session, we are logged in!
43 | user = User.query.filter(User.email == form.data['email']).first()
44 | login_user(user)
45 | return user.to_dict()
46 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
47 |
48 |
49 | @auth_routes.route('/logout')
50 | def logout():
51 | """
52 | Logs a user out
53 | """
54 | logout_user()
55 | return {'message': 'User logged out'}
56 |
57 |
58 | @auth_routes.route('/signup', methods=['POST'])
59 | def sign_up():
60 | """
61 | Creates a new user and logs them in
62 | """
63 | form = SignUpForm()
64 | form['csrf_token'].data = request.cookies['csrf_token']
65 | if form.validate_on_submit():
66 | user = User(
67 | username=form.data['username'],
68 | email=form.data['email'],
69 | password=form.data['password']
70 | )
71 | db.session.add(user)
72 | db.session.commit()
73 | login_user(user)
74 | return user.to_dict()
75 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
76 |
77 |
78 | @auth_routes.route('/unauthorized')
79 | def unauthorized():
80 | """
81 | Returns unauthorized JSON when flask-login authentication fails
82 | """
83 | return {'errors': ['Unauthorized']}, 401
84 |
--------------------------------------------------------------------------------
/user-auth/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | # Cython debug symbols
138 | cython_debug/
139 |
140 | react-app/node_modules
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/weather/getDays.js:
--------------------------------------------------------------------------------
1 | export const getDays = () => {
2 | const options = { weekday: 'long' }
3 | const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
4 | const day = new Date().getDay()
5 | const week = {}
6 |
7 | switch (day) {
8 | case 0:
9 | //sunday
10 | week[0] = 'Sunday'
11 | week[1] = 'Monday'
12 | week[2] = 'Tuesday'
13 | week[3] = 'Wednesday'
14 | week[4] = 'Thursday'
15 | week[5] = 'Friday'
16 | week[6] = 'Saturday'
17 | break;
18 | case 1:
19 | //monday
20 | week[0] = 'Monday'
21 | week[1] = 'Tuesday'
22 | week[2] = 'Wednesday'
23 | week[3] = 'Thursday'
24 | week[4] = 'Friday'
25 | week[5] = 'Saturday'
26 | week[6] = 'Sunday'
27 | break;
28 | case 2:
29 | //tuesday
30 | week[0] = 'Tuesday'
31 | week[1] = 'Wednesday'
32 | week[2] = 'Thursday'
33 | week[3] = 'Friday'
34 | week[4] = 'Saturday'
35 | week[5] = 'Sunday'
36 | week[6] = 'Monday'
37 | break;
38 | case 3:
39 | //wednesday
40 | week[0] = 'Wednesday'
41 | week[1] = 'Thursday'
42 | week[2] = 'Friday'
43 | week[3] = 'Saturday'
44 | week[4] = 'Sunday'
45 | week[5] = 'Monday'
46 | week[6] = 'Tuesday'
47 | break;
48 | case 4:
49 | //thursday
50 | week[0] = 'Thursday'
51 | week[1] = 'Friday'
52 | week[2] = 'Saturday'
53 | week[3] = 'Sunday'
54 | week[4] = 'Monday'
55 | week[5] = 'Tuesday'
56 | week[6] = 'Wednesday'
57 | break;
58 | case 5:
59 | //friday
60 | week[0] = 'Friday'
61 | week[1] = 'Saturday'
62 | week[2] = 'Sunday'
63 | week[3] = 'Monday'
64 | week[4] = 'Tuesday'
65 | week[5] = 'Wednesday'
66 | week[6] = 'Thursday'
67 | break;
68 | case 6:
69 | //saturday
70 | week[0] = 'Saturday'
71 | week[1] = 'Sunday'
72 | week[2] = 'Monday'
73 | week[3] = 'Tuesday'
74 | week[4] = 'Wednesday'
75 | week[5] = 'Thursday'
76 | week[6] = 'Friday'
77 | break;
78 | }
79 |
80 |
81 | return week
82 | }
83 |
84 | export const getAbbrDays = (week) => {
85 | let abbrWeek = {}
86 |
87 | Object.values(week).forEach((day, i) => {
88 | let abbrDay = day.slice(0, 3)
89 | abbrWeek[i] = abbrDay
90 | })
91 |
92 | return abbrWeek
93 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/weather/weather.css:
--------------------------------------------------------------------------------
1 | .weatherParent {
2 | max-width: 100%;
3 | height: 500px;
4 | }
5 |
6 | .weatherContainer {
7 | max-height: 90%;
8 | border-radius: .25rem;
9 | display: grid;
10 | background-color: #cccbc1;
11 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
12 | grid-template-rows: .5fr .5fr 1fr;
13 | grid-template-areas:
14 | "today today today today today today today"
15 | "todayInner todayInner todayInner todayInner todayImg todayImg todayImg"
16 | "weekDay weekDay weekDay weekDay weekDay weekDay weekDay";
17 | }
18 |
19 | .weatherToday {
20 | grid-area: today;
21 | display: flex;
22 | flex-direction: column;
23 | max-width: 100%;
24 | justify-self: flex-start;
25 | padding-top: .5rem;
26 | }
27 |
28 | .city {
29 | align-items: center;
30 | display: flex;
31 | justify-content: space-evenly;
32 | margin-bottom: 2rem;
33 | padding-left: 1rem;
34 | }
35 |
36 | .cityName {
37 | /* color: #2D2D2B; */
38 | letter-spacing: 1px;
39 | font-weight: bolder;
40 | font-size: larger;
41 | border-bottom: 1px solid #c3c1b4;
42 | padding-bottom: 1rem;
43 |
44 | }
45 |
46 | .currentTemp {
47 | margin: 0 auto;
48 | margin-bottom: 2rem;
49 | letter-spacing: 2px;
50 | color: #323232;
51 | }
52 |
53 | .todayImage {
54 | grid-area: todayImg;
55 | margin-top: -3rem;
56 | margin-left: 3rem;
57 | }
58 |
59 | .weekImage {
60 | color: white;
61 | margin-bottom: -1rem;
62 | max-width: 90%;
63 | }
64 |
65 | .todayDescription {
66 | align-self: center;
67 | margin-top: -1rem;
68 | font-weight: 500;
69 | letter-spacing: .03125;
70 |
71 | }
72 |
73 | .weatherWeek {
74 | /* grid-area: weekDay; */
75 | display: flex;
76 | justify-content: space-around;
77 | align-items: center;
78 | flex-direction: column;
79 | /* align-content: flex-end; */
80 | max-width: 100%;
81 | padding-bottom: .5rem;
82 | margin-top: -1rem;
83 | }
84 |
85 | .day {
86 | color: #323232;
87 | font-weight: 700;
88 | letter-spacing: 2px;
89 | font-size: smaller;
90 | padding-left: .25rem;
91 | margin-bottom: -.25rem;
92 | }
93 |
94 | .dayHigh {
95 | color: black;
96 | letter-spacing: 2px;
97 | font-weight: bolder;
98 | margin-bottom: -1rem;
99 | font-size: .8rem;
100 | padding-bottom: .5rem;
101 | }
102 |
103 | .dayLow {
104 | color: #323232;
105 | letter-spacing: 2px;
106 | font-weight: bolder;
107 | font-size: .8rem;
108 | }
109 |
110 | .todayDivOuter {
111 | display: flex;
112 | flex-direction: row;
113 | }
114 |
115 | .todayDivInner {
116 | grid-area: todayInner;
117 | padding-left: 1rem;
118 | margin-top: -1rem;
119 | }
--------------------------------------------------------------------------------
/SETUP INSTRUCTIONS.md:
--------------------------------------------------------------------------------
1 | # Flask React Project
2 |
3 | This is the backend for the Flask React project.
4 |
5 | ## Getting started
6 |
7 | 1. Clone this repository (only this branch)
8 |
9 | ```bash
10 | git clone https://github.com/appacademy-starters/python-project-starter.git
11 | ```
12 |
13 | 2. Install dependencies
14 |
15 | ```bash
16 | pipenv install --dev -r dev-requirements.txt && pipenv install -r requirements.txt
17 | ```
18 |
19 | 3. Create a **.env** file based on the example with proper settings for your
20 | development environment
21 | 4. Setup your PostgreSQL user, password and database and make sure it matches your **.env** file
22 |
23 | 5. Get into your pipenv, migrate your database, seed your database, and run your flask app
24 |
25 | ```bash
26 | pipenv shell
27 | ```
28 |
29 | ```bash
30 | flask db upgrade
31 | ```
32 |
33 | ```bash
34 | flask seed all
35 | ```
36 |
37 | ```bash
38 | flask run
39 | ```
40 |
41 | 6. To run the React App in development, checkout the [README](./react-app/README.md) inside the `react-app` directory.
42 |
43 | ***
44 | *IMPORTANT!*
45 | If you add any python dependencies to your pipfiles, you'll need to regenerate your requirements.txt before deployment.
46 | You can do this by running:
47 |
48 | ```bash
49 | pipenv lock -r > requirements.txt
50 | ```
51 |
52 | *ALSO IMPORTANT!*
53 | psycopg2-binary MUST remain a dev dependency because you can't install it on apline-linux.
54 | There is a layer in the Dockerfile that will install psycopg2 (not binary) for us.
55 | ***
56 |
57 | ## Deploy to Heroku
58 |
59 | 1. Create a new project on Heroku
60 | 2. Under Resources click "Find more add-ons" and add the add on called "Heroku Postgres"
61 | 3. Install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line)
62 | 4. Run
63 |
64 | ```bash
65 | heroku login
66 | ```
67 |
68 | 5. Login to the heroku container registry
69 |
70 | ```bash
71 | heroku container:login
72 | ```
73 |
74 | 6. Update the `REACT_APP_BASE_URL` variable in the Dockerfile.
75 | This should be the full URL of your Heroku app: i.e. "https://flask-react-aa.herokuapp.com"
76 | 7. Push your docker container to heroku from the root directory of your project.
77 | This will build the dockerfile and push the image to your heroku container registry
78 |
79 | ```bash
80 | heroku container:push web -a {NAME_OF_HEROKU_APP}
81 | ```
82 |
83 | 8. Release your docker container to heroku
84 |
85 | ```bash
86 | heroku container:release web -a {NAME_OF_HEROKU_APP}
87 | ```
88 |
89 | 9. set up your database:
90 |
91 | ```bash
92 | heroku run -a {NAME_OF_HEROKU_APP} flask db upgrade
93 | heroku run -a {NAME_OF_HEROKU_APP} flask seed all
94 | ```
95 |
96 | 10. Under Settings find "Config Vars" and add any additional/secret .env variables.
97 |
98 | 11. profit
99 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/auth/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Redirect } from 'react-router-dom';
4 | import { signUp } from '../../store/session';
5 | import './signUpForm.css'
6 |
7 | const SignUpForm = () => {
8 | const dispatch = useDispatch();
9 | const user = useSelector(state => state.session.user)
10 | const [username, setUsername] = useState("");
11 | const [email, setEmail] = useState("");
12 | const [password, setPassword] = useState("");
13 | const [repeatPassword, setRepeatPassword] = useState("");
14 |
15 | const onSignUp = async (e) => {
16 | e.preventDefault();
17 | if (password === repeatPassword) {
18 | const data = await dispatch(signUp(username, email, password));
19 | }
20 | };
21 |
22 | const updateUsername = (e) => {
23 | setUsername(e.target.value);
24 | };
25 |
26 | const updateEmail = (e) => {
27 | setEmail(e.target.value);
28 | };
29 |
30 | const updatePassword = (e) => {
31 | setPassword(e.target.value);
32 | };
33 |
34 | const updateRepeatPassword = (e) => {
35 | setRepeatPassword(e.target.value);
36 | };
37 |
38 | if (user) {
39 | return ;
40 | }
41 |
42 | return (
43 |
89 | );
90 | };
91 |
92 | export default SignUpForm;
93 |
--------------------------------------------------------------------------------
/user-auth/app/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Flask, render_template, request, session, redirect
3 | from flask_cors import CORS
4 | from flask_migrate import Migrate
5 | from flask_wtf.csrf import CSRFProtect, generate_csrf
6 | from flask_login import LoginManager
7 |
8 | from .models import db, User
9 | from .api.user_routes import user_routes
10 | from .api.auth_routes import auth_routes
11 | from .api.interest_routes import interest_routes
12 | from .api.saved_routes import saved_routes
13 | from .api.tag_routes import tag_routes
14 | from .api.article_routes import article_routes
15 | from .api.default_routes import default_routes
16 | from .api.weather_routes import weather_routes
17 |
18 | from .seeds import seed_commands
19 |
20 | from .config import Config
21 |
22 | app = Flask(__name__)
23 |
24 | # Setup login manager
25 | login = LoginManager(app)
26 | login.login_view = 'auth.unauthorized'
27 |
28 |
29 | @login.user_loader
30 | def load_user(id):
31 | return User.query.get(int(id))
32 |
33 |
34 | # Tell flask about our seed commands
35 | app.cli.add_command(seed_commands)
36 |
37 | app.config.from_object(Config)
38 | app.register_blueprint(user_routes, url_prefix='/api/users')
39 | app.register_blueprint(auth_routes, url_prefix='/api/auth')
40 | app.register_blueprint(interest_routes, url_prefix='/api/interests')
41 | app.register_blueprint(tag_routes, url_prefix='/api/tags')
42 | app.register_blueprint(saved_routes, url_prefix='/api/saved')
43 | app.register_blueprint(article_routes, url_prefix='/api/articles')
44 | app.register_blueprint(default_routes, url_prefix='/api/homeData')
45 | app.register_blueprint(weather_routes, url_prefix='/api/weather')
46 | db.init_app(app)
47 | Migrate(app, db)
48 |
49 | # Application Security
50 | CORS(app)
51 |
52 | # Since we are deploying with Docker and Flask,
53 | # we won't be using a buildpack when we deploy to Heroku.
54 | # Therefore, we need to make sure that in production any
55 | # request made over http is redirected to https.
56 | # Well.........
57 |
58 | @app.before_request
59 | def https_redirect():
60 | if os.environ.get('FLASK_ENV') == 'production':
61 | if request.headers.get('X-Forwarded-Proto') == 'http':
62 | url = request.url.replace('http://', 'https://', 1)
63 | code = 301
64 | return redirect(url, code=code)
65 |
66 |
67 | @app.after_request
68 | def inject_csrf_token(response):
69 | response.set_cookie('csrf_token',
70 | generate_csrf(),
71 | secure=True if os.environ.get(
72 | 'FLASK_ENV') == 'production' else False,
73 | samesite='Strict' if os.environ.get(
74 | 'FLASK_ENV') == 'production' else None,
75 | httponly=True)
76 | return response
77 |
78 |
79 | @app.route('/', defaults={'path': ''})
80 | @app.route('/')
81 | def react_root(path):
82 | print("path", path)
83 | if path == 'favicon.ico':
84 | return app.send_static_file('favicon.ico')
85 | return app.send_static_file('index.html')
86 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/content.css:
--------------------------------------------------------------------------------
1 | body {
2 | /* http://www.colorhunter.com/tag/newspaper/1 */
3 | /* background-color: #e3e2d7; */
4 | background-color: #f1f0eb;
5 | box-sizing: border-box;
6 | margin: 0;
7 | padding: 0;
8 | width: 100%;
9 | overflow-x: hidden;
10 | }
11 |
12 | #root {
13 | display: grid;
14 | grid-template-areas:
15 | "navContainer navContainer navContainer"
16 | "titleContainer titleContainer titleContainer"
17 | "interests parent saved"
18 | "interests parent saved"
19 | "foot foot foot";
20 | grid-template-columns: 1fr 2fr 1fr;
21 | width: 100%;
22 | font-family: sans-serif;
23 | letter-spacing: .03125rem;
24 | }
25 |
26 | .parent {
27 | grid-area: parent;
28 | display: flex;
29 | flex-direction: column;
30 | align-items: center;
31 | border-top: 1px solid #C3C1B4;
32 | /* align-self: center; */
33 | padding: 0 5rem;
34 | }
35 |
36 | .navContainer {
37 | grid-area: navContainer;
38 | padding-bottom: 1rem;
39 | border-bottom: 1px solid #C3C1B4;
40 | }
41 |
42 | .footer {
43 | grid-area: foot;
44 | /* padding-top: 1rem;
45 | padding-bottom: -5rem; */
46 | border-top: 1px solid #C3C1B4;
47 | margin-top: 5rem;
48 | height: 50px;
49 | /* margin-bottom: -10rem; */
50 |
51 | }
52 |
53 | .titleContainer {
54 | grid-area: titleContainer;
55 | }
56 |
57 | .interestsOuterMost {
58 | grid-area: interests;
59 | margin: 0 auto;
60 | /* display: flex;
61 | width: 100%;
62 | flex-direction: column;
63 | align-items: center;
64 | margin-top: 1rem;
65 | margin-left: 2.5rem; */
66 | }
67 |
68 | .weatherParent {
69 | grid-area: weather;
70 | }
71 |
72 | .saved {
73 | grid-area: saved;
74 | display: flex;
75 | flex-direction: column;
76 | align-items: center;
77 | border-left: 3px solid #c3c1b4;
78 | }
79 |
80 | .container {
81 | display: flex;
82 | flex-direction: column;
83 | align-items: center;
84 | width: 75%;
85 | }
86 |
87 | .content-container {
88 | display: flex;
89 | width: 66%;
90 | flex-direction: column;
91 | align-items: center;
92 | /* background-color: #ADACA0; */
93 | background-color: #cccbc1;
94 | border-radius: .25rem;
95 | margin: .5rem 0;
96 | padding-bottom: 1rem;
97 | overflow: hidden;
98 | }
99 |
100 | /*
101 | content
102 | div
103 | (
104 | div className="container"
105 | (
106 | article
107 | (
108 | div className="content article"
109 | (
110 | h2
111 | img
112 | p
113 | )
114 |
115 | )
116 | gif
117 | (
118 | div className="content gif"
119 | (
120 | img
121 | a
122 | )
123 | )
124 | )
125 | )
126 |
127 | */
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/interests/Interests.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { addInterest, getAllInterests } from "../../store/interests";
4 | import UpdateInterests from "./UpdateInterests";
5 | import '../content.css'
6 | import './interests.css'
7 | import Weather from "../weather/Weather";
8 |
9 | const Interests = () => {
10 | const dispatch = useDispatch()
11 | const [title, setTitle] = useState('')
12 | const [viewInterests, setViewInterests] = useState('Hide')
13 | const user = useSelector((state) => state.session.user);
14 | let allInterests = useSelector(state => state.interest)
15 | let userId
16 | if (user) {
17 | userId = user["id"];
18 | }
19 |
20 | useEffect(() => {
21 | if (!userId) return
22 | dispatch(getAllInterests())
23 | }, [dispatch, user])
24 |
25 | // allInterests = useSelector(state => state.interest)
26 |
27 | const handleSubmit = (e) => {
28 | e.preventDefault()
29 | dispatch(addInterest(userId, title))
30 | setTitle('')
31 | window.location.reload()
32 | }
33 |
34 | if (!user) {
35 | return null
36 | }
37 |
38 | return (
39 |
40 |
41 |
viewInterests === 'View' ? setViewInterests('Hide') : setViewInterests('View')}
45 | >
46 | {viewInterests} Your Interests
47 |
48 | {viewInterests === 'Hide' && !user && (
49 |
Log in to add interests to your newsfeed!
50 | )}
51 | {viewInterests === 'Hide' && user && (
52 |
53 |
64 |
65 | {Object.values(allInterests).map(interest => (
66 |
67 | )
68 | )}
69 |
70 |
71 | )}
72 |
73 |
74 |
75 | )
76 | }
77 |
78 | export default Interests
79 |
--------------------------------------------------------------------------------
/user-auth/migrations/versions/20210712_174530_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 6735414ea296
4 | Revises:
5 | Create Date: 2021-07-12 17:45:30.959512
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '6735414ea296'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('homeData',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('itemURL', sa.String(), nullable=False),
24 | sa.Column('imageURL', sa.String(), nullable=True),
25 | sa.Column('title', sa.String(), nullable=False),
26 | sa.Column('description', sa.String(), nullable=True),
27 | sa.PrimaryKeyConstraint('id'),
28 | sa.UniqueConstraint('imageURL'),
29 | sa.UniqueConstraint('itemURL')
30 | )
31 | op.create_table('tags',
32 | sa.Column('id', sa.Integer(), nullable=False),
33 | sa.Column('title', sa.String(), nullable=False),
34 | sa.PrimaryKeyConstraint('id'),
35 | sa.UniqueConstraint('title')
36 | )
37 | op.create_table('users',
38 | sa.Column('id', sa.Integer(), nullable=False),
39 | sa.Column('username', sa.String(length=40), nullable=False),
40 | sa.Column('email', sa.String(length=255), nullable=False),
41 | sa.Column('hashed_password', sa.String(length=255), nullable=False),
42 | sa.PrimaryKeyConstraint('id'),
43 | sa.UniqueConstraint('email'),
44 | sa.UniqueConstraint('username')
45 | )
46 | op.create_table('interests',
47 | sa.Column('id', sa.Integer(), nullable=False),
48 | sa.Column('title', sa.String(), nullable=False),
49 | sa.Column('userId', sa.Integer(), nullable=False),
50 | sa.ForeignKeyConstraint(['userId'], ['users.id'], ),
51 | sa.PrimaryKeyConstraint('id'),
52 | sa.UniqueConstraint('title')
53 | )
54 | op.create_table('saved',
55 | sa.Column('id', sa.Integer(), nullable=False),
56 | sa.Column('userId', sa.Integer(), nullable=False),
57 | sa.Column('itemURL', sa.String(), nullable=False),
58 | sa.Column('imageURL', sa.String(), nullable=False),
59 | sa.Column('title', sa.String(), nullable=False),
60 | sa.Column('description', sa.String(), nullable=True),
61 | sa.ForeignKeyConstraint(['userId'], ['users.id'], ),
62 | sa.PrimaryKeyConstraint('id'),
63 | sa.UniqueConstraint('itemURL')
64 | )
65 | op.create_table('userTags',
66 | sa.Column('id', sa.Integer(), nullable=False),
67 | sa.Column('userId', sa.Integer(), nullable=False),
68 | sa.Column('tagId', sa.Integer(), nullable=False),
69 | sa.ForeignKeyConstraint(['tagId'], ['tags.id'], ),
70 | sa.ForeignKeyConstraint(['userId'], ['users.id'], ),
71 | sa.PrimaryKeyConstraint('id')
72 | )
73 | # ### end Alembic commands ###
74 |
75 |
76 | def downgrade():
77 | # ### commands auto generated by Alembic - please adjust! ###
78 | op.drop_table('userTags')
79 | op.drop_table('saved')
80 | op.drop_table('interests')
81 | op.drop_table('users')
82 | op.drop_table('tags')
83 | op.drop_table('homeData')
84 | # ### end Alembic commands ###
85 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/store/saved.js:
--------------------------------------------------------------------------------
1 | // constants
2 | const READ_SAVED_ARTICLES = 'articles/READ_SAVED_ARTICLES'
3 | const POST_SAVED_ARTICLE = 'articles/POST_SAVED_ARTICLE'
4 | const DELETE_SAVED_ARTICLE = 'articles/DELETE_SAVED_ARTICLE'
5 |
6 |
7 | // actions
8 | const postSavedArticle = (article) => ({
9 | type: POST_SAVED_ARTICLE,
10 | article
11 | })
12 |
13 | const readSavedArticles = (articles) => ({
14 | type: READ_SAVED_ARTICLES,
15 | articles
16 | })
17 |
18 | const deleteSavedArticle = (article) => ({
19 | type: DELETE_SAVED_ARTICLE,
20 | article
21 | })
22 |
23 | // thunks
24 | export const addToSaved = (userId, itemURL, imageURL, title, description, contentSource, publishedAt) => async (dispatch) => {
25 | let publishedTime
26 | publishedAt ? publishedTime = publishedAt.split('T')[0] : publishedTime = null
27 | const res = await fetch('/api/saved/', {
28 | method: "POST",
29 | headers: {
30 | "Content-Type": "application/json"
31 | },
32 | body: JSON.stringify({
33 | userId,
34 | itemURL,
35 | imageURL,
36 | title,
37 | description,
38 | contentSource,
39 | publishedTime
40 | })
41 | })
42 |
43 | if (res.ok) {
44 | const data = await res.json()
45 | dispatch(postSavedArticle(data))
46 | }
47 | }
48 |
49 | export const loadSavedArticles = () => async (dispatch) => {
50 | const res = await fetch('/api/saved/')
51 |
52 | if (res.ok) {
53 | const data = await res.json()
54 | dispatch(readSavedArticles(data))
55 | return data
56 | }
57 | }
58 |
59 | export const removeFromSaved = (userId, itemURL, imageURL, title, description, publishedAt) => async (dispatch) => {
60 |
61 | let publishedTime
62 | publishedAt ? publishedTime = publishedAt.split('T')[0] : publishedTime = null
63 | const res = await fetch('/api/saved/', {
64 | method: "DELETE",
65 | headers: {
66 | "Content-Type": "application/json"
67 | },
68 | body: JSON.stringify({
69 | userId,
70 | itemURL,
71 | imageURL,
72 | title,
73 | description,
74 | publishedTime
75 | })
76 | })
77 |
78 | if (res.ok) {
79 | const data = await res.json()
80 | dispatch(deleteSavedArticle(data))
81 | }
82 | }
83 |
84 | // reducer
85 | export default function SavedReducer(state = {}, action) {
86 | let newState = { ...state }
87 |
88 | switch (action.type) {
89 | case READ_SAVED_ARTICLES:
90 | const articles = action.articles.saved
91 | articles.forEach(article => {
92 | newState[article.id] = article
93 | })
94 | return newState
95 | case POST_SAVED_ARTICLE:
96 | newState[action.article.id] = action.article
97 | return newState
98 | case DELETE_SAVED_ARTICLE:
99 | delete newState[action.article.id]
100 | return newState
101 | default:
102 | return state
103 | }
104 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/interests/UpdateInterests.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useDispatch } from "react-redux";
3 | import { editInterestTitle } from "../../store/interests";
4 | import DeleteInterest from "./DeleteInterest";
5 | import './interests.css'
6 |
7 | const UpdateInterests = ({ userId, interest }) => {
8 | const dispatch = useDispatch()
9 | const [showTitleEditor, setShowTitleEditor] = useState(false)
10 | const [selectedInterestTitle, setSelectedInterestTitle] = useState('')
11 | const [editButtonText, setEditButtonText] = useState('Edit')
12 |
13 | useEffect(() => {
14 | if (selectedInterestTitle === '') {
15 | setSelectedInterestTitle(interest.title)
16 | } else {
17 | interest.title = selectedInterestTitle
18 | }
19 | }, [interest, selectedInterestTitle])
20 |
21 | const handleEditConfirm = (e) => {
22 | e.preventDefault()
23 | setShowTitleEditor(false)
24 | dispatch(editInterestTitle(userId, interest.id, selectedInterestTitle))
25 | interest.title = selectedInterestTitle
26 | editButtonText === 'Cancel' ? setEditButtonText('Edit') : setEditButtonText('Cancel')
27 | window.location.reload()
28 | }
29 |
30 | if (!selectedInterestTitle) {
31 | return null
32 | }
33 |
34 | return (
35 |
36 | < form onSubmit={handleEditConfirm}>
37 |
38 |
39 | {showTitleEditor === false && {selectedInterestTitle}
}
40 | {showTitleEditor && (
41 | <>
42 | setSelectedInterestTitle(e.target.value)}
46 | />
47 | {interest.title !== selectedInterestTitle &&
48 |
55 | }
56 | >
57 | )}
58 |
69 |
70 |
71 |
72 |
73 | )
74 | }
75 |
76 | export default UpdateInterests
77 |
--------------------------------------------------------------------------------
/user-auth/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 |
3 | import logging
4 | from logging.config import fileConfig
5 |
6 | from sqlalchemy import engine_from_config
7 | from sqlalchemy import pool
8 |
9 | from alembic import context
10 |
11 | # this is the Alembic Config object, which provides
12 | # access to the values within the .ini file in use.
13 | config = context.config
14 |
15 | # Interpret the config file for Python logging.
16 | # This line sets up loggers basically.
17 | fileConfig(config.config_file_name)
18 | logger = logging.getLogger('alembic.env')
19 |
20 | # add your model's MetaData object here
21 | # for 'autogenerate' support
22 | # from myapp import mymodel
23 | # target_metadata = mymodel.Base.metadata
24 | from flask import current_app
25 | config.set_main_option(
26 | 'sqlalchemy.url',
27 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
28 | target_metadata = current_app.extensions['migrate'].db.metadata
29 |
30 | # other values from the config, defined by the needs of env.py,
31 | # can be acquired:
32 | # my_important_option = config.get_main_option("my_important_option")
33 | # ... etc.
34 |
35 |
36 | def run_migrations_offline():
37 | """Run migrations in 'offline' mode.
38 |
39 | This configures the context with just a URL
40 | and not an Engine, though an Engine is acceptable
41 | here as well. By skipping the Engine creation
42 | we don't even need a DBAPI to be available.
43 |
44 | Calls to context.execute() here emit the given string to the
45 | script output.
46 |
47 | """
48 | url = config.get_main_option("sqlalchemy.url")
49 | context.configure(
50 | url=url, target_metadata=target_metadata, literal_binds=True
51 | )
52 |
53 | with context.begin_transaction():
54 | context.run_migrations()
55 |
56 |
57 | def run_migrations_online():
58 | """Run migrations in 'online' mode.
59 |
60 | In this scenario we need to create an Engine
61 | and associate a connection with the context.
62 |
63 | """
64 |
65 | # this callback is used to prevent an auto-migration from being generated
66 | # when there are no changes to the schema
67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
68 | def process_revision_directives(context, revision, directives):
69 | if getattr(config.cmd_opts, 'autogenerate', False):
70 | script = directives[0]
71 | if script.upgrade_ops.is_empty():
72 | directives[:] = []
73 | logger.info('No changes in schema detected.')
74 |
75 | connectable = engine_from_config(
76 | config.get_section(config.config_ini_section),
77 | prefix='sqlalchemy.',
78 | poolclass=pool.NullPool,
79 | )
80 |
81 | with connectable.connect() as connection:
82 | context.configure(
83 | connection=connection,
84 | target_metadata=target_metadata,
85 | process_revision_directives=process_revision_directives,
86 | **current_app.extensions['migrate'].configure_args
87 | )
88 |
89 | with context.begin_transaction():
90 | context.run_migrations()
91 |
92 |
93 | if context.is_offline_mode():
94 | run_migrations_offline()
95 | else:
96 | run_migrations_online()
97 |
--------------------------------------------------------------------------------
/user-auth/app/seeds/default.py:
--------------------------------------------------------------------------------
1 | from app.models import db, HomeData
2 |
3 | # These are the default items that are to be displayed
4 | # on the homepage if not logged in
5 | def seed_default():
6 | item1 = HomeData(
7 | itemURL='https://giphy.com/gifs/css-13FrpeVH09Zrb2',
8 | imageURL='https://media0.giphy.com/media/13FrpeVH09Zrb2/giphy.gif?cid=0a74af3bzgqxwfvmdp7kzqt9f8eyzsidwyawtkmesuf76iz9&rid=giphy.gif&ct=g',
9 | title='css GIF',
10 | )
11 |
12 | item2 = HomeData(
13 | itemURL='https://www.nytimes.com/2021/06/14/podcasts/the-daily/apple-china-privacy.html',
14 | imageURL='https://static01.nyt.com/images/2021/06/14/podcasts/14DAILY-apple-china/14DAILY-apple-china-facebookJumbo.jpg',
15 | title='Apple\'s Bet on China',
16 | description="When the technology giant first started doing business in China, it thought it would change the country. Decades later, the reverse is true."
17 | )
18 |
19 | item3 = HomeData(
20 | itemURL='https://www.wired.com/story/mystery-malware-stole-26-million-passwords-from-windows-computers/',
21 | imageURL='https://media.wired.com/photos/60c2b2a4301dd5bd8638684b/191:100/w_1280,c_limit/Security-PC-Malware-Passwords-1184216905.jpg',
22 | title="A Mystery Malware Stole 26 Million Passwords From Windows PCs",
23 | description="The credentials were part of a trove containing 1.2 terabytes of sensitive data extracted between 2018 and 2020."
24 | )
25 |
26 | item4 = HomeData(
27 | itemURL='https://giphy.com/gifs/windows-vaporwave-error-mq5y2jHRCAqMo',
28 | imageURL='https://media3.giphy.com/media/mq5y2jHRCAqMo/giphy.gif?cid=0a74af3bae6yike8gzniaugm2rlmtus45bq5xg499vn3hodb&rid=giphy.gif&ct=g',
29 | title='Windows Error GIF',
30 | )
31 |
32 | item5 = HomeData(
33 | itemURL='https://gizmodo.com/coffee-is-good-for-you-coffee-is-bad-for-you-1847123270',
34 | imageURL='https://i.kinja-img.com/gawker-media/image/upload/c_fill,f_auto,fl_progressive,g_center,h_675,pg_1,q_80,w_1200/a442813a35185f585a719d4a8e9ba376.jpg',
35 | title='Coffee Is Good for You, Coffee Is Bad for You',
36 | description="If you’re the sort of person who regularly scans the latest science-related headlines, you’ve probably come across the coffee shuffle at some point: Articles reporting on the latest study to show coffee may do something good for you, followed by articles repo…"
37 | )
38 |
39 | item6 = HomeData(
40 | itemURL='https://www.reuters.com/world/us/brutal-heatwave-descend-us-west-prompting-fire-warnings-2021-06-14/',
41 | imageURL='https://www.reuters.com/resizer/fs8_6Y3NE5uwH8s8Qr8nlgTX2tM=/800x419/smart/filters:quality(80)/cloudfront-us-east-2.images.arcpublishing.com/reuters/DMR3HXQ46VK5FF2ISM2734AYMQ.jpg',
42 | title='Brutal heatwave to descend on U.S. West, prompting fire warnings - Reuters',
43 | description="A heatwave already punishing parts of the U.S. Southwest on Monday was expected to move into California this week, prompting the forecasters to warn of health and fire dangers."
44 | )
45 |
46 | item7 = HomeData(
47 | itemURL='https://giphy.com/gifs/vaporwave-aesthetic-aesthetics-ADSFDg7OI0TDhd9ucJ',
48 | imageURL='https://media2.giphy.com/media/UT4Qm6EXiCwpGJPKLZ/giphy.gif?cid=0a74af3bkhv1tj59h7ffcsalhleeg9rxvllwn3ll4ts4fxfa&rid=giphy.gif&ct=g',
49 | title='GIF by Mr. Cody England',
50 | )
51 |
52 | db.session.add(item1)
53 | db.session.add(item2)
54 | db.session.add(item3)
55 | db.session.add(item4)
56 | db.session.add(item5)
57 | db.session.add(item6)
58 | db.session.add(item7)
59 | db.session.commit()
60 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { NavLink } from "react-router-dom";
4 | import LogoutButton from "./auth/LogoutButton";
5 | import { login } from "../store/session";
6 | import { Event } from "./Google Analytics/GoogleAnalytics";
7 | import "./navbar.css";
8 | import "./content.css";
9 |
10 | const NavBar = () => {
11 | const user = useSelector((state) => state.session.user);
12 | const dispatch = useDispatch();
13 |
14 | const demoUser = async (e) => {
15 | e.preventDefault();
16 | const email = "demo@aa.io";
17 | const password = "password";
18 | await dispatch(login(email, password));
19 | window.location.reload();
20 | };
21 |
22 | // remove navbar once user logs in
23 | // logout button will be rendered with title
24 | // if (user) return (null)
25 |
26 | return (
27 |
97 | );
98 | };
99 |
100 | export default NavBar;
101 |
--------------------------------------------------------------------------------
/user-auth/react-app/src/store/interests.js:
--------------------------------------------------------------------------------
1 | // constants
2 | const CREATE_INTEREST = 'interests/CREATE_INTEREST'
3 | const READ_INTERESTS = 'interests/READ_INTERESTS'
4 | const READ_ONE_INTEREST = 'interests/READ_ONE_INTEREST' // REMOVE THIS?
5 | const UPDATE_INTEREST = 'interests/UPDATE_INTEREST'
6 | const DELETE_INTEREST = 'interests/DELETE_INTEREST'
7 |
8 | // actions
9 | const createNewInterest = (interest) => ({
10 | type: CREATE_INTEREST,
11 | interest
12 | })
13 |
14 | const readAllInterests = (interests) => ({
15 | type: READ_INTERESTS,
16 | interests
17 | })
18 |
19 | const readOneInterest = (interest) => ({
20 | type: READ_ONE_INTEREST,
21 | interest
22 | })
23 |
24 | const updateInterest = (interest) => ({
25 | type: UPDATE_INTEREST,
26 | interest
27 | })
28 |
29 | const deleteInterest = (interest) => ({
30 | type: DELETE_INTEREST,
31 | interest
32 | })
33 |
34 | // thunks
35 | export const addInterest = (userId, title) => async (dispatch) => {
36 | const res = await fetch('/api/interests/', {
37 | method: "POST",
38 | headers: {
39 | "Content-Type": "application/json"
40 | },
41 | body: JSON.stringify({
42 | userId,
43 | title
44 | })
45 | })
46 |
47 | if (res.ok) {
48 | const data = await res.json()
49 | dispatch(createNewInterest(data))
50 | }
51 | }
52 |
53 | export const getAllInterests = () => async (dispatch) => {
54 | const res = await fetch('/api/interests/')
55 |
56 | if (res.ok) {
57 | const data = await res.json()
58 | dispatch(readAllInterests(data))
59 | }
60 | }
61 |
62 | export const getOneInterest = (interestId) => async (dispatch) => {
63 | const res = await fetch(`/api/interest/${interestId}/`)
64 |
65 | if (res.ok) {
66 | const data = await res.json()
67 | dispatch(readAllInterests(data))
68 | }
69 |
70 | if (res.ok) {
71 | const data = await res.json()
72 | dispatch(readOneInterest(data))
73 | }
74 | }
75 |
76 | export const editInterestTitle = (userId, interestId, title) => async (dispatch) => {
77 | const res = await fetch(`/api/interests/`, {
78 | method: "PUT",
79 | headers: {
80 | "Content-Type": "application/json"
81 | },
82 | body: JSON.stringify({
83 | userId,
84 | interestId,
85 | title
86 | })
87 | })
88 | if (res.ok) {
89 | const data = await res.json()
90 | const dataToDispatch = {
91 | userId,
92 | interestId,
93 | "title": data.title
94 | }
95 | dispatch(updateInterest(dataToDispatch))
96 | return(dataToDispatch)
97 | }
98 | }
99 |
100 | export const removeInterest = (userId, interestId, title) => async (dispatch) => {
101 | const res = await fetch(`/api/interests/`, {
102 | method: "DELETE",
103 | headers: {
104 | "Content-Type": "application/json"
105 | },
106 | body: JSON.stringify({
107 | userId,
108 | interestId,
109 | title
110 | })
111 | })
112 | if (res.ok) {
113 | const data = await res.json()
114 | const dataToDispatch = {
115 | userId,
116 | interestId,
117 | "title": data.title
118 | }
119 | dispatch(deleteInterest(dataToDispatch))
120 | }
121 | }
122 |
123 | // reducer
124 | export default function InterestReducer(state={}, action) {
125 | let newState={...state}
126 | switch(action.type) {
127 | case CREATE_INTEREST:
128 | newState[action.interest.id] = action.interest
129 | return newState
130 | case READ_INTERESTS:
131 | action.interests.interests.forEach(interest => {
132 | newState[interest.id] = interest
133 | })
134 | return newState
135 | case READ_ONE_INTEREST:
136 | newState[action.interest.interests.id] = action.interest.interests
137 | return newState
138 | case UPDATE_INTEREST:
139 | const interest = {
140 | "id": action.interest.interestId,
141 | "tite": action.interest.title,
142 | "userId": action.interest.userId
143 | }
144 | newState[interest.id] = interest
145 | return newState
146 | case DELETE_INTEREST:
147 | delete newState[action.interest.interestId]
148 | return newState
149 | default:
150 | return state
151 | }
152 | }
--------------------------------------------------------------------------------
/user-auth/react-app/src/components/weather/Weather.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { getWeather } from '../../store/weather'
4 | import { getDays, getAbbrDays } from './getDays'
5 | import './weather.css'
6 | import '../content.css'
7 |
8 | const Weather = () => {
9 | const dispatch = useDispatch()
10 | const user = useSelector((state) => state.session.user);
11 | const weather = useSelector(state => state.weather)
12 | const [week, setWeek] = useState(getDays())
13 | const [abbrWeek, setAbbrWeek] = useState(getAbbrDays(week))
14 |
15 | //set these vars if weather successfully fetched
16 | const [city, setCity] = useState(weather)
17 |
18 | const [currentTemp, setCurrentTemp] = useState(0)
19 | const [currentTempIcon, setCurrentTempIcon] = useState('')
20 | const [iconImage, setIconImage] = useState('')
21 | const [todayHigh, setTodayHigh] = useState(0)
22 | const [todayLow, setTodayLow] = useState(0)
23 | const [todayDescription, setTodayDescription] = useState('')
24 |
25 | const [day1High, setday1High] = useState(0)
26 | const [day1Low, setday1Low] = useState(0)
27 | const [day1Name, setday1Name] = useState('')
28 | const [day1TempIcon, setDay1TempIcon] = useState('')
29 |
30 | const [day2High, setday2High] = useState(0)
31 | const [day2Low, setday2Low] = useState(0)
32 | const [day2Name, setday2Name] = useState('')
33 | const [day2TempIcon, setDay2TempIcon] = useState('')
34 |
35 | const [day3High, setday3High] = useState(0)
36 | const [day3Low, setday3Low] = useState(0)
37 | const [day3Name, setday3Name] = useState('')
38 | const [day3TempIcon, setDay3TempIcon] = useState('')
39 |
40 | const [day4High, setday4High] = useState(0)
41 | const [day4Low, setday4Low] = useState(0)
42 | const [day4Name, setday4Name] = useState('')
43 | const [day4TempIcon, setDay4TempIcon] = useState('')
44 |
45 | const [day5High, setday5High] = useState(0)
46 | const [day5Low, setday5Low] = useState(0)
47 | const [day5Name, setday5Name] = useState('')
48 | const [day5TempIcon, setDay5TempIcon] = useState('')
49 |
50 | const [day6High, setday6High] = useState(0)
51 | const [day6Low, setday6Low] = useState(0)
52 | const [day6Name, setday6Name] = useState('')
53 | const [day6TempIcon, setDay6TempIcon] = useState('')
54 |
55 |
56 | useEffect(() => {
57 |
58 | const getIconImage = (currentTempIcon) => {
59 | const iconURL = `http://openweathermap.org/img/wn/${currentTempIcon}@2x.png`
60 |
61 | setIconImage(iconURL)
62 |
63 |
64 | }
65 |
66 | //if weather is loaded, set the state values
67 | if (Object.values(weather).length > 0 && !weather.error) {
68 |
69 | setCity(weather.location[0])
70 |
71 | setCurrentTemp(Math.floor(weather.weather.current.temp))
72 | setCurrentTempIcon(weather.weather.current.weather[0].icon)
73 |
74 | getIconImage(weather.weather.current.weather[0].icon)
75 |
76 | setTodayHigh(Math.floor(weather.weather.daily[0].temp.max))
77 | setTodayLow(Math.floor(weather.weather.daily[0].temp.min))
78 | setTodayDescription(weather.weather.current.weather[0].description)
79 |
80 | setday1High(Math.floor(weather.weather.daily[1].temp.max))
81 | setday1Low(Math.floor(weather.weather.daily[1].temp.min))
82 | setday1Name(abbrWeek[1])
83 | const day1Icon = weather.weather.daily[1].weather[0].icon
84 | setDay1TempIcon(`http://openweathermap.org/img/wn/${day1Icon}.png`)
85 |
86 | setday2High(Math.floor(weather.weather.daily[2].temp.max))
87 | setday2Low(Math.floor(weather.weather.daily[2].temp.min))
88 | setday2Name(abbrWeek[2])
89 | const day2Icon = weather.weather.daily[2].weather[0].icon
90 | setDay2TempIcon(`http://openweathermap.org/img/wn/${day2Icon}.png`)
91 |
92 | setday3High(Math.floor(weather.weather.daily[3].temp.max))
93 | setday3Low(Math.floor(weather.weather.daily[3].temp.min))
94 | setday3Name(abbrWeek[3])
95 | const day3Icon = weather.weather.daily[3].weather[0].icon
96 | setDay3TempIcon(`http://openweathermap.org/img/wn/${day3Icon}.png`)
97 |
98 | setday4High(Math.floor(weather.weather.daily[4].temp.max))
99 | setday4Low(Math.floor(weather.weather.daily[4].temp.min))
100 | setday4Name(abbrWeek[4])
101 | const day4Icon = weather.weather.daily[4].weather[0].icon
102 |
103 | setDay4TempIcon(`http://openweathermap.org/img/wn/${day4Icon}.png`)
104 |
105 | setday5High(Math.floor(weather.weather.daily[5].temp.max))
106 | setday5Low(Math.floor(weather.weather.daily[5].temp.min))
107 | setday5Name(abbrWeek[5])
108 | const day5Icon = weather.weather.daily[5].weather[0].icon
109 | setDay5TempIcon(`http://openweathermap.org/img/wn/${day5Icon}.png`)
110 |
111 | setday6High(Math.floor(weather.weather.daily[6].temp.max))
112 | setday6Low(Math.floor(weather.weather.daily[6].temp.min))
113 | setday6Name(abbrWeek[6])
114 | const day6Icon = weather.weather.daily[6].weather[0].icon
115 | setDay6TempIcon(`http://openweathermap.org/img/wn/${day6Icon}.png`)
116 | }
117 | else dispatch(getWeather())
118 |
119 | }, [dispatch, weather])
120 |
121 |
122 | if (weather.error) {
123 |
124 | return null
125 | } else if (!user) {
126 | return null
127 | } else {
128 |
129 |
130 | return (
131 |
132 | {weather.weather && (
133 |
134 |
135 |
136 |
{`Weather in ${city}`}
137 |
138 |
139 |
140 |
{todayDescription}
141 |
{currentTemp}{'\u00B0'}F
142 |
143 |

144 |
145 |
Today
146 | {currentTempIcon &&

}
147 |
148 |
{todayHigh}{'\u00B0'}F
149 |
{todayLow}{'\u00B0'}F
150 |
151 |
152 |
{day1Name}
153 |

154 |
{day1High}{'\u00B0'}F
155 |
{day1Low}{'\u00B0'}F
156 |
157 |
158 |
{day2Name}
159 |

160 |
{day2High}{'\u00B0'}F
161 |
{day2Low}{'\u00B0'}F
162 |
163 |
164 |
{day3Name}
165 |

166 |
{day3High}{'\u00B0'}F
167 |
{day3Low}{'\u00B0'}F
168 |
169 |
170 |
{day4Name}
171 |

172 |
{day4High}{'\u00B0'}F
173 |
{day4Low}{'\u00B0'}F
174 |
175 |
176 |
{day5Name}
177 |

178 |
{day5High}{'\u00B0'}F
179 |
{day5Low}{'\u00B0'}F
180 |
181 |
182 |
{day6Name}
183 |

184 |
{day6High}{'\u00B0'}F
185 |
{day6Low}{'\u00B0'}F
186 |
187 |
188 | )}
189 |
190 |
191 | )
192 | }
193 | }
194 | export default Weather
195 |
--------------------------------------------------------------------------------
/user-auth/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "228bd794ad4b112fa52e6fd2ba8c1d8d34c8e11476a526881ae94010734ea429"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.9"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "alembic": {
20 | "hashes": [
21 | "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c",
22 | "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245"
23 | ],
24 | "index": "pypi",
25 | "version": "==1.4.3"
26 | },
27 | "certifi": {
28 | "hashes": [
29 | "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
30 | "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
31 | ],
32 | "version": "==2021.5.30"
33 | },
34 | "chardet": {
35 | "hashes": [
36 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
37 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
38 | ],
39 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
40 | "version": "==4.0.0"
41 | },
42 | "click": {
43 | "hashes": [
44 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
45 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
46 | ],
47 | "index": "pypi",
48 | "version": "==7.1.2"
49 | },
50 | "dnspython": {
51 | "hashes": [
52 | "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216",
53 | "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"
54 | ],
55 | "markers": "python_version >= '3.6'",
56 | "version": "==2.1.0"
57 | },
58 | "email-validator": {
59 | "hashes": [
60 | "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b",
61 | "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"
62 | ],
63 | "index": "pypi",
64 | "version": "==1.1.3"
65 | },
66 | "flask": {
67 | "hashes": [
68 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
69 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
70 | ],
71 | "index": "pypi",
72 | "version": "==1.1.2"
73 | },
74 | "flask-cors": {
75 | "hashes": [
76 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16",
77 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"
78 | ],
79 | "index": "pypi",
80 | "version": "==3.0.8"
81 | },
82 | "flask-jwt-extended": {
83 | "hashes": [
84 | "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd"
85 | ],
86 | "index": "pypi",
87 | "version": "==3.24.1"
88 | },
89 | "flask-login": {
90 | "hashes": [
91 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b",
92 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"
93 | ],
94 | "index": "pypi",
95 | "version": "==0.5.0"
96 | },
97 | "flask-migrate": {
98 | "hashes": [
99 | "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732",
100 | "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee"
101 | ],
102 | "index": "pypi",
103 | "version": "==2.5.3"
104 | },
105 | "flask-sqlalchemy": {
106 | "hashes": [
107 | "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e",
108 | "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"
109 | ],
110 | "index": "pypi",
111 | "version": "==2.4.4"
112 | },
113 | "flask-wtf": {
114 | "hashes": [
115 | "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2",
116 | "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"
117 | ],
118 | "index": "pypi",
119 | "version": "==0.14.3"
120 | },
121 | "gunicorn": {
122 | "hashes": [
123 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
124 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
125 | ],
126 | "index": "pypi",
127 | "version": "==20.0.4"
128 | },
129 | "idna": {
130 | "hashes": [
131 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
132 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
133 | ],
134 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
135 | "version": "==2.10"
136 | },
137 | "itsdangerous": {
138 | "hashes": [
139 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
140 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
141 | ],
142 | "index": "pypi",
143 | "version": "==1.1.0"
144 | },
145 | "jinja2": {
146 | "hashes": [
147 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
148 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
149 | ],
150 | "index": "pypi",
151 | "version": "==2.11.2"
152 | },
153 | "mako": {
154 | "hashes": [
155 | "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27",
156 | "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"
157 | ],
158 | "index": "pypi",
159 | "version": "==1.1.3"
160 | },
161 | "markupsafe": {
162 | "hashes": [
163 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
164 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
165 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
166 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
167 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
168 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
169 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
170 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
171 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
172 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
173 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
174 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
175 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
176 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
177 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
178 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
179 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
180 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
181 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
182 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
183 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
184 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
185 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
186 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
187 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
188 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
189 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
190 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
191 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
192 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
193 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
194 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
195 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
196 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
197 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
198 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
199 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
200 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
201 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
202 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
203 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
204 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
205 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
206 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
207 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
208 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
209 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
210 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
211 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
212 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
213 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
214 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
215 | ],
216 | "index": "pypi",
217 | "version": "==1.1.1"
218 | },
219 | "pyjwt": {
220 | "hashes": [
221 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
222 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
223 | ],
224 | "index": "pypi",
225 | "version": "==1.7.1"
226 | },
227 | "python-dateutil": {
228 | "hashes": [
229 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
230 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
231 | ],
232 | "index": "pypi",
233 | "version": "==2.8.1"
234 | },
235 | "python-dotenv": {
236 | "hashes": [
237 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d",
238 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"
239 | ],
240 | "index": "pypi",
241 | "version": "==0.14.0"
242 | },
243 | "python-editor": {
244 | "hashes": [
245 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
246 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
247 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8",
248 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77",
249 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"
250 | ],
251 | "index": "pypi",
252 | "version": "==1.0.4"
253 | },
254 | "requests": {
255 | "hashes": [
256 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
257 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
258 | ],
259 | "index": "pypi",
260 | "version": "==2.25.1"
261 | },
262 | "six": {
263 | "hashes": [
264 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
265 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
266 | ],
267 | "index": "pypi",
268 | "version": "==1.15.0"
269 | },
270 | "sqlalchemy": {
271 | "hashes": [
272 | "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb",
273 | "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804",
274 | "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6",
275 | "sha256:26c5ca9d09f0e21b8671a32f7d83caad5be1f6ff45eef5ec2f6fd0db85fc5dc0",
276 | "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe",
277 | "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de",
278 | "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36",
279 | "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e",
280 | "sha256:465c999ef30b1c7525f81330184121521418a67189053bcf585824d833c05b66",
281 | "sha256:51064ee7938526bab92acd049d41a1dc797422256086b39c08bafeffb9d304c6",
282 | "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc",
283 | "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d",
284 | "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce",
285 | "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea",
286 | "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f",
287 | "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365",
288 | "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea",
289 | "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23",
290 | "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338",
291 | "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1",
292 | "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b",
293 | "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e",
294 | "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba",
295 | "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02",
296 | "sha256:b6ff91356354b7ff3bd208adcf875056d3d886ed7cef90c571aef2ab8a554b12",
297 | "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86",
298 | "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d",
299 | "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7",
300 | "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0",
301 | "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac",
302 | "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc",
303 | "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37"
304 | ],
305 | "index": "pypi",
306 | "version": "==1.3.19"
307 | },
308 | "urllib3": {
309 | "hashes": [
310 | "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
311 | "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
312 | ],
313 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
314 | "version": "==1.26.6"
315 | },
316 | "werkzeug": {
317 | "hashes": [
318 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
319 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
320 | ],
321 | "index": "pypi",
322 | "version": "==1.0.1"
323 | },
324 | "wtforms": {
325 | "hashes": [
326 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c",
327 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
328 | ],
329 | "index": "pypi",
330 | "version": "==2.3.3"
331 | }
332 | },
333 | "develop": {
334 | "astroid": {
335 | "hashes": [
336 | "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892",
337 | "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9"
338 | ],
339 | "markers": "python_version ~= '3.6'",
340 | "version": "==2.6.2"
341 | },
342 | "autopep8": {
343 | "hashes": [
344 | "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0",
345 | "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"
346 | ],
347 | "index": "pypi",
348 | "version": "==1.5.7"
349 | },
350 | "isort": {
351 | "hashes": [
352 | "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
353 | "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
354 | ],
355 | "markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
356 | "version": "==5.9.1"
357 | },
358 | "lazy-object-proxy": {
359 | "hashes": [
360 | "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653",
361 | "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61",
362 | "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2",
363 | "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837",
364 | "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3",
365 | "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43",
366 | "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726",
367 | "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3",
368 | "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587",
369 | "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8",
370 | "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a",
371 | "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd",
372 | "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f",
373 | "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad",
374 | "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4",
375 | "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b",
376 | "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf",
377 | "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981",
378 | "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741",
379 | "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e",
380 | "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
381 | "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
382 | ],
383 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
384 | "version": "==1.6.0"
385 | },
386 | "mccabe": {
387 | "hashes": [
388 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
389 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
390 | ],
391 | "version": "==0.6.1"
392 | },
393 | "psycopg2-binary": {
394 | "hashes": [
395 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
396 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
397 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
398 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
399 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
400 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
401 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
402 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
403 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
404 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
405 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
406 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
407 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
408 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
409 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
410 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
411 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
412 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
413 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
414 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
415 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
416 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
417 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
418 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
419 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
420 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
421 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
422 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
423 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
424 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
425 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
426 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
427 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
428 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
429 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
430 | ],
431 | "index": "pypi",
432 | "version": "==2.8.6"
433 | },
434 | "pycodestyle": {
435 | "hashes": [
436 | "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
437 | "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
438 | ],
439 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
440 | "version": "==2.7.0"
441 | },
442 | "pylint": {
443 | "hashes": [
444 | "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a",
445 | "sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc"
446 | ],
447 | "index": "pypi",
448 | "version": "==2.9.3"
449 | },
450 | "toml": {
451 | "hashes": [
452 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
453 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
454 | ],
455 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
456 | "version": "==0.10.2"
457 | },
458 | "wrapt": {
459 | "hashes": [
460 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
461 | ],
462 | "version": "==1.12.1"
463 | }
464 | }
465 | }
466 |
--------------------------------------------------------------------------------