` margin */
432 | line-height: 1.5;
433 | color: #495057;
434 | border: 1px solid transparent;
435 | border-radius: 0.25rem;
436 | -webkit-transition: all 0.1s ease-in-out;
437 | transition: all 0.1s ease-in-out;
438 | }
439 |
440 | .form-label-group input::-webkit-input-placeholder {
441 | color: transparent;
442 | }
443 |
444 | .form-label-group input:-ms-input-placeholder {
445 | color: transparent;
446 | }
447 |
448 | .form-label-group input::-ms-input-placeholder {
449 | color: transparent;
450 | }
451 |
452 | .form-label-group input::placeholder {
453 | color: transparent;
454 | }
455 |
456 | .form-label-group input:not(:placeholder-shown) {
457 | padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
458 | padding-bottom: calc(var(--input-padding-y) / 3);
459 | }
460 |
461 | .form-label-group input:not(:placeholder-shown) ~ label {
462 | padding-top: calc(var(--input-padding-y) / 3);
463 | padding-bottom: calc(var(--input-padding-y) / 3);
464 | font-size: 12px;
465 | color: #777;
466 | }
467 |
468 | footer.sticky-footer {
469 | display: -webkit-box;
470 | display: -ms-flexbox;
471 | display: flex;
472 | position: fixed;
473 | right: 0;
474 | bottom: 0;
475 | width: calc(100% - 90px);
476 | height: 40px;
477 | background-color: #e9ecef;
478 | border-top: 1px solid #dee0e2;
479 | }
480 |
481 | footer.sticky-footer .status-bar {
482 | line-height: 1;
483 | font-size: 0.8rem;
484 | }
485 |
486 | .status-bar-container{
487 | width: 100%;
488 | padding-right: 15px;
489 | padding-left: 15px;
490 | }
491 |
492 | @media (min-width: 768px) {
493 | footer.sticky-footer {
494 | width: calc(100% - 225px);
495 | }
496 | }
497 |
498 | #dashboardApp.sidebar-toggled footer.sticky-footer {
499 | width: 100%;
500 | }
501 |
502 | @media (min-width: 768px) {
503 | #dashboardApp.sidebar-toggled footer.sticky-footer {
504 | width: calc(100% - 90px);
505 | }
506 | }
507 | .table-custom {
508 | font-size: smaller;
509 | border-left: 1px solid #dee2e6;
510 | border-right: 1px solid #dee2e6;
511 | }
512 | .table-custom thead th {
513 | vertical-align: bottom;
514 | border-bottom: 1px solid #dee2e6;
515 | }
516 |
517 | .top-buffer {
518 | margin-top: 10px;
519 | }
520 | .top-buffer-20 {
521 | margin-top: 20px;
522 | }
523 | .simple-link {
524 | color: #1072d4;
525 | }
526 | .simple-link:hover {
527 | color: #e89224;;
528 | text-decoration: none;
529 | }
530 |
531 | /*** Bootstrap extension ***/
532 | .navbar-dark .navbar-nav .nav-link {
533 | color: rgba(255,255,255,.8);
534 | }
535 |
536 | .dropdown-item {
537 | font-size: 15px;
538 | }
539 |
540 | .btn {
541 | height: 35px;
542 | color: #5b5c5d;
543 | cursor: pointer;
544 | border: none;
545 | padding: 0 20px;
546 | box-shadow: 0 -1px 0 1px rgba(0,0,0,.2) inset;
547 | outline: 0;
548 | font-weight: normal;
549 | font-size: 15px;
550 | min-width: 80px;
551 | color: #efefef;
552 | }
553 | .btn-secondary {
554 | color: #ffffff;
555 | }
556 | .btn:active, .btn:hover, .btn:focus, .btn-primary:active, .btn-primary:focus {
557 | outline: none;
558 | box-shadow: none;
559 | }
560 | .btn:not(:disabled):not(.disabled):active:focus, .btn:not(:disabled):not(.disabled).active:focus,
561 | .show > .btn.dropdown-toggle:focus {
562 | box-shadow: none;
563 | }
564 | .btn-primary {
565 | color: #f6f8fa !important;
566 | background-color: #356fa9 !important;
567 | }
568 | .btn-primary:hover {
569 | background-color: #3372b5;
570 | border-color: #03458b;
571 | }
572 | .btn-pause {
573 | color: #f6f8fa !important;
574 | background-color: #786380 !important;
575 | }
576 | .btn-cancel {
577 | color: #f6f8fa !important;
578 | background-color: #b33a1e !important;
579 | }
580 | .btn-resume {
581 | color: #f6f8fa !important;
582 | background-color: #3b943a !important;
583 | }
584 |
585 | .btn-cancel:hover, .btn-pause:hover, .btn-resume {
586 | color: #ffffff;
587 | }
588 | .btn-link {
589 | border: none;
590 | cursor: pointer;
591 | outline: none;
592 | }
593 | .btn-link:focus {
594 | outline: none;
595 | }
596 |
597 | /* When several buttons in a row, we need to put some space between them */
598 | td .btn {
599 | margin-right: 20px;
600 | }
601 |
602 | .form-control {
603 | outline: 0;
604 | padding: 5px;
605 | border-radius: 2px;
606 | border: 1px solid #c5c8ca;
607 | box-shadow: 0 0 0 0.2rem rgba(167,184,202,.12);
608 | background-color: #fff;
609 | font-size: 14px;
610 | height: 35px;
611 | }
612 | .form-control:focus {
613 | box-shadow: none;
614 | border: 1px solid #c5c8ca;
615 | }
616 | .modal-dialog {
617 | max-width: 1000px !important;
618 | }
619 | .modal-dialog-small {
620 | max-width: 500px !important;
621 | }
622 | .modal-footer {
623 | display: inline;
624 | padding: 1rem;
625 | border-top: 1px solid #e9ecef;
626 | }
627 | .modal-body {
628 | max-height: 600px;
629 | overflow-y: auto;
630 | }
631 | .alert {
632 | border-radius: 2px;
633 | }
634 | .table {
635 | border-right: 1px solid #dee2e6;
636 | border-left: 1px solid #dee2e6;
637 | border-bottom: 1px solid #dee2e6;
638 | }
--------------------------------------------------------------------------------
/app/templates/auth/baseAuth.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ${company_name} - {% block title %}{% endblock %}
10 |
11 | {% include './common/csrf_token.html' %}
12 |
13 |
14 |
15 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/templates/auth/confirmed.html:
--------------------------------------------------------------------------------
1 | {% extends "auth/baseAuth.html" %}
2 | {% block title %} Email confirmed {% endblock %}
3 | {%block page_title%} Thanks for confirming your email! {%endblock%}
4 | {% block page_content %}
5 | Now you can login to the system.
6 | Please click the login link if you are not redirected automatically in several seconds.
7 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/auth/confirmemail.html:
--------------------------------------------------------------------------------
1 | {% extends "auth/baseAuth.html" %}
2 | {% block title %} Confirm your email {% endblock %}
3 | {%block page_title%} Please confirm your email {%endblock%}
4 | {% block page_content %}
5 | Thanks for registration!
6 | ${ get_flashed_messages()[0] }
7 | If for some reason you didn't get a confirmation email please resend it .
8 |
9 |
Please correct the following error(s):
10 |
13 |
14 |
15 | The confirmation email has been sent successfully! Please open your inbox and click the link inside.
16 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/auth/login.html:
--------------------------------------------------------------------------------
1 | {% extends "auth/baseAuth.html" %}
2 | {% block title %} Login {% endblock %}
3 | {%block page_title%} Please sign in {%endblock%}
4 | {% block page_content %}
5 | Email address
6 |
7 | Password
8 |
9 |
10 |
11 | Remember me
12 |
13 |
14 | Don't have an account? Register here .
15 | Sign in
16 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/auth/register.html:
--------------------------------------------------------------------------------
1 | {% extends "auth/baseAuth.html" %}
2 | {% block title %} Registration {% endblock %}
3 | {%block page_title%} Please register {%endblock%}
4 | {% block page_content %}
5 | User name (visible by others)
6 |
7 |
8 | Email address
9 |
10 |
11 | Password
12 |
13 |
14 | Confirm password
15 |
16 |
17 | Already have an account? Sign in here .
18 |
19 | Create account
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/app/templates/baseDashboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ${company_name} - {% block title %}{% endblock %}
14 |
15 |
16 |
17 | {% include 'common/csrf_token.html' %}
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ${company_name}
29 |
30 |
33 |
34 |
35 |
45 |
46 |
47 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
109 |
Please click the "Logout" button below if you want to end your current session.
110 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/templates/common/csrf_token.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/templates/dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends "baseDashboard.html" %}
2 | {% block title %} Main {% endblock %}
--------------------------------------------------------------------------------
/app/templates/leftMenu.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/templates/non_auth/baseNonAuth.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ${company_name} - {% block title %}{% endblock %}
10 |
11 |
12 |
13 |
14 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/templates/non_auth/confirmation.html:
--------------------------------------------------------------------------------
1 | Dear ${ user.username },
2 | Welcome to ${get_config_var('COMPANY_NAME')} !
3 | To confirm your account please click here .
4 | Alternatively, you can paste the following link in your browser's address bar:
5 | ${ url_for('auth.confirm', token=token, userid=user.id, _external=True) }
6 | Sincerely,
7 | ${get_config_var('COMPANY_NAME')} Team
--------------------------------------------------------------------------------
/app/templates/non_auth/confirmation.txt:
--------------------------------------------------------------------------------
1 | Dear ${ user.username },
2 | Welcome to ${get_config_var('COMPANY_NAME')}!
3 | To confirm your account please click the following link: ${ url_for('auth.confirm', token=token, userid=user.id, _external=True) }.
4 | Alternatively, you can paste the following link in your browser's address bar:
5 | ${ url_for('auth.confirm', token=token, userid=user.id, _external=True) }
6 | Sincerely,
7 | ${get_config_var('COMPANY_NAME')} Team
--------------------------------------------------------------------------------
/app/templates/non_auth/errorpage.html:
--------------------------------------------------------------------------------
1 | {% extends "non_auth/baseNonAuth.html" %}
2 | {% block title %} Error {% endblock %}
3 | {%block page_title%} ${error_title} {%endblock%}
4 | {% block page_content %}
5 | ${error_text}
6 | Please click the login link or register to authorize.
7 | {% endblock %}
--------------------------------------------------------------------------------
/app/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/app/utils/__init__.py
--------------------------------------------------------------------------------
/app/utils/custom_login_required_api_decorator.py:
--------------------------------------------------------------------------------
1 | from flask_login.config import EXEMPT_METHODS
2 | from functools import wraps
3 | from flask import request, current_app
4 | from flask_login import current_user
5 | from flask import jsonify
6 |
7 | def login_required_for_api(func):
8 | @wraps(func)
9 | def custom_decorated_view_for_api(*args, **kwargs):
10 | if request.method in EXEMPT_METHODS:
11 | return func(*args, **kwargs)
12 | elif current_app.login_manager._login_disabled:
13 | return func(*args, **kwargs)
14 | elif not current_user.is_authenticated:
15 | return jsonify({
16 | 'result': False,
17 | 'errors': ['You are not authorized. Please refresh the page and try again.']
18 | })
19 | return func(*args, **kwargs)
20 | return custom_decorated_view_for_api
--------------------------------------------------------------------------------
/app/utils/dbscaffold.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 | from app import db
4 | from app import alembic
5 |
6 | from app.DAL.models.user_module import User
7 | from app.DAL.models.role_module import Role
8 | from app.utils.guid_module import GUID
9 |
10 |
11 | # Does upgrade or create (NOT both!)
12 |
13 | def reinit_db(db_option=''):
14 | if db_option == 'update':
15 | print('updating database')
16 | alembic.revision('made changes')
17 | alembic.upgrade()
18 | elif db_option == 'create':
19 | print('dropping all')
20 | db.drop_all()
21 | print('recreating all')
22 | db.create_all()
23 |
24 | admin_role = Role(name = 'Admin')
25 | user_role = Role(name = 'User', is_default = True)
26 | db.session.add(admin_role)
27 | db.session.add(user_role)
28 | db.session.commit()
--------------------------------------------------------------------------------
/app/utils/email.py:
--------------------------------------------------------------------------------
1 | from threading import Thread
2 | from flask import current_app, render_template
3 | from flask_mail import Message
4 | from flask_mail import Mail
5 | from app import mail
6 |
7 | def send_async_email(app, msg):
8 | with app.app_context():
9 | mail.send(msg)
10 |
11 |
12 | def send_email(to, subject, template, **kwargs):
13 | app = current_app._get_current_object()
14 | msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + ': ' + subject,
15 | sender=app.config['MAIL_DEFAULT_SENDER'] , recipients=[to])
16 | msg.body = render_template(template + '.txt', **kwargs)
17 | msg.html = render_template(template + '.html', **kwargs)
18 | thr = Thread(target=send_async_email, args=[app, msg])
19 | thr.start()
20 | return thr
--------------------------------------------------------------------------------
/app/utils/error_handler.py:
--------------------------------------------------------------------------------
1 | from flask import render_template, redirect, request, url_for, flash, jsonify, current_app
2 |
3 | def app_error(error_title='ERROR', error_text='Unknown error occured...'):
4 | return render_template('non_auth/errorpage.html', error_title = error_title, error_text = error_text,
5 | company_name=current_app.config['COMPANY_NAME'])
--------------------------------------------------------------------------------
/app/utils/filters.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import current_app
3 | def init(app):
4 | app.jinja_env.filters['generate_url_from_title'] = generate_url_from_title
5 | return
6 |
7 | def generate_url_from_title(title):
8 | return title.replace(' ', '_').lower()
9 |
--------------------------------------------------------------------------------
/app/utils/global_functions.py:
--------------------------------------------------------------------------------
1 | import os, sys
2 | from flask import current_app
3 |
4 | def init(app):
5 | app.jinja_env.globals.update(get_abs_path = get_abs_path)
6 | app.jinja_env.globals.update(get_config_var = get_config_var)
7 | return
8 |
9 | def get_abs_path(relative_path):
10 | return os.path.abspath(os.path.join(current_app.root_path, relative_path))
11 |
12 |
13 | # This function should be accessible only from CONFIG
14 | # to-do: remove from direct using
15 | def get_config_var(var_name, app = current_app):
16 | if var_name in app.config:
17 | return app.config[var_name]
18 | return None
--------------------------------------------------------------------------------
/app/utils/guid_module.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.types import TypeDecorator, CHAR
2 | from sqlalchemy.dialects.postgresql import UUID
3 | import uuid
4 |
5 | class GUID(TypeDecorator):
6 | """Platform-independent GUID type.
7 |
8 | Uses Postgresql's UUID type, otherwise uses
9 | CHAR(32), storing as stringified hex values.
10 |
11 | """
12 | impl = CHAR
13 |
14 | def load_dialect_impl(self, dialect):
15 | if dialect.name == 'postgresql':
16 | return dialect.type_descriptor(UUID())
17 | else:
18 | return dialect.type_descriptor(CHAR(32))
19 |
20 | def process_bind_param(self, value, dialect):
21 | if value is None:
22 | return value
23 | elif dialect.name == 'postgresql':
24 | return str(value)
25 | else:
26 | if not isinstance(value, uuid.UUID):
27 | return "%.32x" % uuid.UUID(value).int
28 | else:
29 | # hexstring
30 | return "%.32x" % value.int
31 |
32 | def process_result_value(self, value, dialect):
33 | if value is None:
34 | return value
35 | else:
36 | return uuid.UUID(value)
--------------------------------------------------------------------------------
/application.py:
--------------------------------------------------------------------------------
1 | import click
2 | from flask import send_from_directory, current_app
3 | from flask.cli import with_appcontext
4 | from app import create_app, init_db, init_app_path
5 |
6 | application = create_app()
7 |
8 | @application.cli.command()
9 | @with_appcontext
10 | def dbupdate():
11 | print('update db')
12 | init_db('update', application)
13 |
14 | @application.cli.command()
15 | @with_appcontext
16 | def dbcreate():
17 | print('create db')
18 | init_db('create', application)
19 |
20 |
21 | @application.cli.command()
22 | @with_appcontext
23 | @click.argument('interface_element')
24 | @click.option('-u', 'option', flag_value='update',
25 | default='')
26 | def scaffold(interface_element, option):
27 | from scaffold.generators.interface import generate
28 | init_app_path(current_app.root_path)
29 | generate(interface_element, option)
30 |
31 |
32 | # to-do:
33 | # To make it work with application.py I need to change set FLASK_APP=main to set FLASK_APP=application in venv
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | basedir = os.path.abspath(os.path.dirname(__file__))
4 |
5 | class Config(object):
6 | @staticmethod
7 | def get_secure_variable(var_name = '', default_value = None):
8 | if var_name != '' and var_name in os.environ:
9 | return os.environ[var_name]
10 | if default_value is not None:
11 | return default_value
12 |
13 | #_ANS = get_secure_variable.__func__(var_name = '')
14 |
15 | ENV = ''
16 | DEBUG = False
17 | SQLALCHEMY_TRACK_MODIFICATIONS = False
18 | SQLALCHEMY_DATABASE_URI = os.environ.get('db_url') # Store it in the hosting config
19 | CSRF_KEY = ''
20 | SECRET_KEY = os.environ.get('secret_key') # Store it in the hosting config
21 | SECRET_SALT = os.environ.get('secret_salt') # Store it in the hosting config
22 | MAIL_SUBJECT_PREFIX = 'Your company email prefix'
23 | COMPANY_NAME = 'Your company name' # Change to your company name
24 | TRIAL_PERIOD_IN_DAYS = 14 # Change to your trial (in days, it's 2 weeks by default). Put 0 if no trial
25 | SAAS_API_KEY = os.environ.get('saas_api_key') # Your api key for project-member
26 | SAAS_API_EMAIL = os.environ.get('saas_api_email')
27 | # Mail sending settings (For privateemail by default)
28 | MAIL_SERVER = os.environ.get('mail_server')
29 | MAIL_PORT = os.environ.get('mail_port')
30 | MAIL_USE_SSL = True
31 | MAIL_USE_TLS = False
32 | MAIL_USERNAME = os.environ.get('mail_username')
33 | SECURITY_EMAIL_SENDER = os.environ.get('mail_username')
34 | MAIL_DEFAULT_SENDER = os.environ.get('mail_username')
35 | MAIL_PASSWORD = os.environ.get('mail_password')
36 | ADMIN_EMAIL = os.environ.get('admin_email')
37 | STRIPE_ENDPOINT_SECRET = os.environ.get('stripe_endpoint_secret')
38 |
39 |
40 | class ProductionConfig(Config):
41 | ENV = 'prod'
42 | DEBUG = False
43 | SAAS_API_URL = 'https://api.caravelkit.com'
44 | STRIPE_PUBLISHABLE_KEY = os.environ.get('STRIPE_PUBLISHABLE_KEY')
45 | STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY')
46 |
47 |
48 | # The values below MUST store in the hosting config variables
49 | #SQLALCHEMY_DATABASE_URI
50 | #SECRET_KEY
51 | #SECRET_SALT
52 |
53 | class DevelopmentConfig(Config):
54 | ENV = 'dev'
55 | DEVELOPMENT = True
56 | DEBUG = True
57 | TRIAL_PERIOD_IN_DAYS = 1 # Change it or remove it
58 | SAAS_API_URL = 'https://api.caravelkit.com'
59 | STRIPE_PUBLISHABLE_KEY = os.environ.get('TEST_STRIPE_PUBLISHABLE_KEY')
60 | STRIPE_SECRET_KEY = os.environ.get('TEST_STRIPE_SECRET_KEY')
61 | SAAS_API_KEY = os.environ.get('saas_api_key')
62 | SAAS_API_EMAIL = os.environ.get('saas_api_email')
63 |
64 | class TestingConfig(Config):
65 | ENV = 'test'
66 | TESTING = True
67 |
68 | config = {
69 | 'dev': DevelopmentConfig,
70 | 'test': TestingConfig,
71 | 'prod': ProductionConfig,
72 | 'default' : DevelopmentConfig
73 | }
74 |
75 |
76 | class ConfigHelper:
77 |
78 | # Allows setting config from argument, or from "env" environment variable (see Activate.bat)
79 |
80 | @staticmethod
81 | def __check_config_name(env_name):
82 | return env_name is not None and env_name != '' and env_name in config is not None
83 |
84 | @staticmethod
85 | def set_config(args):
86 | if (args is not None and len(args) > 1):
87 | # Check argument
88 | if ConfigHelper.__check_config_name(args[1]):
89 | return config[args[1]]
90 |
91 | # Check os env var
92 | env = os.environ.get('env')
93 | if ConfigHelper.__check_config_name(env):
94 | if config.get(env):
95 | return config.get(env)
96 |
97 | # Nothing worked, return default config
98 | return config['default']
99 |
--------------------------------------------------------------------------------
/init.bat:
--------------------------------------------------------------------------------
1 | call venv\Scripts\activate.bat
2 | call python -m pip install --upgrade pip
3 | call pip install -r requirements.txt
4 | call npm install
5 | REM use flask dbcreate for dropping existing database and rolling all new tabels
6 | REM call flask dbupdate
7 | call npm run dev
8 | call flask run
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CaravelDevelopmentStartKit",
3 | "version": "1.0.0",
4 | "description": "This kit allows creating SaaS in the minimal configuration",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1",
7 | "dev": "webpack --config webpack.dev.js",
8 | "prod": "webpack --config webpack.prod.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "Attribution-NonCommercial 3.0 ",
13 | "devDependencies": {
14 | "clean-webpack-plugin": "^0.1.19",
15 | "css-loader": "^1.0.0",
16 | "html-loader": "^0.5.5",
17 | "node-sass": "^4.9.3",
18 | "sass-loader": "^7.1.0",
19 | "style-loader": "^0.23.0",
20 | "vue-loader": "^15.4.2",
21 | "vue-template-compiler": "^2.5.17",
22 | "webpack": "^4.19.0",
23 | "webpack-cli": "^3.3.12",
24 | "webpack-merge": "^4.1.4"
25 | },
26 | "dependencies": {
27 | "@fortawesome/fontawesome-free": "^5.3.1",
28 | "axios": "^0.18.0",
29 | "bootstrap": "^4.1.3",
30 | "file-loader": "^2.0.0",
31 | "jquery": "^3.3.1",
32 | "jquery.easing": "^1.4.1",
33 | "lodash": "^4.17.11",
34 | "moment": "^2.22.2",
35 | "popper.js": "^1.14.4",
36 | "vue": "^2.5.17",
37 | "vue-router": "^3.0.1",
38 | "vuex": "^3.0.1"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alembic==1.0.3
2 | bcrypt==3.1.4
3 | blinker==1.4
4 | certifi==2018.10.15
5 | cffi==1.11.5
6 | chardet==3.0.4
7 | Click==7.0
8 | Flask==1.0.2
9 | Flask-Alembic==2.0.1
10 | Flask-Bcrypt==0.7.1
11 | Flask-Login==0.4.1
12 | Flask-Mail==0.9.1
13 | Flask-SQLAlchemy==2.3.2
14 | Flask-WTF==0.14.2
15 | idna==2.7
16 | itsdangerous==1.1.0
17 | Jinja2==2.10
18 | Mako==1.0.7
19 | MarkupSafe==1.1.0
20 | psycopg2==2.7.6.1
21 | pycparser==2.19
22 | python-dateutil==2.7.5
23 | python-editor==1.0.3
24 | PyYAML==3.13
25 | requests==2.20.1
26 | Requires==0.0.3
27 | six==1.11.0
28 | SQLAlchemy==1.2.14
29 | SQLAlchemy-Utils==0.33.8
30 | stripe==2.12.0
31 | urllib3==1.24.1
32 | Werkzeug==0.14.1
33 | WTForms==2.2.1
34 |
--------------------------------------------------------------------------------
/scaffold/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/scaffold/__init__.py
--------------------------------------------------------------------------------
/scaffold/generators/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/scaffold/generators/__init__.py
--------------------------------------------------------------------------------
/scaffold/generators/common.py:
--------------------------------------------------------------------------------
1 | # Functions used all the generators
2 |
3 | import os
4 |
5 | # Check if file and path exist, if not, create them. Then rewrite file or add content at the
6 | # beginning, commenting the existing part.
7 | def create_write_file(file_path, new_content, rewrite = False, comment_start = '',
8 | ignore_existing_files = False):
9 | file_param = 'r+'
10 | if os.path.exists(file_path):
11 | if ignore_existing_files:
12 | # Ignore existing file and return
13 | print('Ignore: ', file_path)
14 | return
15 | else:
16 | file_param = 'w+'
17 | if not os.path.exists(os.path.dirname(file_path)):
18 | try:
19 | os.makedirs(os.path.dirname(file_path))
20 | except OSError as exc:
21 | if exc.errno != errno.EEXIST:
22 | raise Exception('Path cannot be created, please try again.')
23 |
24 | with open(file_path, file_param) as output_file:
25 | if not rewrite:
26 | output_file.seek(0)
27 | content = output_file.read()
28 | content = content.replace(comment_start, '').replace(comment_end, '')
29 | content = comment_start + content
30 | content += comment_end
31 | content = new_content + content
32 | else:
33 | content = new_content
34 | output_file.seek(0)
35 | output_file.truncate()
36 | output_file.write(content)
37 | output_file.close()
38 |
--------------------------------------------------------------------------------
/scaffold/generators/interface.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 | from . import left_menu_generator
4 |
5 | def generate_left_menu(menu_file_name):
6 | left_menu_generator.generate_code(menu_file_name)
7 |
8 | commands = {
9 | 'left_menu': generate_left_menu
10 | }
11 |
12 | def generate(command_name, option='', **kwargs):
13 | if not command_name in commands:
14 | raise Exception(r'No command found: ' + command_name)
15 | commands[command_name](command_name)
16 | if option != None and option == 'update':
17 | print('Running webpack to rebuild frontend...')
18 | cmd = ['npm', 'run', 'dev']
19 | subprocess.call(cmd, shell=True)
--------------------------------------------------------------------------------
/scaffold/generators/left_menu_generator.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | from yaml import load, dump
3 | import requests
4 | import json
5 | from flask import current_app
6 |
7 | from app import get_path_to_include
8 | from . import common
9 |
10 | def generate_code(yaml_name):
11 | # 1. Read the Yaml file to verify its existence and validity
12 | print('Reading Yaml configuration...')
13 | file_yaml_name = yaml_name + '.yaml'
14 | #file_yaml_name = get_path_to_include(r'..\\scaffold\\' + file_yaml_name)
15 | file_yaml_name = os.path.abspath(r'scaffold\\' + file_yaml_name)
16 |
17 | if not os.path.exists(file_yaml_name):
18 | raise Exception(file_yaml_name + ' file not found! Please create in the /scaffold folder')
19 | stream = open(file_yaml_name, 'rt')
20 |
21 | yaml_object = load(stream) # If yaml file is not valid, exception will be raised
22 | if not yaml_object:
23 | stream.close()
24 | raise Exception('The Yaml config file is empty or non-valid, please check it.')
25 |
26 | stream.seek(0)
27 |
28 | # 2. If no exception, send to the server
29 | print('Requesting the server to generate...')
30 | api_key = current_app.config['SAAS_API_KEY']
31 | api_email = current_app.config['SAAS_API_EMAIL']
32 | api_url = '{0}/scaffold/element/{1}'.format(current_app.config['SAAS_API_URL'], yaml_name)
33 | headers = {
34 | 'Content-Type': 'application/json'
35 | }
36 | #request = requests.post(api_url, files = {'yaml_config': stream})
37 |
38 | yaml_full = {
39 | 'cred': {
40 | 'key': api_key,
41 | 'email': api_email
42 | },
43 | 'menu': yaml_object['menu'],
44 | 'meta': {
45 | 'generate_vue_components' : (yaml_object['meta']['generate_vue_components']
46 | if yaml_object['meta']['generate_vue_components'] != None
47 | else False),
48 | 'breadcrumbs': yaml_object['meta']['breadcrumbs'],
49 | 'components_folder': yaml_object['meta']['components_folder']
50 | }
51 | }
52 | response = requests.post(api_url, headers=headers, data=json.dumps(yaml_full))
53 | stream.close()
54 | result_json = json.loads(response.text)
55 | if not result_json['result']:
56 | err_text = (', '.join(result_json.get('errors'))
57 | if result_json.get('errors') is not None else '''Some error occured on the server.
58 | Please retry you request. If this message
59 | persists please ask for the assistance at support''')
60 | print('ERROR: ', err_text)
61 | return
62 |
63 | result = result_json['render']
64 | #print(result['interface_components_render']['components'][0]['content'])
65 |
66 | # 3. Update the output file - left menu itself
67 | print('Updating the output file...')
68 | output_file_path = yaml_object['meta']['file_output']
69 | output_file_name = os.path.abspath(output_file_path)
70 |
71 | rewrite = yaml_object['meta']['rewrite'] != None and yaml_object['meta']['rewrite'] == True
72 | common.create_write_file(output_file_name, result['left_menu'], rewrite)
73 |
74 |
75 | # 4. If other Vue files should be created also, routes first
76 | output_routes_path = yaml_object['meta']['routes_file']
77 | routes_file_name = os.path.abspath(output_routes_path)
78 | common.create_write_file(routes_file_name, result['interface_components_render']['routes'], rewrite,
79 | comment_start = r'/*', comment_end = r'*/')
80 |
81 |
82 | # 5. Components folders/files
83 | for component in result['interface_components_render']['components']:
84 | if component.get('content') is None:
85 | continue
86 | file_component_name = (component['name'] if component['parent'] is None else (component['parent'] +
87 | '_' + component['name'])) + '.vue'
88 | components_folder = (yaml_object['meta']['components_folder'] if ('components_folder' in yaml_object['meta']
89 | and yaml_object['meta']['components_folder'] is not None)
90 | else '')
91 | full_file_name = get_path_to_include(os.path.join(r'js\\views', components_folder,
92 | file_component_name))
93 | print('Component: ', full_file_name)
94 | common.create_write_file(full_file_name,
95 | component['content'],
96 | rewrite, ignore_existing_files = True)
97 | print('Scaffolding done.')
98 | print('Scaffolding calls left: ', result_json['api_calls_left'])
99 |
100 |
101 |
102 | if __name__ == '__main__':
103 | main()
--------------------------------------------------------------------------------
/scaffold/left_menu.yaml:
--------------------------------------------------------------------------------
1 | # Note: all pathes are relative to root path with the project
2 | meta:
3 | file_output: app\templates\leftMenu.html
4 | routes_file: app\js\userRoutes.js
5 | components_folder: # Folder located in app\js\views folder, leave empty if you want all components in views folder
6 | generate_vue_components: yes
7 | breadcrumbs: yes
8 | rewrite: no
9 | menu:
10 | - name: Sample long page
11 | href: '/sample' # or auto
12 | icon: far fa-file
13 | - name: UI Element library
14 | href: '#'
15 | icon: fas fa-football-ball
16 | submenu:
17 | - name: Simple elements
18 | href: '/library/simple'
19 | icon: far fa-star
20 | - name: Pages
21 | component: 'Pages'
22 | href: auto
23 | icon: far fa-file
--------------------------------------------------------------------------------
/static/media/cc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/static/media/cc.png
--------------------------------------------------------------------------------
/static/media/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/static/media/favicon.ico
--------------------------------------------------------------------------------
/static/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/static/media/logo.png
--------------------------------------------------------------------------------
/static/media/paypal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/static/media/paypal.png
--------------------------------------------------------------------------------
/static/media/powered_by_stripe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/static/media/powered_by_stripe.png
--------------------------------------------------------------------------------
/unittests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CaravelKit/saas-base/048bdd2dbe23bd67f9780d218e0569a4a96708c1/unittests/__init__.py
--------------------------------------------------------------------------------
/unittests/scaffold_tests.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 | import tempfile
4 |
5 | # This is non-working code, reserved for the future use
6 | from usersite import common
7 |
8 | class ScaffoldTestCase(unittest.TestCase):
9 | def setUp(self):
10 | pass
11 |
12 | def tearDown(self):
13 | pass
14 |
15 | def test_check_accountyaml(self):
16 | res = True
17 | try:
18 | common.read_account_settings()
19 | except Exception as e:
20 | print(e)
21 | res = False
22 | assert res == True
23 |
24 | if __name__ == '__main__':
25 | unittest.main()
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CleanWebpackPlugin = require('clean-webpack-plugin');
3 | const { VueLoaderPlugin } = require('vue-loader')
4 |
5 | module.exports = {
6 | entry: {
7 | auth: './app/js/appAuth.js',
8 | dashboard: './app/js/appDashboard.js',
9 | authStyles: './app/js/styles/stylesAuth.js',
10 | dashboardStyles: './app/js/styles/stylesDashboard.js'
11 | },
12 | output: {
13 | filename: '[name].bundle.js',
14 | path: path.resolve(__dirname, 'static/js')
15 | },
16 | plugins: [
17 | new CleanWebpackPlugin(['static/js']), // <== This is Dist folder by fact
18 | new VueLoaderPlugin(),
19 | //new ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en\fr\ru)$/)
20 | ],
21 | resolve: {
22 | alias: {
23 | 'vue$': 'vue/dist/vue.esm.js', // 'vue/dist/vue.common.js' for webpack 1
24 | '@app': path.resolve(__dirname, 'app'),
25 | //'@moment': path.resolve(__dirname, 'node_modules/moment/min/moment.min.js')
26 | }
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
32 | use: [{
33 | loader: 'file-loader',
34 | options: {
35 | name: '[name].[ext]',
36 | outputPath: '../fonts/', // where the fonts will go
37 | publicPath: '/static/fonts' // override the default path
38 | }
39 | }]
40 | },
41 | {
42 | test: /\.css$/,
43 | use: [
44 | 'style-loader',
45 | 'css-loader',
46 | 'sass-loader'
47 | ]
48 | },
49 | {
50 | test: /\.scss$/,
51 | use: [
52 | "style-loader", // creates style nodes from JS strings
53 | "css-loader", // translates CSS into CommonJS
54 | "sass-loader" // compiles Sass to CSS, using Node Sass by default
55 | ]
56 | },
57 | {
58 | test: /\.vue$/,
59 | use: 'vue-loader'
60 | },
61 | {
62 | test: /\.html$/,
63 | use: 'html-loader'
64 | }
65 | ]}
66 | };
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'development',
6 | devtool: 'inline-source-map',
7 | stats: {
8 | colors: true,
9 | modules: true,
10 | reasons: true,
11 | errorDetails: true
12 | }
13 | });
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'production'
6 | });
--------------------------------------------------------------------------------