├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── api.db ├── app ├── User.py ├── http │ ├── controllers │ │ ├── HomeController.py │ │ ├── LoginController.py │ │ ├── RegisterController.py │ │ └── WelcomeController.py │ └── middleware │ │ ├── AuthenticationMiddleware.py │ │ ├── CsrfMiddleware.py │ │ └── LoadUserMiddleware.py └── resources │ └── UserResource.py ├── config ├── __init__.py ├── application.py ├── auth.py ├── database.py ├── middleware.py ├── packages.py ├── providers.py ├── session.py └── storage.py ├── databases └── migrations │ ├── 2018_01_09_043202_create_users_table.py │ └── __init__.py ├── readme.md ├── requirements.txt ├── routes └── web.py ├── setup.py ├── src └── api │ ├── __init__.py │ ├── authentication │ ├── BaseAuthentication.py │ ├── JWTAuthentication.py │ ├── PermissionScopes.py │ ├── TokenAuthentication.py │ └── __init__.py │ ├── controllers │ ├── TokenController.py │ └── __init__.py │ ├── exceptions.py │ ├── filters │ ├── FilterScopes.py │ └── __init__.py │ ├── resources │ ├── Resource.py │ └── __init__.py │ ├── routes │ ├── TokenRoutes.py │ └── __init__.py │ ├── serializers │ ├── JSONSerializer.py │ ├── XMLSerializer.py │ └── __init__.py │ └── templates │ └── __init__.py ├── tests ├── models │ └── User.py ├── test_filter_scopes.py └── test_resource.py └── wsgi.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/python:3.6 6 | steps: 7 | - checkout 8 | - run: pip install https://github.com/$BUILD_REPO/archive/$BUILD_BRANCH.zip --user 9 | - run: pip install -e . --user 10 | - run: pip install -r requirements.txt --user 11 | - run: python -m pytest 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | .vscode 3 | *.egg-info 4 | .DS_STORE 5 | .env 6 | .env.* 7 | venv 8 | __pycache__ 9 | dist 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joseph Mancuso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/api/6e4ec0a991fb2df1a9e23e8676a93bebfd0b53d5/api.db -------------------------------------------------------------------------------- /app/User.py: -------------------------------------------------------------------------------- 1 | """ User Model """ 2 | 3 | from config.database import Model 4 | 5 | 6 | class User(Model): 7 | """User Model 8 | """ 9 | 10 | __fillable__ = ['name', 'email', 'password'] 11 | 12 | __auth__ = 'email' 13 | -------------------------------------------------------------------------------- /app/http/controllers/HomeController.py: -------------------------------------------------------------------------------- 1 | """ The HomeController Module """ 2 | 3 | from masonite.auth import Auth 4 | from masonite.request import Request 5 | from masonite.view import View 6 | 7 | 8 | class HomeController: 9 | """Home Dashboard Controller 10 | """ 11 | 12 | def __init__(self): 13 | pass 14 | 15 | def show(self, request: Request, view: View): 16 | if not Auth(request).user(): 17 | request.redirect('/login') 18 | return view.render('auth/home', {'app': request.app().make('Application'), 'Auth': Auth(request)}) 19 | -------------------------------------------------------------------------------- /app/http/controllers/LoginController.py: -------------------------------------------------------------------------------- 1 | """ A LoginController Module """ 2 | 3 | from masonite.auth import Auth 4 | from masonite.request import Request 5 | from masonite.view import View 6 | 7 | 8 | class LoginController: 9 | """Login Form Controller 10 | """ 11 | 12 | def __init__(self): 13 | """LoginController Constructor. 14 | """ 15 | 16 | pass 17 | 18 | def show(self, request: Request, view: View): 19 | """Show the login page. 20 | 21 | Arguments: 22 | request {masonite.request.Request} -- The Masonite request class. 23 | view {masonite.view.View} -- The Masonite view class. 24 | 25 | Returns: 26 | masonite.view.View -- Returns the Masonite view class. 27 | """ 28 | 29 | return view.render('auth/login', {'app': request.app().make('Application'), 'Auth': Auth(request)}) 30 | 31 | def store(self, request: Request): 32 | """Login the user. 33 | 34 | Arguments: 35 | request {masonite.request.Request} -- The Masonite request class. 36 | 37 | Returns: 38 | masonite.request.Request -- The Masonite request class. 39 | """ 40 | 41 | if Auth(request).login(request.input('username'), request.input('password')): 42 | return request.redirect('/home') 43 | 44 | return request.redirect('/login') 45 | 46 | def logout(self, request: Request): 47 | """Logs out the user. 48 | 49 | Arguments: 50 | request {masonite.request.Request} -- The Masonite request class. 51 | 52 | Returns: 53 | masonite.request.Request -- The Masonite request class. 54 | """ 55 | 56 | Auth(request).logout() 57 | return request.redirect('/login') 58 | -------------------------------------------------------------------------------- /app/http/controllers/RegisterController.py: -------------------------------------------------------------------------------- 1 | """ The RegisterController Module """ 2 | 3 | from config import auth 4 | from masonite.auth import Auth 5 | from masonite.helpers import password as bcrypt_password 6 | from masonite.request import Request 7 | from masonite.view import View 8 | 9 | 10 | class RegisterController: 11 | """The RegisterController class. 12 | """ 13 | 14 | def __init__(self): 15 | """The RegisterController Constructor 16 | """ 17 | 18 | pass 19 | 20 | def show(self, request: Request, view: View): 21 | """Show the registration page. 22 | 23 | Arguments: 24 | Request {masonite.request.request} -- The Masonite request class. 25 | 26 | Returns: 27 | [type] -- [description] 28 | """ 29 | 30 | return view.render('auth/register', {'app': request.app().make('Application'), 'Auth': Auth(request)}) 31 | 32 | def store(self, request: Request): 33 | """Register the user with the database. 34 | 35 | Arguments: 36 | request {masonite.request.Request} -- The Masonite request class. 37 | 38 | Returns: 39 | masonite.request.Request -- The Masonite request class. 40 | """ 41 | 42 | password = bcrypt_password(request.input('password')) 43 | 44 | auth.AUTH['model'].create( 45 | name=request.input('name'), 46 | password=password, 47 | email=request.input('email'), 48 | ) 49 | 50 | # Login the user 51 | if Auth(request).login(request.input(auth.AUTH['model'].__auth__), request.input('password')): 52 | # Redirect to the homepage 53 | return request.redirect('/home') 54 | 55 | # Login failed. Redirect to the register page. 56 | return request.redirect('/register') 57 | -------------------------------------------------------------------------------- /app/http/controllers/WelcomeController.py: -------------------------------------------------------------------------------- 1 | """ Welcome The User To Masonite """ 2 | 3 | from masonite.view import View 4 | from masonite.request import Request 5 | from app.User import User 6 | 7 | class WelcomeController: 8 | """Controller For Welcoming The User 9 | """ 10 | 11 | def show(self, view: View, request: Request): 12 | """Shows the welcome page. 13 | 14 | Arguments: 15 | view {masonite.view.View} -- The Masonite view class. 16 | Application {config.application} -- The application config module. 17 | 18 | Returns: 19 | masonite.view.View -- The Masonite view class. 20 | """ 21 | 22 | User.create({'name': 'John', 'email': 'SomeEmail@email.com', 'password': '1234'}) 23 | 24 | return view.render('welcome', {'app': request.app().make('Application')}) 25 | -------------------------------------------------------------------------------- /app/http/middleware/AuthenticationMiddleware.py: -------------------------------------------------------------------------------- 1 | """ Authentication Middleware """ 2 | 3 | from masonite.request import Request 4 | 5 | 6 | class AuthenticationMiddleware: 7 | """Middleware To Check If The User Is Logged In 8 | """ 9 | 10 | def __init__(self, request: Request): 11 | """Inject Any Dependencies From The Service Container 12 | 13 | Arguments: 14 | Request {masonite.request.Request} -- The Masonite request object 15 | """ 16 | 17 | self.request = request 18 | 19 | def before(self): 20 | """Run This Middleware Before The Route Executes 21 | """ 22 | 23 | if not self.request.user(): 24 | self.request.redirect_to('login') 25 | 26 | def after(self): 27 | """Run This Middleware After The Route Executes 28 | """ 29 | 30 | pass 31 | -------------------------------------------------------------------------------- /app/http/middleware/CsrfMiddleware.py: -------------------------------------------------------------------------------- 1 | """ CSRF Middleware """ 2 | 3 | from masonite.middleware import CsrfMiddleware as Middleware 4 | 5 | 6 | class CsrfMiddleware(Middleware): 7 | """ Verify CSRF Token Middleware """ 8 | 9 | exempt = [ 10 | '/api/user', 11 | '/token', 12 | '/jwt', 13 | '/jwt/refresh', 14 | '/authorize' 15 | ] 16 | -------------------------------------------------------------------------------- /app/http/middleware/LoadUserMiddleware.py: -------------------------------------------------------------------------------- 1 | """ Load User Middleware """ 2 | 3 | from masonite.auth import Auth 4 | from masonite.request import Request 5 | 6 | 7 | class LoadUserMiddleware: 8 | """Middleware class which loads the current user into the request 9 | """ 10 | 11 | def __init__(self, request: Request, auth: Auth): 12 | """Inject Any Dependencies From The Service Container 13 | 14 | Arguments: 15 | Request {masonite.request.Request} -- The Masonite request object. 16 | """ 17 | 18 | self.request = request 19 | self.auth = auth 20 | 21 | def before(self): 22 | """Run This Middleware Before The Route Executes 23 | """ 24 | 25 | self.load_user() 26 | return self.request 27 | 28 | def after(self): 29 | """Run This Middleware After The Route Executes 30 | """ 31 | 32 | pass 33 | 34 | def load_user(self): 35 | """Load user into the request 36 | 37 | Arguments: 38 | request {masonite.request.Request} -- The Masonite request object. 39 | """ 40 | 41 | self.request.set_user(self.auth.user()) 42 | -------------------------------------------------------------------------------- /app/resources/UserResource.py: -------------------------------------------------------------------------------- 1 | from masonite.request import Request 2 | 3 | from src.api.authentication import (JWTAuthentication, PermissionScopes, 4 | TokenAuthentication) 5 | from src.api.resources import Resource 6 | from src.api.serializers import JSONSerializer 7 | from app.User import User 8 | from src.api.filters import FilterScopes 9 | 10 | class UserResource(Resource, JSONSerializer): 11 | model = User 12 | # methods = ['create', 'index', 'show'] 13 | scopes = ['user:read'] 14 | 15 | def index(self, request: Request): 16 | return {'id': 1} -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/api/6e4ec0a991fb2df1a9e23e8676a93bebfd0b53d5/config/__init__.py -------------------------------------------------------------------------------- /config/application.py: -------------------------------------------------------------------------------- 1 | """ Application Settings """ 2 | 3 | import os 4 | 5 | """ 6 | |-------------------------------------------------------------------------- 7 | | Application Name 8 | |-------------------------------------------------------------------------- 9 | | 10 | | This value is the name of your application. This value is used when the 11 | | framework needs to place the application's name in a notification or 12 | | any other location as required by the application or its packages. 13 | | 14 | """ 15 | 16 | NAME = 'Masonite 2.1' 17 | 18 | """ 19 | |-------------------------------------------------------------------------- 20 | | Application Debug Mode 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When your application is in debug mode, detailed error messages with 24 | | stack traces will be shown on every error that occurs within your 25 | | application. If disabled, a simple generic error page is shown 26 | | 27 | """ 28 | 29 | DEBUG = os.getenv('APP_DEBUG', False) 30 | 31 | """ 32 | |-------------------------------------------------------------------------- 33 | | Secret Key 34 | |-------------------------------------------------------------------------- 35 | | 36 | | This key is used to encrypt and decrypt various values. Out of the box 37 | | Masonite uses this key to encrypt or decrypt cookies so you can use 38 | | it to encrypt and decrypt various values using the Masonite Sign 39 | | class. Read the documentation on Encryption to find out how. 40 | | 41 | """ 42 | 43 | KEY = os.getenv('KEY', 'wzWfd04IxEjyoyNHN9M-pizOO4TYOnWUoxBqjO61FCA=') 44 | 45 | """ 46 | |-------------------------------------------------------------------------- 47 | | Application URL 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Sets the root URL of the application. This is primarily used for testing 51 | | 52 | """ 53 | 54 | URL = 'http://localhost:8000' 55 | 56 | """ 57 | |-------------------------------------------------------------------------- 58 | | Base Directory 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Sets the root path of your project 62 | | 63 | """ 64 | 65 | BASE_DIRECTORY = os.getcwd() 66 | 67 | """ 68 | |-------------------------------------------------------------------------- 69 | | Static Root 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Set the static root of your application that you wil use to store assets 73 | | 74 | """ 75 | 76 | STATIC_ROOT = os.path.join(BASE_DIRECTORY, 'storage') 77 | 78 | """ 79 | |-------------------------------------------------------------------------- 80 | | Autoload Directories 81 | |-------------------------------------------------------------------------- 82 | | 83 | | List of directories that are used to find classes and autoload them into 84 | | the Service Container. This is initially used to find models and load 85 | | them in but feel free to autoload any directories 86 | | 87 | """ 88 | 89 | AUTOLOAD = [ 90 | 'app', 91 | ] 92 | -------------------------------------------------------------------------------- /config/auth.py: -------------------------------------------------------------------------------- 1 | """ Authentication Settings """ 2 | 3 | import os 4 | from app.User import User 5 | 6 | """ 7 | |-------------------------------------------------------------------------- 8 | | Authentication Model 9 | |-------------------------------------------------------------------------- 10 | | 11 | | Put the model here that will be used to authenticate users to your site. 12 | | Currently the model must contain a password field. In the model should 13 | | be an auth_column = 'column' in the Meta class. This column will be 14 | | used to verify credentials in the Auth facade or any other auth 15 | | classes. The auth_column will be used to change auth things 16 | | like 'email' to 'user' to easily switch which column will 17 | | be authenticated. 18 | | 19 | | @see masonite.auth.Auth 20 | | 21 | """ 22 | 23 | AUTH = { 24 | 'driver': os.getenv('AUTH_DRIVER', 'cookie'), 25 | 'model': User, 26 | } 27 | -------------------------------------------------------------------------------- /config/database.py: -------------------------------------------------------------------------------- 1 | """ Database Settings """ 2 | 3 | import os 4 | 5 | from masonite.environment import LoadEnvironment 6 | from orator import DatabaseManager, Model 7 | 8 | """ 9 | |-------------------------------------------------------------------------- 10 | | Load Environment Variables 11 | |-------------------------------------------------------------------------- 12 | | 13 | | Loads in the environment variables when this page is imported. 14 | | 15 | """ 16 | 17 | LoadEnvironment() 18 | 19 | """ 20 | |-------------------------------------------------------------------------- 21 | | Database Settings 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Set connection database settings here as a dictionary. Follow the 25 | | format below to create additional connection settings. 26 | | 27 | | @see Orator migrations documentation for more info 28 | | 29 | """ 30 | 31 | DATABASES = { 32 | 'default': os.environ.get('DB_DRIVER'), 33 | 'sqlite': { 34 | 'driver': 'sqlite', 35 | 'database': os.environ.get('DB_DATABASE') 36 | }, 37 | os.environ.get('DB_DRIVER'): { 38 | 'driver': os.environ.get('DB_DRIVER'), 39 | 'database': os.environ.get('DB_DATABASE'), 40 | 'prefix': '' 41 | } 42 | } 43 | 44 | DB = DatabaseManager(DATABASES) 45 | Model.set_connection_resolver(DB) 46 | -------------------------------------------------------------------------------- /config/middleware.py: -------------------------------------------------------------------------------- 1 | """ Middleware Configuration Settings """ 2 | 3 | from masonite.middleware import ResponseMiddleware 4 | 5 | from app.http.middleware.AuthenticationMiddleware import AuthenticationMiddleware 6 | from app.http.middleware.CsrfMiddleware import CsrfMiddleware 7 | from app.http.middleware.LoadUserMiddleware import LoadUserMiddleware 8 | 9 | """ 10 | |-------------------------------------------------------------------------- 11 | | HTTP Middleware 12 | |-------------------------------------------------------------------------- 13 | | 14 | | HTTP middleware is middleware that will be ran on every request. Middleware 15 | | is only ran when a HTTP call is successful (a 200 response). This list 16 | | should contain a simple aggregate of middleware classes. 17 | | 18 | """ 19 | 20 | HTTP_MIDDLEWARE = [ 21 | LoadUserMiddleware, 22 | CsrfMiddleware, 23 | ResponseMiddleware 24 | ] 25 | 26 | """ 27 | |-------------------------------------------------------------------------- 28 | | Route Middleware 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Route middleware is middleware that is registered with a name and can 32 | | be used in the routes/web.py file. This middleware should really be 33 | | used for middleware on an individual route like a dashboard route. 34 | | 35 | | The Route Middleware is a dictionary with the key being what is specified 36 | | in your route/web.py file (in the .middleware() method) and the value is 37 | | a string with the full module path of the middleware class 38 | | 39 | """ 40 | 41 | ROUTE_MIDDLEWARE = { 42 | 'auth': AuthenticationMiddleware, 43 | } 44 | -------------------------------------------------------------------------------- /config/packages.py: -------------------------------------------------------------------------------- 1 | """ Packages Configuration Settings """ 2 | 3 | """ 4 | |-------------------------------------------------------------------------- 5 | | Site Packages 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Although not used often, you may have to add several additional package 9 | | directories while building third party packages. Put the path of the 10 | | package here and Masonite will pick it up. 11 | | 12 | | ---------- 13 | | @example 14 | | SITE_PACKAGES = [ 15 | | 'venv/lib/python3.6/site-packages' 16 | | ] 17 | | ---------- 18 | | 19 | """ 20 | 21 | SITE_PACKAGES = [ 22 | # '/Users/joseph/Programming/core' 23 | ] 24 | -------------------------------------------------------------------------------- /config/providers.py: -------------------------------------------------------------------------------- 1 | """ Providers Configuration File """ 2 | 3 | from masonite.providers import ( 4 | AppProvider, 5 | SessionProvider, 6 | RouteProvider, 7 | StatusCodeProvider, 8 | SassProvider, 9 | WhitenoiseProvider, 10 | MailProvider, 11 | UploadProvider, 12 | ViewProvider, 13 | HelpersProvider, 14 | QueueProvider, 15 | BroadcastProvider, 16 | CacheProvider, 17 | CsrfProvider, 18 | ) 19 | 20 | """ 21 | |-------------------------------------------------------------------------- 22 | | Providers List 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Providers are a simple way to remove or add functionality for Masonite 26 | | The providers in this list are either ran on server start or when a 27 | | request is made depending on the provider. Take some time to learn 28 | | more about Service Providers in our documentation 29 | | 30 | """ 31 | 32 | 33 | PROVIDERS = [ 34 | # Framework Providers 35 | AppProvider, 36 | SessionProvider, 37 | RouteProvider, 38 | StatusCodeProvider, 39 | # WhitenoiseProvider, 40 | ViewProvider, 41 | 42 | # Optional Framework Providers 43 | # SassProvider, 44 | # MailProvider, 45 | # UploadProvider, 46 | # QueueProvider, 47 | # CacheProvider, 48 | # BroadcastProvider, 49 | # CacheProvider, 50 | CsrfProvider, 51 | # HelpersProvider, 52 | 53 | # Third Party Providers 54 | 55 | # Application Providers 56 | 57 | ] 58 | -------------------------------------------------------------------------------- /config/session.py: -------------------------------------------------------------------------------- 1 | """ Session Settings """ 2 | 3 | import os 4 | 5 | """ 6 | |-------------------------------------------------------------------------- 7 | | Session Driver 8 | |-------------------------------------------------------------------------- 9 | | 10 | | Sessions are able to be linked to an individual user and carry data from 11 | | request to request. The memory driver will store all the session data 12 | | inside memory which will delete when the server stops running. 13 | | 14 | | Supported: 'memory', 'cookie' 15 | | 16 | """ 17 | 18 | DRIVER = os.getenv('SESSION_DRIVER', 'cookie') 19 | -------------------------------------------------------------------------------- /config/storage.py: -------------------------------------------------------------------------------- 1 | """ Storage Settings """ 2 | 3 | import os 4 | 5 | """ 6 | |-------------------------------------------------------------------------- 7 | | Storage Driver 8 | |-------------------------------------------------------------------------- 9 | | 10 | | The default driver you will like to use for storing uploads. You may add 11 | | additional drivers as you need or pip install additional drivers. 12 | | 13 | | Supported: 'disk', 's3' 14 | | 15 | """ 16 | 17 | DRIVER = os.getenv('STORAGE_DRIVER', 'disk') 18 | 19 | """ 20 | |-------------------------------------------------------------------------- 21 | | Storage Drivers 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Different drivers you can use for storing file uploads. 25 | | 26 | """ 27 | 28 | DRIVERS = { 29 | 'disk': { 30 | 'location': 'storage/uploads' 31 | }, 32 | 's3': { 33 | 'client': os.getenv('S3_CLIENT', 'AxJz...'), 34 | 'secret': os.getenv('S3_SECRET', 'HkZj...'), 35 | 'bucket': os.getenv('S3_BUCKET', 's3bucket'), 36 | } 37 | } 38 | 39 | 40 | """ 41 | |-------------------------------------------------------------------------- 42 | | Static Files 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Put anywhere you keep your static assets in a key, value dictionary here 46 | | The key will be the folder you put your assets in relative to the root 47 | | and the value will be the alias you wish to have in your templates. 48 | | You may have multiple aliases of the same name 49 | | 50 | | Example will be the static assets folder at /storage/static 51 | | and an alias of 52 | | 53 | """ 54 | 55 | STATICFILES = { 56 | # folder # template alias 57 | 'storage/static': 'static/', 58 | 'storage/compiled': 'static/', 59 | 'storage/uploads': 'static/', 60 | } 61 | 62 | """ 63 | |-------------------------------------------------------------------------- 64 | | SASS Settings 65 | |-------------------------------------------------------------------------- 66 | | 67 | | These settings is what Masonite will use to compile SASS into CSS. 68 | | 69 | | importFrom should contain a list of all folders where your main SASS 70 | | files live. Masonite will search in this folder for any .scss files 71 | | that do not start with an underscore and compile them. 72 | | 73 | | includePaths should contain a list of directories of any .scss files 74 | | that you plan to @import. 75 | | 76 | | compileTo should contain a string with the directory you want your sass 77 | | compiled to. 78 | | 79 | """ 80 | 81 | SASSFILES = { 82 | 'importFrom': [ 83 | 'storage/static' 84 | ], 85 | 'includePaths': [ 86 | 'storage/static/sass' 87 | ], 88 | 'compileTo': 'storage/compiled' 89 | } 90 | -------------------------------------------------------------------------------- /databases/migrations/2018_01_09_043202_create_users_table.py: -------------------------------------------------------------------------------- 1 | from orator.migrations import Migration 2 | 3 | 4 | class CreateUsersTable(Migration): 5 | 6 | def up(self): 7 | """ 8 | Run the migrations. 9 | """ 10 | with self.schema.create('users') as table: 11 | table.increments('id') 12 | table.string('name') 13 | table.string('email').unique() 14 | table.string('password') 15 | table.string('remember_token').nullable() 16 | table.timestamps() 17 | 18 | def down(self): 19 | """ 20 | Revert the migrations. 21 | """ 22 | self.schema.drop('users') 23 | -------------------------------------------------------------------------------- /databases/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.append(os.getcwd()) 4 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 |

