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 5.0.2
Menambahkan path static 'mobi' and configuration 'mobile_static_path' Menambahkan path static 'mobi' and configuration 'mobile_static_path'
5.0.1 5.0.1
- Bug Saat get captcha session tidak berfungsi dimana ditambahkan checking melalui - Bug Saat get captcha session tidak berfungsi dimana ditambahkan checking melalui
file jpg yang tersimpan dalam folder captcha files file jpg yang tersimpan dalam folder captcha files
......
...@@ -9,13 +9,12 @@ ...@@ -9,13 +9,12 @@
use = egg:opensipkd_base use = egg:opensipkd_base
reload_templates = true reload_templates = true
pyramid.debug_all = true pyramid.debug_all = true
pyramid.debug_authorization = false pyramid.debug_authorization = true
pyramid.debug_notfound = true pyramid.debug_notfound = true
pyramid.debug_routematch = true pyramid.debug_routematch = true
pyramid.debug_templates = true pyramid.debug_templates = true
default_locale_name = id default_locale_name = id
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/asinan sqlalchemy.url = postgresql://aagusti:a@localhost:5432/demos
session.url = postgresql://aagusti:a@localhost:5432/asinan
pyramid.includes = pyramid.includes =
pyramid_tm pyramid_tm
pyramid_beaker pyramid_beaker
...@@ -27,12 +26,21 @@ pyramid.includes = ...@@ -27,12 +26,21 @@ pyramid.includes =
; opensipkd.pbb.monitoring ; opensipkd.pbb.monitoring
session.url = postgresql://aagusti:a@localhost:5432/demo
session.type = ext:database session.type = ext:database
session.secret = s0s3cr3ts session.secret = s0s3cr3ts
session.cookie_expires = true session.cookie_expires = true
session.key = WhatEver session.key = WhatEver
session.timeout = 3000 session.timeout = 3000
session.lock_dir = %(here)s/tmp 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 timezone = Asia/Jakarta
;localization = id_ID.UTF-8 ;localization = id_ID.UTF-8
#localization = Indonesian_indonesia.1252 #localization = Indonesian_indonesia.1252
...@@ -43,6 +51,8 @@ temp_files = C:\tmp ...@@ -43,6 +51,8 @@ temp_files = C:\tmp
partner_doc = C:\\tmp\\docs\\partner\\ partner_doc = C:\\tmp\\docs\\partner\\
;home_tpl = opensipkd.samsat.jabar.views:templates/login.pt ;home_tpl = opensipkd.samsat.jabar.views:templates/login.pt
;login_tpl = opensipkd.samsat.jabar.views:templates/login.pt ;login_tpl = opensipkd.samsat.jabar.views:templates/login.pt
# Securitty
single_device = true
login_captcha = 0 login_captcha = 0
# Registrasi User # Registrasi User
...@@ -141,8 +151,8 @@ keys = console, filelog ...@@ -141,8 +151,8 @@ keys = console, filelog
keys = generic keys = generic
[logger_root] [logger_root]
level = WARN level = DEBUG
handlers = handlers = console, filelog
#, tabel #, tabel
[logger_opensipkd] [logger_opensipkd]
...@@ -151,7 +161,7 @@ handlers =console, filelog ...@@ -151,7 +161,7 @@ handlers =console, filelog
qualname = opensipkd qualname = opensipkd
[logger_sqlalchemy] [logger_sqlalchemy]
level = WARN level = DEBUG
handlers = handlers =
qualname = sqlalchemy.engine qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries. # "level = INFO" logs SQL queries.
...@@ -183,13 +193,13 @@ format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s ...@@ -183,13 +193,13 @@ format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
# End logging configuration # End logging configuration
[alembic_ziggurat] ; [alembic_ziggurat]
script_location = ziggurat_foundations:migrations ; script_location = ziggurat_foundations:migrations
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/asinan ; sqlalchemy.url = postgresql://aagusti:a@localhost:5432/asinan
[alembic_base] [alembic_base]
script_location = opensipkd.base:alembic script_location = opensipkd.base:alembic
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/asinan sqlalchemy.url = postgresql://aagusti:a@localhost:5432/demos
[alembic_base_init] [alembic_base_init]
script_location = opensipkd.base:alembic 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): ...@@ -336,6 +336,8 @@ def main(global_config, **settings):
BASE_CLASS.route_from_csv(config, filename=routes_file) BASE_CLASS.route_from_csv(config, filename=routes_file)
BASE_CLASS.route_from_list(config) BASE_CLASS.route_from_list(config)
BASE_CLASS.static_view(config, settings=settings) BASE_CLASS.static_view(config, settings=settings)
BASE_CLASS.single_device = settings.get(
"single_device", "false").lower() == "true"
config.scan(".") config.scan(".")
# _logging.debug(config) # _logging.debug(config)
return config.make_wsgi_app() return config.make_wsgi_app()
...@@ -457,6 +459,16 @@ def add_cors_headers_response_callback(event): ...@@ -457,6 +459,16 @@ def add_cors_headers_response_callback(event):
event.request.add_response_callback(cors_headers) 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) @subscriber(BeforeRender)
def add_global_render(event): def add_global_render(event):
event['has_permission'] = has_permission_ event['has_permission'] = has_permission_
...@@ -504,6 +516,7 @@ class BaseApp(): ...@@ -504,6 +516,7 @@ class BaseApp():
self.login_captcha = 0 self.login_captcha = 0
self.base_dir = os.path.split(__file__)[0] self.base_dir = os.path.split(__file__)[0]
self.reg_nip = 0 self.reg_nip = 0
self.single_device = "false"
def get_route_file(self, filename="routes.csv"): def get_route_file(self, filename="routes.csv"):
fullpath = os.path.join(self.base_dir, 'scripts', 'data', filename) fullpath = os.path.join(self.base_dir, 'scripts', 'data', filename)
......
...@@ -26,6 +26,11 @@ target_metadata = Base.metadata ...@@ -26,6 +26,11 @@ target_metadata = Base.metadata
# ... etc. # ... 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: def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode. """Run migrations in 'offline' mode.
...@@ -44,8 +49,8 @@ def run_migrations_offline() -> None: ...@@ -44,8 +49,8 @@ def run_migrations_offline() -> None:
target_metadata=target_metadata, target_metadata=target_metadata,
literal_binds=True, literal_binds=True,
dialect_opts={"paramstyle": "named"}, dialect_opts={"paramstyle": "named"},
version_table='alembic_base' version_table='alembic_base',
include_object=include_object
) )
with context.begin_transaction(): 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 from datetime import datetime
import logging import logging
from math import log from math import log
from requests import session
import pytz import pytz
import sqlalchemy as sa import sqlalchemy as sa
from pyramid.authorization import (Allow, Authenticated, ALL_PERMISSIONS) from pyramid.authorization import (Allow, Authenticated, ALL_PERMISSIONS)
from sqlalchemy import ( from sqlalchemy import (
Column, Integer, DateTime, String) Column, Integer, DateTime, SmallInteger, String)
from sqlalchemy.orm import (relationship, backref) from sqlalchemy.orm import (relationship, backref)
from ziggurat_foundations import ziggurat_model_init from ziggurat_foundations import ziggurat_model_init
from ziggurat_foundations.models.base import BaseModel from ziggurat_foundations.models.base import BaseModel
...@@ -107,7 +109,8 @@ class _User(UserMixin, BaseModel): ...@@ -107,7 +109,8 @@ class _User(UserMixin, BaseModel):
api_key = Column(String(256)) api_key = Column(String(256))
partner_id = Column(Integer) # , ForeignKey(Partner.id)) partner_id = Column(Integer) # , ForeignKey(Partner.id))
company_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')) # partners = relationship(Partner, backref=backref('users'))
def _get_password(self): def _get_password(self):
......
...@@ -6,7 +6,7 @@ from pyramid.httpexceptions import ( ...@@ -6,7 +6,7 @@ from pyramid.httpexceptions import (
HTTPSeeOther) HTTPSeeOther)
from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IRoutesMapper
from pyramid.view import view_config 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 pyramid.renderers import render_to_response
from .base_views import BaseView from .base_views import BaseView
from datetime import timedelta from datetime import timedelta
......
...@@ -86,13 +86,23 @@ def login_validator(form, value): ...@@ -86,13 +86,23 @@ def login_validator(form, value):
def get_login_headers(request, user): 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) UserService.regenerate_security_code(user)
headers = remember(request, user.id) headers = remember(request, user.id)
headers.append(("Token", user.security_code)) headers.append(("Token", user.security_code))
log.debug(headers) log.debug(headers)
user.last_login_date = create_now() user.last_login_date = create_now()
DBSession.add(user) User.db_session.add(user)
DBSession.flush() User.db_session.flush()
return headers return headers
...@@ -237,9 +247,9 @@ def oauth2_login(request, params=None): ...@@ -237,9 +247,9 @@ def oauth2_login(request, params=None):
user = User() user = User()
user.from_dict(values) user.from_dict(values)
DBSession.add(user) User.db_session.add(user)
DBSession.flush() User.db_session.flush()
DBSession.refresh(user) User.db_session.refresh(user)
values = {'external_id': id_info['sub'], values = {'external_id': id_info['sub'],
'external_user_name': id_info["name"], 'external_user_name': id_info["name"],
'external_email': id_info["email"], 'external_email': id_info["email"],
...@@ -248,8 +258,8 @@ def oauth2_login(request, params=None): ...@@ -248,8 +258,8 @@ def oauth2_login(request, params=None):
"status": 1} "status": 1}
external = ExternalIdentity() external = ExternalIdentity()
external.from_dict(values) external.from_dict(values)
DBSession.add(external) User.db_session.add(external)
DBSession.flush() User.db_session.flush()
if user and user.status != 1: if user and user.status != 1:
raise Oauth2UserExc( raise Oauth2UserExc(
"User anda masih menunggu verifikasi atau lagi di blokir") "User anda masih menunggu verifikasi atau lagi di blokir")
...@@ -320,7 +330,7 @@ class ViewAuth(BaseView): ...@@ -320,7 +330,7 @@ class ViewAuth(BaseView):
# start cek external module # start cek external module
pckgs = get_params('external-uim') pckgs = get_params('external-uim')
if user: if user:
external_user = DBSession.query(ExternalIdentity).\ external_user = self.db_session.query(ExternalIdentity).\
filter_by(local_user_id=user.id, filter_by(local_user_id=user.id,
external_user_name=identity).first() external_user_name=identity).first()
pckgs = external_user and pckgs or None pckgs = external_user and pckgs or None
...@@ -334,9 +344,7 @@ class ViewAuth(BaseView): ...@@ -334,9 +344,7 @@ class ViewAuth(BaseView):
log.warn(str(e)) log.warn(str(e))
request.session.flash(str(e), "error") request.session.flash(str(e), "error")
return HTTPFound(location=request.route_url('base-login')) return HTTPFound(location=request.route_url('base-login'))
else:
else:
login = LoginUser(self.req) login = LoginUser(self.req)
if not login.login(values, user): if not login.login(values, user):
request.session.flash(login.message, "error") request.session.flash(login.message, "error")
...@@ -475,8 +483,8 @@ def xhr_response(user, headers): ...@@ -475,8 +483,8 @@ def xhr_response(user, headers):
def redirect_login(request, user): def redirect_login(request, user):
set_user_log("Login Sukses", request, log, user.user_name) set_user_log("Login Sukses", request, log, user.user_name)
for g in user.groups: # for g in user.groups:
log.debug(f"Group: {g.id} as {g.group_name}") # log.debug(f"Group: {g.id} as {g.group_name}")
headers = get_login_headers(request, user) headers = get_login_headers(request, user)
log.debug(request.headers) log.debug(request.headers)
...@@ -524,7 +532,7 @@ class ViewPassword(BaseView): ...@@ -524,7 +532,7 @@ class ViewPassword(BaseView):
if 'submit' in request.POST: if 'submit' in request.POST:
controls = request.POST.items() controls = request.POST.items()
identity = request.POST.get('email') 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() schema.user = user = q.first()
try: try:
c = form.validate(controls) c = form.validate(controls)
...@@ -599,7 +607,7 @@ class ViewPassword(BaseView): ...@@ -599,7 +607,7 @@ class ViewPassword(BaseView):
request.session.flash('Anda sudah login', 'error') request.session.flash('Anda sudah login', 'error')
return HTTPFound(location=f"{request.home}") return HTTPFound(location=f"{request.home}")
code = request.matchdict['code'] 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() user = q.first()
now = create_now() now = create_now()
if not user or now - user.security_code_date > one_hour: if not user or now - user.security_code_date > one_hour:
...@@ -624,7 +632,7 @@ class ViewPassword(BaseView): ...@@ -624,7 +632,7 @@ class ViewPassword(BaseView):
user.security_code = None user.security_code = None
UserService.set_password(user, c['new_password']) UserService.set_password(user, c['new_password'])
DBSession.add(user) self.db_session.add(user)
headers = get_login_headers(request, user) headers = get_login_headers(request, user)
request.session.flash('Password baru Anda sudah disimpan.') request.session.flash('Password baru Anda sudah disimpan.')
set_user_log("Change Password", request, log) set_user_log("Change Password", request, log)
...@@ -645,7 +653,7 @@ class ViewPassword(BaseView): ...@@ -645,7 +653,7 @@ class ViewPassword(BaseView):
if 'recreate' not in request.POST: if 'recreate' not in request.POST:
return HTTPFound(location=f"{request.home}") return HTTPFound(location=f"{request.home}")
request.user.api_key = api_key = generate_api_key() 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) msg = 'API Key Anda yang baru {}'.format(api_key)
request.session.flash(msg) request.session.flash(msg)
return HTTPFound(location=f"{request.home}") return HTTPFound(location=f"{request.home}")
...@@ -659,8 +667,8 @@ class ViewPassword(BaseView): ...@@ -659,8 +667,8 @@ class ViewPassword(BaseView):
user = self.req.user user = self.req.user
user.security_code = Random().randint(10000, 99999) user.security_code = Random().randint(10000, 99999)
DBSession.add(user) self.db_session.add(user)
DBSession.flush() self.db_session.flush()
minutes = two_minutes minutes = two_minutes
data = dict(passcode=user.security_code, minutes=minutes) data = dict(passcode=user.security_code, minutes=minutes)
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
...@@ -788,7 +796,7 @@ def send_email_pending( ...@@ -788,7 +796,7 @@ def send_email_pending(
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
body_file = os.path.join(here, body_default_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_tpl = f.read()
body = _(body_msg_id, default=body_tpl) body = _(body_msg_id, default=body_tpl)
sending_mail(request, user, subject, body) sending_mail(request, user, subject, body)
...@@ -804,5 +812,5 @@ def regenerate_security_code(user, hour=1.0): ...@@ -804,5 +812,5 @@ def regenerate_security_code(user, hour=1.0):
UserService.regenerate_security_code(user) UserService.regenerate_security_code(user)
user.security_code_date = create_now() user.security_code_date = create_now()
log.debug("Security code: %s", user.security_code) log.debug("Security code: %s", user.security_code)
DBSession.add(user) User.db_session.add(user)
return hour return hour
...@@ -15,7 +15,7 @@ show_error_codes = true ...@@ -15,7 +15,7 @@ show_error_codes = true
[project] [project]
name = 'opensipkd_base' name = 'opensipkd_base'
version = '5.0' version = '5.0.1'
dependencies = [ dependencies = [
'deform', 'deform',
'colander', 'colander',
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!