init-branch-user-device

1 parent 166bffbe
5.0.3 2026-04-28
- Penambahan field session_id pada table users digunakan untuk membatasi jumlah
login hanya bisa ada pada satu device
- Penambahan parameter single_device
- Penambahan module change password saat menggunakan external-uim
5.0.2
Menambahkan path static 'mobi' and configuration 'mobile_static_path'
5.0.1
- Bug Saat get captcha session tidak berfungsi dimana ditambahkan checking melalui
file jpg yang tersimpan dalam folder captcha files
......
......@@ -9,13 +9,12 @@
use = egg:opensipkd_base
reload_templates = true
pyramid.debug_all = true
pyramid.debug_authorization = false
pyramid.debug_authorization = true
pyramid.debug_notfound = true
pyramid.debug_routematch = true
pyramid.debug_templates = true
default_locale_name = id
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/asinan
session.url = postgresql://aagusti:a@localhost:5432/asinan
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/demos
pyramid.includes =
pyramid_tm
pyramid_beaker
......@@ -27,12 +26,21 @@ pyramid.includes =
; opensipkd.pbb.monitoring
session.url = postgresql://aagusti:a@localhost:5432/demo
session.type = ext:database
session.secret = s0s3cr3ts
session.cookie_expires = true
session.key = WhatEver
session.timeout = 3000
session.lock_dir = %(here)s/tmp
session.httponly=True
; Prevents JavaScript from stealing the session cookie (mitigates XSS).
; session.secure=True
; Forces the cookie to be sent only over HTTPS.
session.samesite='Lax'
; or 'Strict' Prevents Cross-Site Request Forgery (CSRF) by limiting cookie scope.
; session.timeout 1800 (30 mins) Automatically expires the session after inactivity.
timezone = Asia/Jakarta
;localization = id_ID.UTF-8
#localization = Indonesian_indonesia.1252
......@@ -43,6 +51,8 @@ temp_files = C:\tmp
partner_doc = C:\\tmp\\docs\\partner\\
;home_tpl = opensipkd.samsat.jabar.views:templates/login.pt
;login_tpl = opensipkd.samsat.jabar.views:templates/login.pt
# Securitty
single_device = true
login_captcha = 0
# Registrasi User
......@@ -141,8 +151,8 @@ keys = console, filelog
keys = generic
[logger_root]
level = WARN
handlers =
level = DEBUG
handlers = console, filelog
#, tabel
[logger_opensipkd]
......@@ -151,7 +161,7 @@ handlers =console, filelog
qualname = opensipkd
[logger_sqlalchemy]
level = WARN
level = DEBUG
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
......@@ -183,13 +193,13 @@ format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
# End logging configuration
[alembic_ziggurat]
script_location = ziggurat_foundations:migrations
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/asinan
; [alembic_ziggurat]
; script_location = ziggurat_foundations:migrations
; sqlalchemy.url = postgresql://aagusti:a@localhost:5432/asinan
[alembic_base]
script_location = opensipkd.base:alembic
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/asinan
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/demos
[alembic_base_init]
script_location = opensipkd.base:alembic
......
###
# app configuration
# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
;Digunakan untuk setting sebagai app:main atau app:virdir (virtual directory)
[app:main]
;[app:virdir]
use = egg:opensipkd_base
reload_templates = true
pyramid.debug_all = true
pyramid.debug_authorization = false
pyramid.debug_notfound = true
pyramid.debug_routematch = true
pyramid.debug_templates = true
default_locale_name = id
sqlalchemy.url = postgresql://user:pass@server:port/db
sqlalchemy.pool_pre_ping = True
sqlalchemy.pool_size = 20
sqlalchemy.echo = False
session.url = postgresql://user:pass@server:port/db
pyramid.includes =
pyramid_tm
pyramid_beaker
pyramid_chameleon
pyramid_rpc.jsonrpc
# pyramid_debugtoolbar
session.type = ext:database
session.secret = s0s3cr3ts
session.cookie_expires = true
session.key = WhatEver
session.timeout = 3000
session.lock_dir = %(here)s/tmp
timezone = Asia/Jakarta
;localization = id_ID.UTF-8
#localization = Indonesian_indonesia.1252
localization = English_Australia.1252
# Base Configuration
# temp_files digunakan untuk menyimpan file sementara, seperti hasil export,
# hasil upload, dll default C:\tmp atau /tmp
temp_files = %(here)s/../tmp
# partner_doc digunakan untuk menyimpan file upload dokumen partner harus permanen,
# default /tmp/docs/partner/ C:\\tmp\\docs\\partner\\
partner_doc = %(here)s/../tmp/docs/partner/
# Registrasi User
# Digunakan untuk mengatur apakah aplikasi akan menyediakan fitur registrasi user baru,
allow_register = 1
# Template form registrasi, jika kosong akan menggunakan template default dari opensipkd,
reg_form =
# Digunakan untuk mengatur apakah aplikasi akan meminta input nomor KTP, jika 1
# maka akan muncul field input nomor KTP pada form registrasi,
reg_idcard = 1
# Digunakan untuk mengatur apakah aplikasi akan meminta input nomor Captcha, jika 1
# maka akan muncul field input nomor Captcha pada form registrasi,
reg_captcha = 1
# reg_verify digunakan untuk mengatur apakah aplikasi akan dilakukan verifikasi
# manual email verifikasi setelah registrasi, jika 1 maka aplikasi akan mengirim
# email verifikasi ke email yang didaftarkan oleh user,
reg_verify = 1
# Custome template untuk login ke aplikasi
login_tpl =
# Menampilkan captcha pada form login, jika 1 maka akan muncul field input nomor Captcha pada form login,
login_captcha = 1
static_files = %(here)s/../files
captcha_files = %(here)s/../tmp/captcha
;company = Opensipkd
;ibukota = Bekasi
;departement = IT
;address_1 = Jalan....
;address_2 = Bekasi ...
;
;center.phone = 021123456789
;center.mobile = 081311045668
;center.email = aa.gustiana@gmail.com
;center.email_password =
;center.smtp_server =
;
;#_host = http://localhost:5433/demo2
;
;unoconv_py = C:\Program Files\LibreOffice\program\python.exe
;unoconv_bin = C:\product\venv-lates\Scripts\unoconv
;
# `menus` Digunakan untuk mengatur menu apa saja yang akan ditampilkan pada aplikasi,
# format menu: url:label atu routekode:label antar menu dipisahkan dengan baris baru, contoh:
; menus =
; bphtb:BPHTB
; five:Five
# Default nama aplikasi
;app_name = OPENSIPKD-APP
# digunakan untuk mengatur apakah user bisa berganti2 dinas
;change_unit = False
;departemen_chg_id = 3
; PROXY Setting tambahan untuk proxy
;trusted_proxy_headers = "forwarded x-forwarded-for x-forwarded-host x-forwarded-proto x-forwarded-port"
;url_prefix='/wsgi'
;[composite:main]
;use = egg:rutter#urlmap
;/ = opensipkd_base
;/wsgi/ = opensipkd_base
;[filter:proxy-prefix]
;use = egg:PasteDeploy#prefix
# digunakan untuk mengatur prefix url jika aplikasi dijalankan di virtual
# directory, contoh:
# jika aplikasi dijalankan di http://localhost:6543/virdir maka prefix yang digunakan adalah /virdir,
# jika aplikasi dijalankan di http://localhost:6543/ maka prefix yang digunakan adalah /,
# jika aplikasi dijalankan di http://localhost:6543/app/ maka prefix yang digunakan adalah /app, dst
;prefix = /virdir
;[pipeline:main]
;pipeline =
; proxy-prefix
; opensipkd_base
[server:main]
use = egg:waitress#main
host = 0.0.0.0
port = 6543
[alembic_ziggurat]
script_location = ziggurat_foundations:migrations
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/demo2
[alembic_base]
script_location = opensipkd.base.scripts:alembic
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/demo2
[pytest]
filterwarnings =
error
ignore::UserWarning
ignore:function ham\(\) is deprecated:DeprecationWarning
# Begin logging configuration
[loggers]
keys = root, opensipkd, sqlalchemy
[handlers]
keys = console, filelog
#, tabel
[formatters]
keys = generic
[logger_root]
level = WARN
handlers =
#, tabel
[logger_opensipkd]
level = DEBUG
handlers =console, filelog
qualname = opensipkd
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
# "level = DEBUG" logs SQL queries and results.
# "level = WARN" logs neither. (Recommended for production systems.)
[handler_filelog]
class = FileHandler
; args = ('log_file','a')
args = ('/tmp/logs/opensipkd.log','a')
level = DEBUG
formatter = generic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[handler_tabel]
class = opensipkd.base.handlers.SQLAlchemyHandler
args = ()
level = WARN
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
# End logging configuration
......@@ -336,6 +336,8 @@ def main(global_config, **settings):
BASE_CLASS.route_from_csv(config, filename=routes_file)
BASE_CLASS.route_from_list(config)
BASE_CLASS.static_view(config, settings=settings)
BASE_CLASS.single_device = settings.get(
"single_device", "false").lower() == "true"
config.scan(".")
# _logging.debug(config)
return config.make_wsgi_app()
......@@ -457,6 +459,16 @@ def add_cors_headers_response_callback(event):
event.request.add_response_callback(cors_headers)
@subscriber(NewRequest)
def check_single_device_session(event):
request = event.request
if request.user and BASE_CLASS.single_device:
if request.user.session_id != request.session.id:
request.session.invalidate()
request.session.flash("Sesi Anda telah berakhir karena login dari perangkat lain.", "error")
raise HTTPFound(location=request.route_url('base-login'), headers=forget(request))
@subscriber(BeforeRender)
def add_global_render(event):
event['has_permission'] = has_permission_
......@@ -504,6 +516,7 @@ class BaseApp():
self.login_captcha = 0
self.base_dir = os.path.split(__file__)[0]
self.reg_nip = 0
self.single_device = "false"
def get_route_file(self, filename="routes.csv"):
fullpath = os.path.join(self.base_dir, 'scripts', 'data', filename)
......
......@@ -26,6 +26,11 @@ target_metadata = Base.metadata
# ... etc.
def include_object(object, name, type_, reflected, compare_to):
if type_ == "table" and name != "users":
return False
return True
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
......@@ -44,8 +49,8 @@ def run_migrations_offline() -> None:
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
version_table='alembic_base'
version_table='alembic_base',
include_object=include_object
)
with context.begin_transaction():
......
"""update_user_session
Revision ID: 1be80caf7f0a
Revises: 8de8d8168688
Create Date: 2026-04-28 01:46:36.931499
"""
# revision identifiers, used by Alembic.
revision = '1be80caf7f0a'
down_revision = '8de8d8168688'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('session_id', sa.String(length=256), nullable=True))
op.add_column('users', sa.Column('multi_device', sa.SmallInteger(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'multi_device')
op.drop_column('users', 'session_id')
# ### end Alembic commands ###
from datetime import datetime
import logging
from math import log
from requests import session
import pytz
import sqlalchemy as sa
from pyramid.authorization import (Allow, Authenticated, ALL_PERMISSIONS)
from sqlalchemy import (
Column, Integer, DateTime, String)
Column, Integer, DateTime, SmallInteger, String)
from sqlalchemy.orm import (relationship, backref)
from ziggurat_foundations import ziggurat_model_init
from ziggurat_foundations.models.base import BaseModel
......@@ -107,7 +109,8 @@ class _User(UserMixin, BaseModel):
api_key = Column(String(256))
partner_id = Column(Integer) # , ForeignKey(Partner.id))
company_id = Column(Integer) # , ForeignKey(Partner.id))
session_id = Column(String(256))
multi_device = Column(SmallInteger)
# partners = relationship(Partner, backref=backref('users'))
def _get_password(self):
......
......@@ -6,7 +6,7 @@ from pyramid.httpexceptions import (
HTTPSeeOther)
from pyramid.interfaces import IRoutesMapper
from pyramid.view import view_config
from opensipkd.base import get_params, get_home
from opensipkd.base import get_params
from pyramid.renderers import render_to_response
from .base_views import BaseView
from datetime import timedelta
......
......@@ -86,13 +86,23 @@ def login_validator(form, value):
def get_login_headers(request, user):
if not request.is_xhr and BASE_CLASS.single_device and \
not user.multi_device:
# if user.session_id and user.session_id != request.session.id:
# from beaker.session import Session
# Session(request.headers, id=user.session_id).delete()
user.session_id = request.session.id
User.db_session.add(user)
User.db_session.flush()
UserService.regenerate_security_code(user)
headers = remember(request, user.id)
headers.append(("Token", user.security_code))
log.debug(headers)
user.last_login_date = create_now()
DBSession.add(user)
DBSession.flush()
User.db_session.add(user)
User.db_session.flush()
return headers
......@@ -237,9 +247,9 @@ def oauth2_login(request, params=None):
user = User()
user.from_dict(values)
DBSession.add(user)
DBSession.flush()
DBSession.refresh(user)
User.db_session.add(user)
User.db_session.flush()
User.db_session.refresh(user)
values = {'external_id': id_info['sub'],
'external_user_name': id_info["name"],
'external_email': id_info["email"],
......@@ -248,8 +258,8 @@ def oauth2_login(request, params=None):
"status": 1}
external = ExternalIdentity()
external.from_dict(values)
DBSession.add(external)
DBSession.flush()
User.db_session.add(external)
User.db_session.flush()
if user and user.status != 1:
raise Oauth2UserExc(
"User anda masih menunggu verifikasi atau lagi di blokir")
......@@ -320,7 +330,7 @@ class ViewAuth(BaseView):
# start cek external module
pckgs = get_params('external-uim')
if user:
external_user = DBSession.query(ExternalIdentity).\
external_user = self.db_session.query(ExternalIdentity).\
filter_by(local_user_id=user.id,
external_user_name=identity).first()
pckgs = external_user and pckgs or None
......@@ -334,9 +344,7 @@ class ViewAuth(BaseView):
log.warn(str(e))
request.session.flash(str(e), "error")
return HTTPFound(location=request.route_url('base-login'))
else:
else:
login = LoginUser(self.req)
if not login.login(values, user):
request.session.flash(login.message, "error")
......@@ -475,8 +483,8 @@ def xhr_response(user, headers):
def redirect_login(request, user):
set_user_log("Login Sukses", request, log, user.user_name)
for g in user.groups:
log.debug(f"Group: {g.id} as {g.group_name}")
# for g in user.groups:
# log.debug(f"Group: {g.id} as {g.group_name}")
headers = get_login_headers(request, user)
log.debug(request.headers)
......@@ -524,7 +532,7 @@ class ViewPassword(BaseView):
if 'submit' in request.POST:
controls = request.POST.items()
identity = request.POST.get('email')
q = DBSession.query(User).filter_by(email=identity)
q = self.db_session.query(User).filter_by(email=identity)
schema.user = user = q.first()
try:
c = form.validate(controls)
......@@ -599,7 +607,7 @@ class ViewPassword(BaseView):
request.session.flash('Anda sudah login', 'error')
return HTTPFound(location=f"{request.home}")
code = request.matchdict['code']
q = DBSession.query(User).filter_by(security_code=code)
q = self.db_session.query(User).filter_by(security_code=code)
user = q.first()
now = create_now()
if not user or now - user.security_code_date > one_hour:
......@@ -624,7 +632,7 @@ class ViewPassword(BaseView):
user.security_code = None
UserService.set_password(user, c['new_password'])
DBSession.add(user)
self.db_session.add(user)
headers = get_login_headers(request, user)
request.session.flash('Password baru Anda sudah disimpan.')
set_user_log("Change Password", request, log)
......@@ -645,7 +653,7 @@ class ViewPassword(BaseView):
if 'recreate' not in request.POST:
return HTTPFound(location=f"{request.home}")
request.user.api_key = api_key = generate_api_key()
DBSession.add(request.user)
self.db_session.add(request.user)
msg = 'API Key Anda yang baru {}'.format(api_key)
request.session.flash(msg)
return HTTPFound(location=f"{request.home}")
......@@ -659,8 +667,8 @@ class ViewPassword(BaseView):
user = self.req.user
user.security_code = Random().randint(10000, 99999)
DBSession.add(user)
DBSession.flush()
self.db_session.add(user)
self.db_session.flush()
minutes = two_minutes
data = dict(passcode=user.security_code, minutes=minutes)
here = os.path.abspath(os.path.dirname(__file__))
......@@ -788,7 +796,7 @@ def send_email_pending(
here = os.path.abspath(os.path.dirname(__file__))
body_file = os.path.join(here, body_default_file)
with open(body_file) as f:
with open(body_file, encoding='utf-8') as f:
body_tpl = f.read()
body = _(body_msg_id, default=body_tpl)
sending_mail(request, user, subject, body)
......@@ -804,5 +812,5 @@ def regenerate_security_code(user, hour=1.0):
UserService.regenerate_security_code(user)
user.security_code_date = create_now()
log.debug("Security code: %s", user.security_code)
DBSession.add(user)
User.db_session.add(user)
return hour
......@@ -15,7 +15,7 @@ show_error_codes = true
[project]
name = 'opensipkd_base'
version = '5.0'
version = '5.0.1'
dependencies = [
'deform',
'colander',
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!