7 | 8 | 9 | Python Version License License 10 | License 11 | gitter 12 | 13 |

14 | 15 | ## About Masonite 16 | 17 | The modern and developer centric Python web framework that strives for an actual batteries included developer tool with a lot of out of the box functionality with an extremely extendable architecture. Masonite is perfect for beginner developers getting into their first web applications as well as experienced devs that need to utilize the full expotential of Masonite to get their applications done. 18 | 19 | Masonite works hard to be fast and easy from install to deployment so developers can go from concept to creation in as quick and efficiently as possible. Use it for your next SaaS! Try it once and you’ll fall in love. 20 | 21 | * Having a simple and expressive routing engine 22 | * Extremely powerful command line helpers called `craft` commands 23 | * A simple migration system, removing the "magic" and finger crossing of migrations 24 | * A great Active Record style ORM called Orator 25 | * A great filesystem architecture for navigating and expanding your project 26 | * An extremely powerful Service Container (IOC Container) 27 | * Service Providers which makes Masonite extremely extendable 28 | 29 | ## Learning Masonite 30 | 31 | Masonite strives to have extremely comprehensive documentation. All documentation can be [Found Here](https://masoniteframework.gitbooks.io/docs/content/) and would be wise to go through the tutorials there. If you find any discrepencies or anything that doesn't make sense, be sure to comment directly on the documentation to start a discussion! 32 | 33 | Also be sure to join the [Slack channel](https://masoniteframework.gitbooks.io/docs/content/)! 34 | 35 | ## Linux 36 | 37 | If you are running on a Linux flavor, you’ll need a few extra packages before you start. You can download these packages by running: 38 | 39 | ``` 40 | $ sudo apt-get install python-dev libssl-dev 41 | ``` 42 | 43 | Instead of `python-dev` you may need to specify your Python version 44 | 45 | ``` 46 | $ sudo apt-get install python3.6-dev libssl-dev 47 | ``` 48 | 49 | ## Installation: 50 | 51 | ``` 52 | $ pip3 install masonite-cli :: (may need sudo if using UNIX) :: 53 | $ craft new project 54 | $ cd project 55 | $ craft install 56 | $ craft serve 57 | ``` 58 | 59 | Go to `http://localhost:8000/` 60 | **** 61 | 62 |

