Compare commits
3 Commits
master
...
f57e28f50b
Author | SHA1 | Date |
---|---|---|
![]() |
f57e28f50b | |
![]() |
7a826cc4bc | |
![]() |
6b29e2a373 |
|
@ -1,5 +1,3 @@
|
|||
__pycache__
|
||||
*.py[co]
|
||||
*.sqlite
|
||||
data/
|
||||
dist/
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
FROM tiangolo/meinheld-gunicorn-flask
|
||||
LABEL org.opencontainers.image.authors="Liberty Tech Force"
|
||||
|
||||
RUN pip install --no-cache-dir flask_admin flask_sqlalchemy flask_login psycopg2-binary pymysql 'greenlet<0.5,>=0.4.5'
|
||||
COPY ./statusforce /app/statusforce
|
||||
COPY ./docker/main.py /app/
|
|
@ -1,20 +0,0 @@
|
|||
version: '3'
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- "8000:80"
|
||||
environment:
|
||||
- SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://statusforce:statusforce@db/statusforce?sslmode=disable
|
||||
- FLASK_SECRET_KEY=my-super-secret-key
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=statusforce
|
||||
- POSTGRES_PASSWORD=statusforce
|
||||
- POSTGRES_DB=statusforce
|
||||
volumes:
|
||||
- "./data:/var/lib/postgresql/data"
|
|
@ -1,3 +0,0 @@
|
|||
from statusforce.app import get_app
|
||||
|
||||
app = get_app()
|
|
@ -1,64 +0,0 @@
|
|||
[build-system]
|
||||
requires = ["hatchling", "hatch-vcs"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "StatusForce"
|
||||
description = "Simple status page for system status"
|
||||
readme = "README.rst"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Raoul Snyman", email = "raoul@libertytechforce.com" },
|
||||
]
|
||||
keywords = [
|
||||
"status",
|
||||
"website",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
dependencies = [
|
||||
"Flask",
|
||||
"Flask-Admin",
|
||||
"Flask-Login",
|
||||
"Flask-SQLAlchemy",
|
||||
"psycopg2-binary",
|
||||
"pymysql",
|
||||
"greenlet<0.5,>=0.4.5"
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://libertytechforce.com"
|
||||
Issues = "https://git.libertytechforce.com/libertytechforce/statusforce/issues"
|
||||
Source = "https://git.libertytechforce.com/libertytechforce/statusforce"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tests = [
|
||||
"pytest",
|
||||
"pytest-faker",
|
||||
"pytest-flask"
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "vcs"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"/statusforce",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = [
|
||||
"statusforce",
|
||||
]
|
|
@ -0,0 +1,37 @@
|
|||
[metadata]
|
||||
name = StatusForce
|
||||
version = 0.0.1
|
||||
author = Raoul Snyman
|
||||
author_email = raoul@libertytechforce.com
|
||||
description = Simple status page for system status
|
||||
long_description = file:README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
url = https://libertytechforce.com
|
||||
license = MIT
|
||||
classifiers =
|
||||
Development Status :: 3 - Alpha
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Topic :: Utilities
|
||||
keywords = website, status
|
||||
|
||||
[options]
|
||||
py_modules = statusforce
|
||||
python_requires = >=3.7
|
||||
install_requires =
|
||||
Flask
|
||||
Flask-Admin
|
||||
Flask-SQLAlchemy
|
||||
Flask-Login
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
The statusforce package
|
||||
"""
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
|
@ -1,48 +1,36 @@
|
|||
import os
|
||||
from flask import Flask
|
||||
from flask import Flask, render_template
|
||||
from flask_admin import Admin
|
||||
from flask_login import LoginManager
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
|
||||
from statusforce.db import db, session
|
||||
from statusforce.models import Service, Incident, User
|
||||
from statusforce.views import AuthAdminIndexView, AuthModelView, main
|
||||
from statusforce.models import Service, Incident
|
||||
|
||||
|
||||
def get_app():
|
||||
# Set up Flask application and initial config
|
||||
app = Flask(__name__)
|
||||
app.secret_key = b'super-secret-please-replace-me'
|
||||
app.config['FLASK_ADMIN_SWATCH'] = 'cosmo'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///statusforce.sqlite'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
# Set up Flask application and initial config
|
||||
app = Flask(__name__)
|
||||
app.secret_key = b'GSADFGST#$%^$%&^2345234534576476'
|
||||
app.config['FLASK_ADMIN_SWATCH'] = 'materia'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///statusforce.sqlite'
|
||||
|
||||
# Pull configuration from environment variables
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith('FLASK_ADMIN_') or key.startswith('SQLALCHEMY_'):
|
||||
app.config[key] = value
|
||||
elif key == 'FLASK_SECRET_KEY':
|
||||
# Special case, apparently
|
||||
app.secret_key = value
|
||||
elif key.startswith('FLASK_'):
|
||||
app.config[key.replace('FLASK_', '')] = value
|
||||
|
||||
# Set up database
|
||||
db.init_app(app)
|
||||
with app.app_context():
|
||||
# Set up database
|
||||
db.init_app(app)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Set up the authentication
|
||||
login_manager = LoginManager(app)
|
||||
# Set up admin interface
|
||||
admin = Admin(app, name='StatusForce', template_mode='bootstrap4')
|
||||
admin.add_view(ModelView(Service, session))
|
||||
admin.add_view(ModelView(Incident, session))
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(user_id)
|
||||
|
||||
# Set up admin interface
|
||||
admin = Admin(app, name='StatusForce', index_view=AuthAdminIndexView(), template_mode='bootstrap4')
|
||||
admin.add_view(AuthModelView(Service, session))
|
||||
admin.add_view(AuthModelView(Incident, session))
|
||||
|
||||
# Finally, attache the blueprint
|
||||
app.register_blueprint(main)
|
||||
return app
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""
|
||||
Show the status page
|
||||
"""
|
||||
services = Service.query.all()
|
||||
incidents = Incident.query.order_by(Incident.created.desc()).limit(10).all()
|
||||
service_statuses = [service.status for service in services]
|
||||
overall_status = 'operational' if all(status == 'operational' for status in service_statuses) else \
|
||||
'offline' if all(status == 'offline' for status in service_statuses) else 'unclear'
|
||||
return render_template('index.html', services=services, incidents=incidents, overall_status=overall_status)
|
||||
|
|
|
@ -7,11 +7,9 @@ from statusforce.db import Model, Column, ForeignKey, Boolean, DateTime, Enum, I
|
|||
|
||||
|
||||
class Service(Model):
|
||||
__tablename__ = 'services'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
status = Column(Enum('operational', 'unclear', 'offline', native_enum=False), default='operational')
|
||||
status = Column(Enum('operational', 'unclear', 'offline'), default='operational')
|
||||
incidents = relationship('Incident', backref=backref('service', lazy=True))
|
||||
|
||||
def __str__(self):
|
||||
|
@ -19,10 +17,8 @@ class Service(Model):
|
|||
|
||||
|
||||
class Incident(Model):
|
||||
__tablename__ = 'incidents'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
service_id = Column(Integer, ForeignKey('services.id'))
|
||||
service_id = Column(Integer, ForeignKey('service.id'))
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
is_resolved = Column(Boolean, default=False)
|
||||
|
@ -31,27 +27,3 @@ class Incident(Model):
|
|||
|
||||
def __str__(self):
|
||||
return '{} ({})'.format(self.title, self.service.name)
|
||||
|
||||
|
||||
class User(Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
full_name = Column(String)
|
||||
email = Column(String, nullable=False)
|
||||
password = Column(String, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
return self.id
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name or self.email
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 65 KiB |
|
@ -1,22 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
{{ lib.form_css() }}
|
||||
{% endblock head %}
|
||||
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
{% if current_user.is_authenticated %}
|
||||
<p class="lead">Use the menu items above to add services and incidents.</p>
|
||||
{% else %}
|
||||
{{lib.render_form(form)}}
|
||||
{% endif %}
|
||||
{% endblock body %}
|
||||
|
||||
{% block tail %}
|
||||
{{super()}}
|
||||
{{lib.form_js()}}
|
||||
{% endblock tail %}
|
|
@ -6,7 +6,6 @@
|
|||
<title>System Status</title>
|
||||
<link href="{{url_for('static', filename='css/bootstrap.min.css')}}" rel="stylesheet">
|
||||
<link href="{{url_for('static', filename='css/bootstrap-icons.css')}}" rel="stylesheet">
|
||||
<link rel="icon" href="{{url_for('static', filename='img/ltf-logo.png')}}">
|
||||
<style>
|
||||
.bd-placeholder-img {
|
||||
font-size: 1.125rem;
|
||||
|
@ -27,7 +26,7 @@
|
|||
<main>
|
||||
<div class="py-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="{{url_for('static', filename='img/ltf-logo.svg')}}" alt="" width="72" height="57">
|
||||
<h2>Systems Status</h2>
|
||||
<h2>Server Status</h2>
|
||||
<p class="lead">The current status of the Liberty Tech Force systems.</p>
|
||||
</div>
|
||||
{% if overall_status == 'operational' %}
|
||||
|
@ -56,21 +55,13 @@
|
|||
{% endif %}
|
||||
{{service.name}}
|
||||
{% if service.status == 'operational' %}
|
||||
<span class="text-success">
|
||||
<span class="p-2 bg-success rounded-circle">
|
||||
{% elif service.status == 'unclear' %}
|
||||
<span class="text-warning">
|
||||
<span class="p-2 bg-warning rounded-circle">
|
||||
{% elif service.status == 'offline' %}
|
||||
<span class="text-danger">
|
||||
{% endif %}
|
||||
{{service.status.title()}}
|
||||
|
||||
{% if service.status == 'operational' %}
|
||||
<i class="bi-check-circle-fill"></i>
|
||||
{% elif service.status == 'unclear' %}
|
||||
<i class="bi-question-circle-fill"></i>
|
||||
{% elif service.status == 'unclear' %}
|
||||
<i class="bi-exclamation-circle-fill"></i>
|
||||
<span class="p-2 bg-danger rounded-circle">
|
||||
{% endif %}
|
||||
<span class="visually-hidden">{{service.status}}</span>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
from flask import Blueprint, redirect, request, url_for, render_template
|
||||
from flask_admin import AdminIndexView, expose
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
from flask_admin.helpers import validate_form_on_submit
|
||||
from flask_login import current_user, login_user, logout_user
|
||||
from werkzeug.security import check_password_hash
|
||||
from wtforms.form import Form
|
||||
from wtforms.fields import StringField, PasswordField
|
||||
from wtforms.validators import ValidationError, required
|
||||
|
||||
from statusforce.models import Service, Incident, User
|
||||
|
||||
# The blueprint for the main page
|
||||
main = Blueprint('main', __name__, url_prefix='/')
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
email = StringField(validators=[required()])
|
||||
password = PasswordField(validators=[required()])
|
||||
|
||||
def validate_login(self, field):
|
||||
user = self.get_user()
|
||||
if user is None or not check_password_hash(user.password, self.password.data):
|
||||
raise ValidationError('Unable to log user in')
|
||||
|
||||
def get_user(self):
|
||||
return User.query.filter_by(email=self.email.data).first()
|
||||
|
||||
|
||||
class AuthModelView(ModelView):
|
||||
"""
|
||||
A derivative ModelView that supports user authentication
|
||||
"""
|
||||
def is_accessible(self):
|
||||
return current_user.is_authenticated
|
||||
|
||||
|
||||
class AuthAdminIndexView(AdminIndexView):
|
||||
"""
|
||||
A derivative AdminIndexView that supports user authentication
|
||||
"""
|
||||
|
||||
@expose('/')
|
||||
def index(self):
|
||||
"""
|
||||
Override the index page implementation to check for user permissions, and redirect to the login page
|
||||
"""
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for('.login'))
|
||||
return super().index()
|
||||
|
||||
@expose('/login/', methods=('GET', 'POST'))
|
||||
def login(self):
|
||||
"""
|
||||
A page for the user to log in
|
||||
"""
|
||||
form = LoginForm(request.form)
|
||||
if validate_form_on_submit(form):
|
||||
user = form.get_user()
|
||||
login_user(user)
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('.index'))
|
||||
self._template_args['form'] = form
|
||||
return super().index()
|
||||
|
||||
@expose('/logout/')
|
||||
def logout(self):
|
||||
"""
|
||||
Log the user out of the system
|
||||
"""
|
||||
logout_user()
|
||||
return redirect(url_for('.index'))
|
||||
|
||||
|
||||
@main.route('')
|
||||
def index():
|
||||
"""
|
||||
Show the status page
|
||||
"""
|
||||
services = Service.query.all()
|
||||
incidents = Incident.query.order_by(Incident.created.desc()).limit(10).all()
|
||||
service_statuses = [service.status for service in services]
|
||||
overall_status = 'operational' if all(status == 'operational' for status in service_statuses) else \
|
||||
'offline' if all(status == 'offline' for status in service_statuses) else 'unclear'
|
||||
return render_template('index.html', services=services, incidents=incidents, overall_status=overall_status)
|
Loading…
Reference in New Issue