main[ADD]attendance_flask:Codigos QR para asistencias

This commit is contained in:
Jorge Enrique Gómez Gómez 2022-10-10 20:59:37 +00:00
parent 6c9b5169d2
commit cbd3e19d7a
6 changed files with 343 additions and 139 deletions

28
get_query.py Normal file
View File

@ -0,0 +1,28 @@
import sqlite3
from datetime import datetime
import pytz
import requests
def get_db_connection():
conn = sqlite3.connect('database.db')
conn.row_factory = sqlite3.Row
return conn
conn = get_db_connection()
data = conn.execute('SELECT user, datein, dateout, ip_branch, id FROM hits WHERE dateout != ? AND dateinspe = ? ', ('', '')).fetchall()
for d in data:
params ={
'employee_id': d[0],
'check_in': d[1],
'check_out': d[2],
'ip_branch': d[3],
}
response = requests.post('http://137.184.126.24:8080/hr_attendance_extended/public/attendance/', json = params).json()
if response["message"]:
time = datetime.now(pytz.timezone('America/Bogota')).strftime("%Y-%m-%d %H:%M:%S")
conn.execute("UPDATE hits SET dateinspe = ? WHERE id = ?", (time, d[4]))
conn.commit()
else:
continue

136
main.py
View File

@ -2,106 +2,164 @@
import uuid import uuid
import sqlite3 import sqlite3
import base64
from datetime import datetime from datetime import datetime
from flask import Flask, request, render_template, current_app import pytz
import os
from flask import Flask, request, render_template
from flask_uuid import FlaskUUID from flask_uuid import FlaskUUID
from flask_qrcode import QRcode from flask_qrcode import QRcode
from flask_socketio import SocketIO, emit from flask_socketio import SocketIO, emit
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
import hashlib import hashlib
import json import requests
# WIP: use sockets to broadcast when a hit to the expected uuid has been
# received, so that connected clients (the Chromium public browser) can
# refresh the qr code and update the list of users
# https://flask-socketio.readthedocs.io
app = Flask(__name__) app = Flask(__name__)
FlaskUUID(app) FlaskUUID(app)
#QRcode(app)
qrcode = QRcode(app) qrcode = QRcode(app)
app.config['SECRET_KEY'] = '53a8373e7ae652cd38beba15454b1dc4' app.config['SECRET_KEY'] = '53a8373e7ae652cd38beba15454b1dc4'
#app = ProxyFix(app, x_for=1, x_host=1)
app.wsgi_app = ProxyFix(app.wsgi_app) app.wsgi_app = ProxyFix(app.wsgi_app)
socketio = SocketIO(app, async_mode=None) socketio = SocketIO(app, async_mode=None)
def get_db_connection(): def get_db_connection():
conn = sqlite3.connect('database.db') conn = sqlite3.connect('database.db')
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
return conn return conn
def hash_string(string): def hash_string(string):
salt = 'xpwQDFQ7gvcQ--rgO7l9yA' salt = 'xpwQDFQ7gvcQ--rgO7l9yA'
return hashlib.md5(salt.encode() + string.encode()).hexdigest() return hashlib.md5(salt.encode() + string.encode()).hexdigest()
def check_hash(string, hashed_string): def check_hash(string, hashed_string):
salt = 'xpwQDFQ7gvcQ--rgO7l9yA' salt = 'xpwQDFQ7gvcQ--rgO7l9yA'
return hashed_string == hashlib.md5(salt.encode() + string.encode()).hexdigest() return hashed_string == hashlib.md5(salt.encode() + string.encode()).hexdigest()
def generate_next_url():
def type_string(string):
return hashlib.md5(string.encode()).hexdigest()
def check_type(string):
if string == '02181ec55d150f6230547dbbf313e4f8':
type_check = 'CheckIn'
elif string == '231d5cc583e69d5b6c5bdced40dcc27c':
type_check = 'CheckOut'
else:
type_check = 'mistake'
return type_check
def generate_next_url(type, ip_branch):
next_uuid = str(uuid.uuid1()) next_uuid = str(uuid.uuid1())
url = request.url_root + str(next_uuid) + '/' + hash_string(next_uuid) url = str(next_uuid) + '/' + hash_string(next_uuid) + \
'/' + type_string(type) + '/' + (ip_branch)
message_bytes = url.encode('ascii')
base64_bytes = base64.b64encode(message_bytes)
url = base64_bytes.decode('ascii')
return url return url
@app.route('/') @app.route('/')
def show_qr_and_list(): def show_qr_and_list():
# TODO: reject direct connections to server; allow access only via proxy # TODO: reject direct connections to server; allow access only via proxy
get_list = requests.get('http://137.184.126.24:8080/hr_attendance_extended/public/attendance/').json()
list_ips = get_list["list_ips"]
ip_branch = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
if ip_branch in list_ips:
access = True
else:
access = False
return render_template("template.html", ip_branch=ip_branch, access=access)
return render_template("template.html")
@app.route('/scan') @app.route('/<uuid:id>/<hashed>/<type>/<ip_branch>')
def show_qr_reader(): def catch_uuids(id, hashed, type, ip_branch):
return current_app.send_static_file('scan.html')
@app.route('/<uuid:id>/<hashed>')
def catch_uuids(id, hashed):
user = request.headers.get('Remote-User') user = request.headers.get('Remote-User')
# TODO: Check directly with Authelia using https://auth.agofer.net/api/verify # TODO: Check directly with Authelia using https://auth.agofer.net/api/verify
time = datetime.now().strftime("%A %Y-%m-%d %H:%M:%S") time = datetime.now(pytz.timezone('America/Bogota')
).strftime("%Y-%m-%d %H:%M:%S")
type_check = check_type(str(type))
error = None error = None
data = [] data = []
conn = get_db_connection() conn = get_db_connection()
existing = conn.execute( existing1 = conn.execute(
'SELECT * FROM hits WHERE uuid = ?', (str(id),)).fetchone() 'SELECT * FROM hits WHERE uuid1 = ?', (str(id),)).fetchone()
existing2 = conn.execute(
'SELECT * FROM hits WHERE uuid2 = ?', (str(id),)).fetchone()
if not user: if not user:
error = 'NO_USERNAME' error = 'NO_USERNAME'
elif existing: elif existing1:
error = 'ALREADY_USED'
elif existing2:
error = 'ALREADY_USED' error = 'ALREADY_USED'
elif not check_hash(str(id), str(hashed)): elif not check_hash(str(id), str(hashed)):
error = 'DIFFERENT_NODE' error = 'DIFFERENT_NODE'
else: else:
conn.execute("INSERT INTO hits (uuid, user) VALUES (?, ?)", (str(id), user)) if type_check == 'mistake':
conn.commit() error = 'DIFFERENT_TYPE'
#socketio.emit('qr_used', {'data': (time, user, str(id))}) else:
url = generate_next_url() exit = conn.execute(
qr = qrcode(url) 'SELECT * FROM hits WHERE user = ? AND dateout = ?', (user, '')).fetchone()
socketio.emit('qr_used', {'data': (qr, url, user, time)}) if type_check == 'CheckIn':
if exit:
error = 'MISSING_EXIT'
else:
conn.execute(
"INSERT INTO hits (user, uuid1, datein) VALUES (?, ?, ?)", (user, str(id), time))
conn.commit()
url = generate_next_url(type_check, str(ip_branch))
qr = qrcode(url)
socketio.emit('qr_used', {'data': (
qr, url, user, time, type_check,)})
elif type_check == 'CheckOut':
if exit:
conn.execute(
"UPDATE hits SET dateout = ?, uuid2 = ?, ip_branch = ? WHERE user = ? AND dateout = ?", (time, str(id), str(ip_branch), user, ''))
conn.commit()
url = generate_next_url(type_check, str(ip_branch))
qr = qrcode(url)
os.system('python get_query.py')
socketio.emit('qr_used', {'data': (
qr, url, user, time, type_check)})
else:
error = 'MISSING_ENTER'
data = conn.execute(
'SELECT * FROM hits WHERE user = ? ORDER BY id DESC LIMIT 10', (user,)).fetchall()
conn.close()
return render_template('thanks.html', user=user, time=time, error=error, type=type_check, hits=data)
data = conn.execute(
'SELECT * FROM hits WHERE user = ? ORDER BY id DESC LIMIT 10', (user,)
).fetchall()
conn.close()
return render_template('thanks.html', user=user, time=time, error=error, hits=data)
@socketio.on('message') @socketio.on('message')
def handle_message(data): def handle_message(data):
print('received message: ' + data) print('received message: ' + data)
@socketio.on('connection') @socketio.on('connection')
def handle_initial_connection(json_data): def handle_initial_connection(json_data):
url = generate_next_url() ip_branch = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
url = generate_next_url('CheckIn', ip_branch)
url2 = generate_next_url('CheckOut', ip_branch)
qr = qrcode(url) qr = qrcode(url)
qr2 = qrcode(url2)
conn = get_db_connection() conn = get_db_connection()
last_entries = conn.execute('SELECT user, created FROM hits ORDER BY id DESC LIMIT 10') last_entries = conn.execute(
'SELECT user, datein FROM hits WHERE datein != ? ORDER BY id DESC LIMIT 10', ('',)).fetchall()
last_entries2 = conn.execute(
'SELECT user, dateout FROM hits WHERE dateout != ? ORDER BY id DESC LIMIT 10', ('',)).fetchall()
array_entries = [] array_entries = []
array_entries2 = []
for entry in last_entries: for entry in last_entries:
array_entries.append((entry[0], entry[1])) array_entries.append((entry[0], entry[1]))
for entry2 in last_entries2:
array_entries2.append((entry2[0], entry2[1]))
conn.close() conn.close()
emit('initial_qr', {'data': (qr, url, array_entries)}) emit('initial_qr', {
#socketio.emit('initial_qr', {'data': (qr,)}) 'data': (qr, url, array_entries, qr2, url2, array_entries2)})
print('received json: ' + str(json_data)) print('received json: ' + str(json_data))
if __name__ == '__main__': if __name__ == '__main__':
#app.run(host='0.0.0.0')
socketio.run(app, host='0.0.0.0') socketio.run(app, host='0.0.0.0')

View File

@ -2,8 +2,12 @@ DROP TABLE IF EXISTS hits;
CREATE TABLE hits ( CREATE TABLE hits (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, user TEXT NOT NULL,
uuid TEXT NOT NULL, uuid1 TEXT NOT NULL,
user TEXT NOT NULL uuid2 TEXT NOT NULL DEFAULT '',
datein TEXT NOT NULL DEFAULT '' ,
dateout TEXT NOT NULL DEFAULT '',
ip_branch TEXT NOT NULL DEFAULT '',
dateinspe TEXT NOT NULL DEFAULT ''
); );

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Instascan</title>
<!-- script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script -->
<!-- script src="https://rawgit.com/schmich/instascan-builds/master/instascan.min.js"></script -->
<script src="/static/instascan.min.js"></script>
</head>
<body>
<video style="outline:none; width:90%;" id="preview"></video>
<script type="text/javascript">
let options = {
video: document.getElementById('preview'),
mirror: false,
};
let scanner = new Instascan.Scanner(options);
scanner.addListener('scan', function (content) {
window.location.replace(content);
});
Instascan.Camera.getCameras().then(function (cameras) {
if (cameras.length > 1) {
scanner.start(cameras[1]);
} else if (cameras.length > 0) {
scanner.start(cameras[0]);
} else {
console.error('No cameras found.');
}
}).catch(function (e) {
console.error(e);
});
</script>
</body>
</html>

View File

@ -1,60 +1,139 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script src="/static/instascan.min.js"></script>
<script src="//code.jquery.com/jquery-1.12.4.min.js"></script> <script src="//code.jquery.com/jquery-1.12.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"
integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA=="
crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
$(document).ready(function() { $(document).ready(function () {
var socket = io(); var socket = io();
socket.on('connect', function() { socket.on('connect', function () {
socket.emit('connection', {data: 'Connected to server'}); socket.emit('connection', { data: 'Connected to server' });
}); });
socket.on('initial_qr', function(msg, cb) { socket.on('initial_qr', function (msg, cb) {
$('#qrcode').prop('src', msg.data[0]); $('#qrcode').prop('src', msg.data[0]);
$('#qrcode2').prop('src', msg.data[3]);
$('#url').prop('href', msg.data[1]).text(msg.data[1]); $('#url').prop('href', msg.data[1]).text(msg.data[1]);
$('#url2').prop('href', msg.data[4]).text(msg.data[4]);
$('#log').empty(); $('#log').empty();
msg.data[2].forEach(function(item) { msg.data[2].forEach(function (item) {
$('#log').append('<li>' + item[0] + ': ' + item[1] + '</li>'); $('#log').append('<li>' + item[0] + ': ' + item[1] + '</li>');
} }
); );
$('#log2').empty();
msg.data[5].forEach(function (item) {
$('#log2').append('<li>' + item[0] + ': ' + item[1] + '</li>');
}
);
if (cb) if (cb)
cb(); cb();
}); });
socket.on('qr_used', function(msg, cb) { socket.on('qr_used', function (msg, cb) {
/* if (msg.data[4] == 'CheckIn') {
location.reload(true); $('#qrcode').prop('src', msg.data[0]).fadeTo(3000, 0.05, function () { $(this).delay(100).fadeTo('slow', 1) });
*/ $('#url').prop('href', msg.data[1]).text(msg.data[1]);
$('#qrcode').prop('src', msg.data[0]).fadeTo('fast', 0.05, function(){$(this).delay(100).fadeTo('slow', 1)}); var new_entry = '<li>' + msg.data[2] + ': ' + msg.data[3] + '</li>';
$('#url').prop('href', msg.data[1]).text(msg.data[1]); $(new_entry).hide().prependTo('#log').fadeIn('slow');
//$('#log').fadeIn(500, function() { $(this).prepend('<li>' + msg.data[2] + ': ' + msg.data[3] + '</li>') }); $('#log').fadeIn(500, function () { $(this).prepend() });
var new_entry = '<li>' + msg.data[2] + ': ' + msg.data[3] + '</li>'; $('#log li:gt(9)').remove();
$(new_entry).hide().prependTo('#log').fadeIn('slow'); }
$('#log').fadeIn(500, function() { $(this).prepend() }); if (msg.data[4] == 'CheckOut') {
$('#log li:gt(9)' ).remove(); $('#qrcode2').prop('src', msg.data[0]).fadeTo(3000, 0.05, function () { $(this).delay(100).fadeTo('slow', 1) });
$('#url2').prop('href', msg.data[1]).text(msg.data[1]);
var new_entry = '<li>' + msg.data[2] + ': ' + msg.data[3] + '</li>';
$(new_entry).hide().prependTo('#log2').fadeIn('slow');
$('#log2').fadeIn(500, function () { $(this).prepend() });
$('#log2 li:gt(9)').remove();
}
if (cb) if (cb)
cb(); cb();
}); });
}); });
</script> </script>
<title>Example</title> <title>Códigos QR</title>
</head> <style>
<body> #contenedor {
<h1>QR Code:</h1> display: flex;
{# flex-direction: row;
<p>For URL <a href="{{ request.url_root + next_uuid }}">{{ request.url_root + next_uuid }}</a></p> flex-wrap: wrap;
<img id="qrcode" src="{{ qrcode(request.url_root + next_uuid, box_size=12, border=5) }}"> }
#}
<p>For URL <a id="url" href="/">__loading...__</a></p>
<img id="qrcode" src="/static/loading.gif">
<h1>Last 10 users:</h1>
<div><ul id="log"></ul></div>
{#
<ul>
{% for hit in hits %}
<li>{{ hit['created'] }}, {{ hit['user'] }}</li>
{% endfor %}
</ul>
#}
</body>
</html>
#contenedor>div {
width: 50%;
}
.block {
text-align: center;
}
.list {
text-align: left;
display: inline-block;
}
#refuse {
font-size: 24pt;
}
</style>
</head>
<body style="text-align: center;">
{% if access == true %}
<h1>Códigos QR</h1>
<p> IP: {{ ip_branch }}</p>
<div id="contenedor">
<div>
<h1>Entrada</h1>
<img id="qrcode" src="/static/loading.gif">
<h1>Últimos 10 usuarios:</h1>
<div class="block">
<ul class="list" id="log"></ul>
</div>
</div>
<div>
<h1>Salida</h1>
<img id="qrcode2" src="/static/loading.gif">
<h1>Últimos 10 usuarios:</h1>
<div class="block">
<ul class="list" id="log2"></ul>
</div>
</div>
</div>
{% else %}
<div id="refuse">
<video style="width:100%; height:100%;" id="preview"></video>
<script type="text/javascript">
let options = {
video: document.getElementById('preview'),
mirror: false,
};
let scanner = new Instascan.Scanner(options);
scanner.addListener('scan', function (content) {
const decodedData = atob(content)
window.location.replace(decodedData);
});
Instascan.Camera.getCameras().then(function (cameras) {
if (cameras.length > 3) {
scanner.start(cameras[3]);
} else if (cameras.length > 2) {
scanner.start(cameras[2]);
} else if (cameras.length > 1) {
scanner.start(cameras[1]);
} else if (cameras.length > 0) {
scanner.start(cameras[0]);
} else {
console.error('No cameras found.');
}
}).catch(function (e) {
console.error(e);
});
</script>
</div>
{% endif %}
</body>
</html>