63 | * * * * 64 |

65 | 66 | Not all computers are made the same so you may have some trouble installing Masonite depending on your machine. If you have any issues be sure to read the [Known Installation Issues](https://docs.masoniteproject.com/prologue/known-installation-issues) Documentation. 67 | 68 |

69 | * * * * 70 |

71 | 72 | **** 73 | 74 | ## Contributing 75 | 76 | Please read the [Contributing Documentation](https://masoniteframework.gitbook.io/docs/prologue/contributing-guide) here. Development will be on the current releasing branch of the [Core Repository](https://github.com/MasoniteFramework/core) (typically the `develop` branch) so check open issues, the current Milestone and the releases in that repository. Ask any questions you like in the issues so we can have an open discussion about the framework, design decisions and future of the project. 77 | 78 | ## Contributors 79 | 80 | Thank you for those who have contributed to Masonite! 81 | 82 | 83 | 84 | | | | | 85 | | :-------------: | :-------------: | :-------------: | 86 | | [
Martín Peveri](https://github.com/mapeveri) | [
Adorifski](https://github.com/afdolriski) | [
Abram C. Isola](https://github.com/aisola) | 87 | 88 | ## License 89 | 90 | The Masonite framework is open-sourced software licensed under the MIT license. 91 | 92 | ## Hello World 93 | 94 | Getting started is very easy. Below is how you can get a simple Hello World application up and running. 95 | 96 | ## Installation 97 | 98 | You can easily create new applications with `craft`. To create a new application run: 99 | 100 | $ craft new project_name 101 | 102 | **** 103 | 104 |

105 | * * * * 106 |

107 | 108 | NOTE: If you do not have the craft command, you can run `pip install masonite-cli` which will install `craft` command line tool. 109 | 110 |

111 | * * * * 112 |

113 | 114 | **** 115 | 116 | This command will create a new directory called `project_name` with our new Masonite project. 117 | 118 | You can now cd into this directory by doing: 119 | 120 | $ cd project_name 121 | 122 | Now we just need to add the pip dependencies. You can run `pip3 install -r "requirements.txt"` or you can run the `craft` command: 123 | 124 | $ craft install 125 | 126 | **** 127 | 128 |

129 | * * * * 130 |

131 | 132 | NOTE: Python craft commands are essentially wrappers around common mundane tasks. Read the docs about the craft command tool to learn more 133 | 134 |

135 | * * * * 136 |

137 | 138 | **** 139 | 140 | 141 | This will install all the required dependencies to run this framework. Now we can run the `craft` command: 142 | 143 | $ craft serve 144 | 145 | This will run the server at `localhost:8000`. Navigating to that URL should show the Masonite welcome message. 146 | 147 | If that port is blocked you can specify a port by running: 148 | 149 | $ craft serve --port 8080 150 | 151 | Or specify a host by running: 152 | 153 | $ craft serve --host 192.168.1.283 154 | 155 | The server can also be auto reloaded by passing in a `-r` flag (short for `--reload`) 156 | 157 | $ craft serve -r 158 | 159 | This will reload the server when Masonite detects file changes. This is very similiar to Django. 160 | 161 | ## Hello World 162 | 163 | All web routes are in `routes/web.py`. In this file is already the route to the welcome controller. To start your hello world example just add something like: 164 | 165 | ```python 166 | Get().route('/hello/world', 'HelloWorldController@show'), 167 | ``` 168 | 169 | our routes constant file should now look something like: 170 | 171 | ```python 172 | ROUTES = [ 173 | Get().route('/', 'WelcomeController@show'), 174 | Get().route('/hello/world', 'HelloWorldController@show'), 175 | ] 176 | ``` 177 | 178 | **** 179 | 180 |

181 | * * * * 182 |

183 | 184 | NOTE: Notice this new interesting string syntax in our route. This will grant our route access to a controller (which we will create below) 185 | 186 |

187 | * * * * 188 |

189 | 190 | **** 191 | 192 | Since we used a string controller we don't have to import our controller into this file. All imports are done through Masonite on the backend. 193 | 194 | You'll notice that we have a reference to the HelloWorldController class which we do not have yet. This framework uses controllers in order to separate the application logic. Controllers can be looked at as the views.py in a Django application. The architectural standard here is 1 controller per file. 195 | 196 | In order to make the `HelloWorldController` we can use a `craft` command: 197 | 198 | $ craft controller HelloWorld 199 | 200 | This will scaffold the controller for you and put it in `app/http/controllers/HelloWorldController.py` 201 | 202 | We will have a `show()` method by default which is the typical method we will use to "show" our views and content. 203 | 204 | Inside the `HelloWorldController` we can make our `show` method like this: 205 | 206 | ```python 207 | def show(self): 208 | """ Show Hello World Template """ 209 | return view('helloworld') 210 | ``` 211 | 212 | As you see above, we are returning a `helloworld` template but we do not have that yet. All templates are in `resources/templates`. We can simply make a file called `helloworld.html` or run the `craft` command: 213 | 214 | $ craft view helloworld 215 | 216 | Which will create the `resources/templates/helloworld.html` template for us. 217 | 218 | Lastly all templates run through the Jinja2 rendering engine so we can use any Jinja2 code inside our template like: 219 | 220 | inside the `resources/views/helloworld.html` 221 | 222 | ``` 223 | {{ 'Hello World' }} 224 | ``` 225 | 226 | Now just run the server: 227 | 228 | $ craft serve 229 | 230 | And navigate to `localhost:8000/hello/world` and you will see `Hello World` in your browser. 231 | 232 | Happy Crafting! 233 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | masonite>=2.2,<2.3 2 | pyjwt 3 | coveralls 4 | flake8 5 | pytest -------------------------------------------------------------------------------- /routes/web.py: -------------------------------------------------------------------------------- 1 | """ Web Routes """ 2 | from masonite.routes import Get, Post 3 | 4 | from src.api.routes import JWTRoutes, TokenRoutes 5 | from app.resources.UserResource import UserResource 6 | 7 | ROUTES = [ 8 | Get().route('/', 'WelcomeController@show').name('welcome'), 9 | UserResource('/api/user').routes(), 10 | 11 | TokenRoutes('/token'), 12 | JWTRoutes('/authorize'), 13 | ] 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='masonite-api', 5 | 6 | # Versions should comply with PEP440. For a discussion on single-sourcing 7 | # the version across setup.py and the project code, see 8 | # https://packaging.python.org/en/latest/single_source_version.html 9 | version='2.2.2', 10 | package_dir={'': 'src'}, 11 | 12 | description='Masonite API Package', 13 | long_description='Masonite API Package', 14 | 15 | # The project's main homepage. 16 | url='https://github.com/masoniteframework/api', 17 | 18 | # Author details 19 | author='The Masonite Community', 20 | author_email='joe@masoniteproject.com', 21 | 22 | # Choose your license 23 | license='MIT', 24 | 25 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 26 | classifiers=[ 27 | # How mature is this project? Common values are 28 | # 3 - Alpha 29 | # 4 - Beta 30 | # 5 - Production/Stable 31 | 'Development Status :: 3 - Alpha', 32 | 33 | # Indicate who your project is intended for 34 | 'Intended Audience :: Developers', 35 | 'Topic :: Software Development :: Build Tools', 36 | 'Environment :: Web Environment', 37 | 38 | # Pick your license as you wish (should match "license" above) 39 | 'License :: OSI Approved :: MIT License', 40 | 41 | 'Operating System :: OS Independent', 42 | 43 | # Specify the Python versions you support here. In particular, ensure 44 | # that you indicate whether you support Python 2, Python 3 or both. 45 | 'Programming Language :: Python :: 3.4', 46 | 'Programming Language :: Python :: 3.5', 47 | 'Programming Language :: Python :: 3.6', 48 | 'Programming Language :: Python :: 3.7', 49 | 50 | 'Topic :: Internet :: WWW/HTTP', 51 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 52 | 'Topic :: Internet :: WWW/HTTP :: WSGI', 53 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 54 | 'Topic :: Software Development :: Libraries :: Python Modules', 55 | ], 56 | 57 | # What does your project relate to? 58 | keywords='masonite python framework', 59 | 60 | # You can just specify the packages manually here if your project is 61 | # simple. Or you can use find_packages(). 62 | packages=find_packages(exclude=[ 63 | 'tests*', 64 | 'app', 65 | 'bootstrap', 66 | 'config', 67 | 'databases', 68 | 'resources', 69 | 'routes', 70 | 'storage' 71 | ]), 72 | 73 | # List run-time dependencies here. These will be installed by pip when 74 | # your project is installed. For an analysis of "install_requires" vs pip's 75 | # requirements files see: 76 | # https://packaging.python.org/en/latest/requirements.html 77 | install_requires=[ 78 | 'pyjwt>=1.7.1' 79 | ], 80 | 81 | # List additional groups of dependencies here (e.g. development 82 | # dependencies). You can install these using the following syntax, 83 | # for example: 84 | # $ pip install -e .[dev,test] 85 | extras_require={ 86 | 'test': ['coverage', 'pytest'], 87 | }, 88 | 89 | # If there are data files included in your packages that need to be 90 | # installed, specify them here. If using Python 2.6 or less, then these 91 | # have to be included in MANIFEST.in as well. 92 | ## package_data={ 93 | ## 'sample': [], 94 | ## }, 95 | 96 | # Although 'package_data' is the preferred approach, in some case you may 97 | # need to place data files outside of your packages. See: 98 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 99 | # In this case, 'data_file' will be installed into '/my_data' 100 | ## data_files=[('my_data', ['data/data_file.txt'])], 101 | 102 | # To provide executable scripts, use entry points in preference to the 103 | # "scripts" keyword. Entry points provide cross-platform support and allow 104 | # pip to create the appropriate form of executable for the target platform. 105 | ## entry_points={ 106 | ## 'console_scripts': [ 107 | ## 'sample=sample:main', 108 | ## ], 109 | ## }, 110 | ) 111 | -------------------------------------------------------------------------------- /src/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/api/6e4ec0a991fb2df1a9e23e8676a93bebfd0b53d5/src/api/__init__.py -------------------------------------------------------------------------------- /src/api/authentication/BaseAuthentication.py: -------------------------------------------------------------------------------- 1 | from ..exceptions import (ApiNotAuthenticated, ExpiredToken, InvalidToken, 2 | NoApiTokenFound, PermissionScopeDenied, 3 | RateLimitReached) 4 | 5 | 6 | class BaseAuthentication: 7 | 8 | def run_authentication(self): 9 | """Call the authenticate method and check for any exceptions thrown 10 | 11 | Returns: 12 | None|dict -- Should return None if a successful authentication or a dictionary with an error if not successfully authenticated 13 | """ 14 | 15 | try: 16 | return self.request.app().resolve(self.authenticate) 17 | except ApiNotAuthenticated: 18 | return {'error': 'token not authenticated'} 19 | except ExpiredToken: 20 | return {'error': 'token has expired'} 21 | except InvalidToken: 22 | return {'error': 'token is invalid'} 23 | except NoApiTokenFound: 24 | return {'error': 'no API token found'} 25 | except PermissionScopeDenied: 26 | return {'error': 'token has invalid scope permissions'} 27 | except RateLimitReached: 28 | return {'error': 'rate limit reached'} 29 | except Exception as e: 30 | raise e 31 | return {'error': str(e)} 32 | 33 | def fetch_token(self): 34 | """Gets the token from the request object 35 | 36 | Raises: 37 | NoApiTokenFound -- Raised if no API token can be located 38 | 39 | Returns: 40 | string -- Returns the token as a string 41 | """ 42 | 43 | if self.request.input('token'): 44 | token = self.request.input('token') 45 | elif self.request.header('HTTP_AUTHORIZATION'): 46 | token = self.request.header( 47 | 'HTTP_AUTHORIZATION').replace('Basic ', '') 48 | else: 49 | raise NoApiTokenFound 50 | 51 | return token 52 | -------------------------------------------------------------------------------- /src/api/authentication/JWTAuthentication.py: -------------------------------------------------------------------------------- 1 | from ..authentication import BaseAuthentication 2 | from ..exceptions import NoApiTokenFound, ExpiredToken, InvalidToken 3 | import jwt 4 | from config.application import KEY 5 | from masonite.request import Request 6 | import pendulum 7 | 8 | class JWTAuthentication(BaseAuthentication): 9 | 10 | def authenticate(self, request: Request): 11 | """Authenticate using a JWT token 12 | """ 13 | token = self.get_token() 14 | if pendulum.parse(token['expires']).is_past(): 15 | raise ExpiredToken 16 | 17 | def get_token(self): 18 | """Returns the decrypted string as a dictionary. This method needs to be overwritten on each authentication class. 19 | 20 | Returns: 21 | dict -- Should always return a dictionary 22 | """ 23 | 24 | try: 25 | return jwt.decode(self.fetch_token(), KEY, algorithms=['HS256']) 26 | except jwt.exceptions.DecodeError: 27 | raise InvalidToken 28 | -------------------------------------------------------------------------------- /src/api/authentication/PermissionScopes.py: -------------------------------------------------------------------------------- 1 | from ..exceptions import PermissionScopeDenied 2 | 3 | 4 | class PermissionScopes: 5 | 6 | scopes = ['*'] 7 | 8 | def scope(self): 9 | token = self.get_token() 10 | if self.scopes == ['*']: 11 | return 12 | 13 | # If the current required scopes are not a subset of the user supplied scopes 14 | if 'scopes' not in token or not token['scopes'] or not set(self.scopes).issubset(token['scopes'].split(',')): 15 | raise PermissionScopeDenied 16 | 17 | def run_scope(self): 18 | try: 19 | return self.scope() 20 | except PermissionScopeDenied: 21 | return {'error': 'Permission scope denied. Requires scopes: ' + ', '.join(self.scopes)} 22 | -------------------------------------------------------------------------------- /src/api/authentication/TokenAuthentication.py: -------------------------------------------------------------------------------- 1 | from ..authentication import BaseAuthentication 2 | from masonite.auth import Sign 3 | from ..exceptions import NoApiTokenFound, ApiNotAuthenticated 4 | from masonite.request import Request 5 | 6 | class TokenAuthentication(BaseAuthentication): 7 | 8 | def authenticate(self, request: Request): 9 | """Authentication using Signed tokens 10 | 11 | Returns: 12 | None -- Should return None if a successful authentication or an exception if not. 13 | """ 14 | 15 | try: 16 | self.get_token() 17 | except Exception as e: 18 | raise ApiNotAuthenticated 19 | 20 | def get_token(self): 21 | """Returns the decrypted string as a dictionary. This method needs to be overwritten on each authentication class. 22 | 23 | Returns: 24 | dict -- Should always return a dictionary 25 | """ 26 | 27 | return Sign().unsign(self.fetch_token()) 28 | -------------------------------------------------------------------------------- /src/api/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | from .BaseAuthentication import BaseAuthentication 2 | from .TokenAuthentication import TokenAuthentication 3 | from .JWTAuthentication import JWTAuthentication 4 | from .PermissionScopes import PermissionScopes -------------------------------------------------------------------------------- /src/api/controllers/TokenController.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import pendulum 3 | from masonite.auth import Auth, Sign 4 | from masonite.helpers import password as bcrypt_password 5 | from masonite.helpers.misc import random_string 6 | from masonite.request import Request 7 | 8 | from ..exceptions import NoApiTokenFound 9 | from config.application import KEY 10 | 11 | 12 | class TokenController: 13 | 14 | """Placeholder for the authentication model. This is set via the corresponding TokenRoutes function. 15 | This will default to the auth.py authentication class. 16 | """ 17 | 18 | __auth__ = None 19 | 20 | def __init__(self): 21 | if self.__auth__ == None: 22 | from config import auth 23 | self.__auth__ = auth.AUTH['model'] 24 | 25 | def token(self): 26 | return {'token': Sign().sign(random_string(10))} 27 | 28 | def jwt(self, request: Request, auth: Auth): 29 | if not request.input('username') or not request.input('password'): 30 | return {'error': 'missing username or password'} 31 | 32 | if auth.once().login( 33 | request.input('username'), 34 | request.input('password'), 35 | ): 36 | payload = { 37 | 'issued': str(pendulum.now()), 38 | 'expires': str(pendulum.now().add(minutes=1)), 39 | 'refresh': str(pendulum.now().add(days=1)), 40 | 'scopes': request.input('scopes'), 41 | } 42 | 43 | return {'token': bytes(jwt.encode(payload, KEY, algorithm='HS256')).decode('utf-8')} 44 | 45 | return {'error': 'invalid authentication credentials'} 46 | 47 | def jwt_refresh(self, request: Request): 48 | try: 49 | token = jwt.decode(self.fetch_token(request), KEY, algorithms=['HS256']) 50 | except jwt.exceptions.DecodeError: 51 | return {'error': 'invalid JWT token'} 52 | 53 | if not pendulum.parse(token['refresh']).is_past(): 54 | payload = { 55 | 'issued': str(pendulum.now()), 56 | 'expires': str(pendulum.now().add(minutes=5)), 57 | 'refresh': str(pendulum.now().add(days=1)), 58 | 'scopes': token['scopes'], 59 | } 60 | 61 | return {'token': bytes(jwt.encode(payload, KEY, algorithm='HS256')).decode('utf-8')} 62 | 63 | return {'error': 'the refresh key on the jwt token has expired'} 64 | 65 | def fetch_token(self, request): 66 | """Gets the token from the request object 67 | 68 | Raises: 69 | NoApiTokenFound -- Raised if no API token can be located 70 | 71 | Returns: 72 | string -- Returns the token as a string 73 | """ 74 | 75 | if request.input('token'): 76 | token = request.input('token') 77 | elif request.header('HTTP_AUTHORIZATION'): 78 | token = request.header( 79 | 'HTTP_AUTHORIZATION').replace('Basic ', '') 80 | else: 81 | raise NoApiTokenFound 82 | 83 | return token 84 | -------------------------------------------------------------------------------- /src/api/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | from .TokenController import TokenController -------------------------------------------------------------------------------- /src/api/exceptions.py: -------------------------------------------------------------------------------- 1 | class ApiNotAuthenticated(Exception): 2 | pass 3 | 4 | 5 | class NoApiTokenFound(Exception): 6 | pass 7 | 8 | 9 | class PermissionScopeDenied(Exception): 10 | pass 11 | 12 | 13 | class RateLimitReached(Exception): 14 | pass 15 | 16 | 17 | class InvalidToken(Exception): 18 | pass 19 | 20 | 21 | class ExpiredToken(Exception): 22 | pass 23 | -------------------------------------------------------------------------------- /src/api/filters/FilterScopes.py: -------------------------------------------------------------------------------- 1 | class FilterScopes: 2 | 3 | filter_scopes = { 4 | 'user:read': ['name', 'email'], 5 | 'manager': ['name', 'email', 'password'] 6 | } 7 | 8 | def filter(self, response): 9 | 10 | if 'error' in response: 11 | return response 12 | 13 | token = self.get_token() 14 | scopes = token['scopes'].split(',') 15 | 16 | filtered_response = {} 17 | 18 | for scope in scopes: 19 | if scope in self.filter_scopes: 20 | if isinstance(response, dict): 21 | return self.filter_dict(response, scope) 22 | elif isinstance(response, list): 23 | return self.filter_list(response, scope) 24 | 25 | return filtered_response 26 | 27 | def filter_dict(self, response, scope): 28 | filtered_response = {} 29 | for key, value in response.items(): 30 | if key in self.filter_scopes[scope]: 31 | filtered_response.update({key: value}) 32 | 33 | return filtered_response 34 | 35 | def filter_list(self, response, scope): 36 | filtered_response = {} 37 | for elem in response: 38 | for key, value in elem.items(): 39 | if key in self.filter_scopes[scope]: 40 | filtered_response.update({key: value}) 41 | 42 | return filtered_response 43 | -------------------------------------------------------------------------------- /src/api/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from .FilterScopes import FilterScopes 2 | -------------------------------------------------------------------------------- /src/api/resources/Resource.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from masonite.request import Request 4 | from masonite.routes import BaseHttpRoute 5 | 6 | from ..exceptions import (ApiNotAuthenticated, ExpiredToken, InvalidToken, 7 | NoApiTokenFound, PermissionScopeDenied, 8 | RateLimitReached) 9 | 10 | 11 | class Resource(BaseHttpRoute): 12 | """Resource class that will use a similar structure as a Route class. 13 | """ 14 | 15 | model = None 16 | methods = ['create', 'index', 'show', 'update', 'delete'] 17 | prefix = '/api' 18 | required_domain = None 19 | without = [] 20 | 21 | def __init__(self, url=None, method_type='GET'): 22 | self.list_middleware = [] 23 | self.route_url = url 24 | self.method_type = method_type 25 | self.named_route = None 26 | self.model.__hidden__ = self.without 27 | if url and method_type: 28 | self._compiled_url = self.compile_route_to_regex() 29 | 30 | def routes(self): 31 | routes = [] 32 | if 'create' in self.methods: 33 | routes.append(self.__class__(self.route_url, 'POST').middleware(*self.list_middleware)) 34 | if 'index' in self.methods: 35 | routes.append(self.__class__(self.route_url, 'GET').middleware(*self.list_middleware)) 36 | if 'show' in self.methods: 37 | routes.append(self.__class__(self.route_url + '/@id', 'GET').middleware(*self.list_middleware)) 38 | if 'update' in self.methods: 39 | routes.append(self.__class__(self.route_url + '/@id', 'PUT').middleware(*self.list_middleware)) 40 | if 'delete' in self.methods: 41 | routes.append(self.__class__(self.route_url + '/@id', 'DELETE').middleware(*self.list_middleware)) 42 | 43 | return routes 44 | 45 | def get_response(self): 46 | """Gets the response that should be returned from this resource 47 | """ 48 | 49 | response = None 50 | 51 | if hasattr(self, 'authenticate'): 52 | # Get a response from the authentication method if one exists 53 | response = self.run_authentication() 54 | 55 | if hasattr(self, 'scope'): 56 | # Get a response from the authentication method if one exists 57 | if not response: 58 | response = self.run_scope() 59 | 60 | # If the authenticate method did not return a response, continue on to one of the CRUD responses 61 | if not response: 62 | if 'POST' in self.method_type: 63 | response = self.request.app().resolve(getattr(self, 'create')) 64 | elif 'GET' in self.method_type and '@' in self.route_url: 65 | response = self.request.app().resolve(getattr(self, 'show')) 66 | elif 'GET' in self.method_type: 67 | response = self.request.app().resolve(getattr(self, 'index')) 68 | elif 'PUT' in self.method_type or 'PATCH' in self.method_type: 69 | response = self.request.app().resolve(getattr(self, 'update')) 70 | elif 'DELETE' in self.method_type: 71 | response = self.request.app().resolve(getattr(self, 'delete')) 72 | 73 | # If the resource needs it's own serializer method 74 | if hasattr(self, 'serialize'): 75 | response = self.serialize(response) 76 | # If the resource needs it's own serializer method 77 | if hasattr(self, 'filter'): 78 | response = self.filter(response) 79 | 80 | return response 81 | 82 | def load_request(self, request): 83 | self.request = request 84 | return self 85 | 86 | def create(self): 87 | """Logic to create data from a given model 88 | """ 89 | try: 90 | record = self.model.create(self.request.all()) 91 | except Exception as e: 92 | return {'error': str(e)} 93 | return record 94 | 95 | def index(self): 96 | """Logic to read data from several models 97 | """ 98 | return self.model.all() 99 | 100 | def show(self, request: Request): 101 | """Logic to read data from 1 model 102 | """ 103 | return self.model.find(request.param('id')) 104 | 105 | def update(self, request: Request): 106 | """Logic to update data from a given model 107 | """ 108 | record = self.model.find(request.param('id')) 109 | record.update(request.all()) 110 | return record 111 | 112 | def delete(self, request: Request): 113 | """Logic to delete data from a given model 114 | """ 115 | record = self.model.find(request.param('id')) 116 | if record: 117 | record.delete() 118 | return record 119 | 120 | return {'error': 'Record does not exist'} 121 | -------------------------------------------------------------------------------- /src/api/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .Resource import Resource -------------------------------------------------------------------------------- /src/api/routes/TokenRoutes.py: -------------------------------------------------------------------------------- 1 | from masonite.routes import Get, Post 2 | from ..controllers import TokenController 3 | 4 | 5 | def TokenRoutes(url='/token'): 6 | return [ 7 | Get().route(url, TokenController.token) 8 | ] 9 | 10 | def JWTRoutes(url='/jwt', auth=None): 11 | controller = TokenController 12 | controller.__auth__ = auth 13 | return [ 14 | Post().route(url, controller.jwt), 15 | Post().route(url + '/refresh', controller.jwt_refresh), 16 | ] 17 | 18 | -------------------------------------------------------------------------------- /src/api/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .TokenRoutes import TokenRoutes, JWTRoutes -------------------------------------------------------------------------------- /src/api/serializers/JSONSerializer.py: -------------------------------------------------------------------------------- 1 | import json 2 | from orator.support.collection import Collection 3 | from orator import Model 4 | 5 | 6 | class JSONSerializer: 7 | 8 | def serialize(self, response): 9 | """Serialize the model into JSON 10 | """ 11 | if isinstance(response, Collection): 12 | return response.serialize() 13 | elif isinstance(response, Model): 14 | return response.to_dict() 15 | 16 | return response 17 | -------------------------------------------------------------------------------- /src/api/serializers/XMLSerializer.py: -------------------------------------------------------------------------------- 1 | class XMLSerializer: 2 | 3 | def serialize(self): 4 | """Serialize the model into XML 5 | """ 6 | pass 7 | -------------------------------------------------------------------------------- /src/api/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from .JSONSerializer import JSONSerializer 2 | from .XMLSerializer import XMLSerializer -------------------------------------------------------------------------------- /src/api/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/api/6e4ec0a991fb2df1a9e23e8676a93bebfd0b53d5/src/api/templates/__init__.py -------------------------------------------------------------------------------- /tests/models/User.py: -------------------------------------------------------------------------------- 1 | from orator import Model 2 | class User(Model): 3 | 4 | id = 1 5 | name = 'Test User' 6 | email = 'test@email.com' 7 | 8 | @staticmethod 9 | def find(id): 10 | user = User() 11 | user.id = 1 12 | user.name = 'Test User' 13 | user.email = 'test@email.com' 14 | return user 15 | 16 | def serialize(self): 17 | return {'id': 1} 18 | -------------------------------------------------------------------------------- /tests/test_filter_scopes.py: -------------------------------------------------------------------------------- 1 | from src.api.resources import Resource 2 | from src.api.filters import FilterScopes 3 | from src.api.authentication import PermissionScopes, JWTAuthentication 4 | from src.api.serializers import JSONSerializer 5 | # from tests.models.User import User 6 | from masonite.providers import RouteProvider 7 | from masonite.testsuite import TestSuite, generate_wsgi 8 | from masonite.routes import Route, RouteGroup 9 | from masonite.request import Request 10 | from masonite.response import Response 11 | from masonite.helpers.routes import flatten_routes 12 | import pendulum 13 | import jwt 14 | from config.application import KEY 15 | from app.User import User 16 | from masonite.testing import TestCase 17 | from masonite.helpers import password 18 | 19 | class MockJWTAuthentication(JWTAuthentication): 20 | 21 | def fetch_token(self): 22 | payload = { 23 | 'issued': str(pendulum.now()), 24 | 'expires': str(pendulum.now().add(minutes=1)), 25 | 'refresh': str(pendulum.now().add(days=1)), 26 | 'scopes': 'user:read', 27 | } 28 | return jwt.encode(payload, KEY, algorithm='HS256') 29 | 30 | class UserResourceTest(Resource, JSONSerializer, MockJWTAuthentication, PermissionScopes, FilterScopes): 31 | model = User 32 | scopes = ['user:read'] 33 | filter_scopes = { 34 | 'user:read': ['name', 'email'], 35 | 'user:manager': ['id', 'name', 'email', 'active', 'password'] 36 | } 37 | 38 | def show(self): 39 | return { 40 | 'name': 'Joe', 41 | 'email': 'test@email.com', 42 | 'active': 1, 43 | 'password': '1234' 44 | } 45 | 46 | def index(self): 47 | return self.model.all() 48 | 49 | class TestFilterScopes(TestCase): 50 | 51 | transactions = True 52 | 53 | def setUp(self): 54 | super().setUp() 55 | self.routes(UserResourceTest('/api').routes()) 56 | 57 | def setUpFactories(self): 58 | User.create({ 59 | 'name': 'Joe', 60 | 'email': 'joe@email.com', 61 | 'password': password('secret') 62 | }) 63 | 64 | def test_filter_scopes_filters_dictionary(self): 65 | self.assertTrue(self.json('GET', '/api/1').contains('name')) 66 | self.assertTrue(self.json('GET', '/api/1').contains('email')) 67 | self.assertFalse(self.json('GET', '/api/1').contains('id')) 68 | -------------------------------------------------------------------------------- /tests/test_resource.py: -------------------------------------------------------------------------------- 1 | from src.api.resources import Resource 2 | from masonite.routes import RouteGroup 3 | from masonite.request import Request 4 | from masonite.testing import TestCase 5 | from app.User import User 6 | from masonite.helpers import password 7 | 8 | 9 | class ResourceTest(Resource): 10 | model = User 11 | method_type = 'GET' 12 | 13 | def show(self, request: Request): 14 | request.status(203) 15 | return 'read_single' 16 | 17 | 18 | class ResourceJsonTest(Resource): 19 | model = User 20 | method_type = 'GET' 21 | 22 | def index(self, request: Request): 23 | return {'id': 1} 24 | 25 | 26 | class TestResource(TestCase): 27 | 28 | def setUp(self): 29 | super().setUp() 30 | self.routes(ResourceTest('/api').routes()) 31 | 32 | def setUpFactories(self): 33 | User.create({ 34 | 'name': 'Joe', 35 | 'email': 'Joe@email.com', 36 | 'password': password('secret') 37 | }) 38 | 39 | def test_resource_can_return_response_acting_as_route(self): 40 | self.assertTrue(self.json('GET', '/api/1').hasJson('name', 'Joe')) 41 | 42 | def test_resource_returns_correct_routes(self): 43 | routes = ResourceTest('/api').routes() 44 | 45 | assert len(routes) == 5 46 | 47 | def test_resource_middleware_returns_json_from_dictionary(self): 48 | self.routes(ResourceJsonTest('/api/json').routes()) 49 | self.assertTrue(self.json('GET', '/api/json/1').hasJson('id', 1)) 50 | 51 | def test_route_groups_middleware(self): 52 | group = RouteGroup([ 53 | ResourceJsonTest('/api').routes() 54 | ], middleware=('auth',)) 55 | 56 | assert group[0].list_middleware == ['auth'] 57 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | """ First Entry For The WSGI Server """ 2 | class PackageContainer: 3 | 4 | def create(self): 5 | from masonite.app import App 6 | from config import providers 7 | 8 | """ 9 | |-------------------------------------------------------------------------- 10 | | Instantiate Container And Perform Important Bindings 11 | |-------------------------------------------------------------------------- 12 | | 13 | | Some Service providers need important bindings like the WSGI application 14 | | and the application configuration file before they boot. 15 | | 16 | """ 17 | 18 | container = App() 19 | 20 | # container.bind('WSGI', app) 21 | # container.bind('Application', application) 22 | container.bind('Container', container) 23 | 24 | container.bind('ProvidersConfig', providers) 25 | container.bind('Providers', []) 26 | container.bind('WSGIProviders', []) 27 | 28 | """ 29 | |-------------------------------------------------------------------------- 30 | | Bind all service providers 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Let's register everything into the Service Container. Once everything is 34 | | in the container we can run through all the boot methods. For reasons 35 | | some providers don't need to execute with every request and should 36 | | only run once when the server is started. Providers will be ran 37 | | once if the wsgi attribute on a provider is False. 38 | | 39 | """ 40 | 41 | for provider in container.make('ProvidersConfig').PROVIDERS: 42 | located_provider = provider() 43 | located_provider.load_app(container).register() 44 | if located_provider.wsgi: 45 | container.make('WSGIProviders').append(located_provider) 46 | else: 47 | container.make('Providers').append(located_provider) 48 | 49 | for provider in container.make('Providers'): 50 | container.resolve(provider.boot) 51 | 52 | """ 53 | |-------------------------------------------------------------------------- 54 | | Get the application from the container 55 | |-------------------------------------------------------------------------- 56 | | 57 | | Some providers may change the WSGI Server like wrapping the WSGI server 58 | | in a Whitenoise container for an example. Let's get a WSGI instance 59 | | from the container and pass it to the application variable. This 60 | | will allow WSGI servers to pick it up from the command line 61 | | 62 | """ 63 | 64 | return container 65 | 66 | 67 | container = PackageContainer().create() --------------------------------------------------------------------------------