View File

@ -1,29 +1,97 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<title>Example</title>
</head>
<body>
{% if error == 'NO_USERNAME' %}
<h1>Error</h1>
<p>No username received. <a href='https://auth.agofer.net/'>Login here</a>.</p>
{% elif error == 'ALREADY_USED' %}
<h1>Error</h1>
<p>Code has been used already.</p>
{% elif error == 'DIFFERENT_NODE' %}
<h1>Error</h1>
<p>Code was not generated by this system.</p>
{% else %}
<h1>Thanks</h1>
<p><strong>At {{ time }}</strong>,</p>
<p>Registered entrance or exit for user {{ user }} .</p>
<h2>Last 10 registers for {{ user }}:</h2>
<ul>
{% for hit in hits %}
<li>{{ hit['created'] }}</li>
{% endfor %}
</ul>
{% endif %}
</body>
</html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<title>Registro</title>
<style>
.list {
text-align: left;
display: inline-block;
}
body {
margin: 0;
text-align: center;
font-size: 24pt;
}
.header {
font-size: 40pt;
}
.contenedor {
width: 100%;
margin: auto;
}
button {
display: inline-block;
padding: 2% 8%;
font-size: 34pt;
cursor: pointer;
text-decoration: none;
color: #ffffff;
background-color: #060674;
border: none;
border-radius: 0;
}
</style>
</head>
<body>
<div id="contenedor">
<br /> <br />
{% if error == 'NO_USERNAME' %}
<h1>Error</h1>
<p>No se encontro el usuario.<br /><br /><a href='https://auth.agofer.net/'>Inicia sesión AQUI</a>.</p>
{% elif error == 'ALREADY_USED' %}
<h1>Error</h1>
<p>El enlace ya ha sido utilizado.<br /><br /><a href='https://attendance.agofer.net'>inténteta de nuevo
AQUI</a>.</p>
{% elif error == 'DIFFERENT_NODE' %}
<h1>Error</h1>
<p>El código no fue generado por este sistema.<br /><br /><a href='https://attendance.agofer.net'>inténteta
de nuevo AQUI</a>.</p>
{% elif error == 'DIFFERENT_TYPE' %}
<h1>Error</h1>
<p>No se encontró el tipo de entrada.<br /><br /><a href='https://attendance.agofer.net'>inténteta de nuevo
AQUI</a>.</p>
{% elif error == 'MISSING_EXIT' %}
<h1>!Olvidaste la Salida!</h1>
<p>Para registrar la entrada es necesario terminar la jornada de trabajo anterior.<br /><br /><a
href='https://attendance.agofer.net'>Escanear Salida AQUI</a>.</p>
{% elif error == 'MISSING_ENTER' %}
<h1>!Olvidaste la Entrada!</h1>
<p>Para registrar la salida es necesario iniciar la jornada laboral.<br /><br /><a
href='https://attendance.agofer.net'>Escanear entrada AQUI</a>.</p>
{% else %}
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x text-success" style="color:green;"></i>
<i class="fa fa-check fa-stack-1x fa-inverse"></i>
</span>
<span class="header"><strong>Registro Guardado</strong></span>
<br />
<p><strong>{{ time }}</strong></p>
<p>Registrado {{ type }} para el usuario {{ user }} .</p>
<br />
<span><strong>Últimos 10 registros para {{ user }}:</strong></span>
<br />
<ul class="list">
{% if type == 'CheckOut' %}
{% for hit in hits %}
<li>{{ hit['dateout'] }}</li>
{% endfor %}
{% else %}
{% for hit in hits %}
<li>{{ hit['datein'] }}</li>
{% endfor %}
{% endif %}
</ul>
{% endif %}
<br /> <br />
</div>
<button onclick="window.close();">Cerrar</button>
</body>
</html>