Merge branch 'master' into dist/envole/6/master
This commit is contained in:
commit
f0ae7eb4f4
|
@ -137,6 +137,7 @@ gos_web_socket:
|
|||
server:
|
||||
port: %websocket_port% #The port the socket server will listen on
|
||||
host: %websocket_host% #The host ip to bind to
|
||||
|
||||
router:
|
||||
resources:
|
||||
- "@CadolesWebsocketBundle/Resources/config/topic.yml"
|
||||
|
|
|
@ -5,10 +5,11 @@ SET foreign_key_checks = 0;
|
|||
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
|
||||
|
||||
INSERT IGNORE INTO `niveau01` (`id`, `label`, `siren`) VALUES
|
||||
(-100, 'scribe26', '0000000A');
|
||||
(-100, 'DRAAF', '130007107');
|
||||
|
||||
INSERT IGNORE INTO `user` (`id`, `niveau01_id`, `username`, `firstname`, `lastname`, `password`, `email`, `avatar`, `role`,`siren`,`authlevel`) VALUES
|
||||
(-100, -100, 'admin', 'Administrateur', 'scribe26', 'PWD_CAS', 'admin@scribe26.ac-arno.fr', 'admin.jpg', 'ROLE_ADMIN', '0000000A', 'simple');
|
||||
(-100, -100, 'admin', 'Administrateur', 'draaf', '{SSHA}d+3hdhVA7sp4QIbp8DlYbcKNekrYbPkG
|
||||
', 'admin@ldapbundle.ac-arno.fr', 'admin.jpg', 'ROLE_ADMIN', '130007107', 'simple');
|
||||
|
||||
|
||||
|
||||
|
@ -24,6 +25,7 @@ INSERT IGNORE INTO `sidebar` (`id`, `parent_id`, `roworder`, `label`, `path`, `f
|
|||
(1200, NULL, 1200, 'ORGANISATION', NULL, 'fa-sitemap', 'ROLE_ADMIN,ROLE_MODO', ''),
|
||||
(1210, 1200, 1210, 'Listes Blanche', 'cadoles_core_config_whitelist', 'fa-tasks', 'ROLE_ADMIN,ROLE_MODO', ''),
|
||||
(1220, 1200, 1220, 'Niveau 01', 'cadoles_core_config_niveau01', 'fa-building', 'ROLE_ADMIN,ROLE_MODO', ''),
|
||||
(1230, 1200, 1230, 'Niveau 02', 'cadoles_core_config_niveau02', 'fa-sitemap', 'ROLE_ADMIN,ROLE_MODO', ''),
|
||||
(1240, 1200, 1240, 'Groupes', 'cadoles_core_config_group', 'fa-users', 'ROLE_ADMIN,ROLE_MODO', ''),
|
||||
(1250, 1200, 1250, 'Inscriptions', 'cadoles_core_config_registration', 'fa-pencil-square-o', 'ROLE_ADMIN,ROLE_MODO', ''),
|
||||
(1260, 1200, 1260, 'Utilisateurs', 'cadoles_core_config_user', 'fa-child', 'ROLE_ADMIN,ROLE_MODO', ''),
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Binary file not shown.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
@ -105,4 +105,47 @@ class ChatController extends Controller
|
|||
$response->headers->set('Content-Type', 'application/json');
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function listAction(Request $request) {
|
||||
// S'assurer que c'est un appel ajax
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
return new JsonResponse(array('message' => 'Interdit'), 400);
|
||||
}
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$id=$request->request->get('id');
|
||||
$output=array();
|
||||
|
||||
// On récupère le groupe
|
||||
$group=$em->getRepository("CadolesCoreBundle:Group")->find($id);
|
||||
if(!$group) die();
|
||||
|
||||
// On récupere le user
|
||||
$user=$this->getUser();
|
||||
if(!$user) die();
|
||||
|
||||
// On récupère le lien usergroup
|
||||
$usergroup=$em->getRepository("CadolesCoreBundle:UserGroup")->findOneBy(["group"=>$group,"user"=>$user]);
|
||||
if(!$usergroup) die();
|
||||
|
||||
// On récupere les messages
|
||||
$messages=$em->getRepository("CadolesWebsocketBundle:Message")->findBy(["group"=>$group],["submitdate"=>"DESC"],10);
|
||||
|
||||
foreach($messages as $message) {
|
||||
$tmp=[];
|
||||
$tmp["id"]=$message->getId();
|
||||
$tmp["message"]=$message->getTopic();
|
||||
$tmp["submitdate"]=$message->getSubmitdate();
|
||||
$tmp["userid"]=$message->getUser()->getId();
|
||||
$tmp["userlastname"]=$message->getUser()->getLastname();
|
||||
$tmp["userfirstname"]=$message->getUser()->getFirstname();
|
||||
$tmp["useravatar"]=$message->getUser()->getAvatar();
|
||||
|
||||
array_push($output,$tmp);
|
||||
}
|
||||
|
||||
$response = new Response(json_encode($output));
|
||||
$response->headers->set('Content-Type', 'application/json');
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,8 @@ cadoles_websocket_chat:
|
|||
|
||||
cadoles_websocket_message_see:
|
||||
path: /user/message/see
|
||||
defaults: { _controller: CadolesWebsocketBundle:Chat:see }
|
||||
defaults: { _controller: CadolesWebsocketBundle:Chat:see }
|
||||
|
||||
cadoles_websocket_message_list:
|
||||
path: /user/message/list
|
||||
defaults: { _controller: CadolesWebsocketBundle:Chat:list }
|
|
@ -115,12 +115,90 @@
|
|||
|
||||
{% block localexternalscript %}
|
||||
<script src="/{{alias}}/bundles/goswebsocket/js/vendor/autobahn.min.js"></script>
|
||||
<script src="/{{alias}}/bundles/goswebsocket/js/gos_web_socket_client.js"></script>
|
||||
<script type="text/javascript" src="/{{alias}}/ckeditor/adapters/jquery.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block localjavascript %}
|
||||
var webSocket;
|
||||
{% if websocket_activate and app.user %}
|
||||
var WS = (function()
|
||||
{
|
||||
var GosSocket = function(uri, sessionConfig){
|
||||
this._uri = uri;
|
||||
this._session = false;
|
||||
this._listeners = {};
|
||||
this.connect(sessionConfig);
|
||||
};
|
||||
|
||||
GosSocket.prototype.connect = function (sessionConfig) {
|
||||
var that = this;
|
||||
|
||||
ab.connect(this._uri,
|
||||
|
||||
//Function on connect
|
||||
function(session){
|
||||
that.fire({type: "socket/connect", data: session });
|
||||
},
|
||||
|
||||
//Function on disconnect / error
|
||||
function(code, reason){
|
||||
that._session = false;
|
||||
|
||||
that.fire({type: "socket/disconnect", data: {code: code, reason: reason}});
|
||||
},
|
||||
|
||||
sessionConfig
|
||||
);
|
||||
};
|
||||
|
||||
GosSocket.prototype.on = function(type, listener){
|
||||
if (typeof this._listeners[type] == "undefined"){
|
||||
this._listeners[type] = [];
|
||||
}
|
||||
|
||||
this._listeners[type].push(listener);
|
||||
};
|
||||
|
||||
GosSocket.prototype.fire = function(event){
|
||||
if (typeof event == "string"){
|
||||
event = { type: event };
|
||||
}
|
||||
if (!event.target){
|
||||
event.target = this;
|
||||
}
|
||||
|
||||
if (!event.type){ //falsy
|
||||
throw new Error("Event object missing 'type' property.");
|
||||
}
|
||||
|
||||
if (this._listeners[event.type] instanceof Array){
|
||||
var listeners = this._listeners[event.type];
|
||||
for (var i=0, len=listeners.length; i < len; i++){
|
||||
listeners[i].call(this, event.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
GosSocket.prototype.off = function(type, listener){
|
||||
if (this._listeners[type] instanceof Array){
|
||||
var listeners = this._listeners[type];
|
||||
for (var i=0, len=listeners.length; i < len; i++){
|
||||
if (listeners[i] === listener){
|
||||
listeners.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
connect: function(uri, sessionConfig)
|
||||
{
|
||||
return new GosSocket(uri, sessionConfig);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
{% endif %}
|
||||
|
||||
$(document).ready(function(){
|
||||
if (CKEDITOR.instances["chat_message"]) CKEDITOR.instances["chat_message"].destroy();
|
||||
|
@ -147,21 +225,49 @@
|
|||
},500);
|
||||
|
||||
$(".switch").bootstrapSwitch();
|
||||
var dateoptions = {weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" };
|
||||
$("#mymodal-sendmail").removeAttr("tabindex");
|
||||
|
||||
{% if websocket_activate and app.user %}
|
||||
websocket();
|
||||
|
||||
});
|
||||
|
||||
{% if websocket_activate and app.user %}
|
||||
|
||||
var session;
|
||||
var dateoptions = {weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" };
|
||||
var timer;
|
||||
var delayRetry=5000;
|
||||
var isdeco=false;
|
||||
var tosend=false;
|
||||
var tosendmail=false;
|
||||
var todelete=false;
|
||||
var idtodelete;
|
||||
|
||||
function websocket()
|
||||
{
|
||||
var _WS_URI = "wss://{{ gos_web_socket_server_host }}:{{ gos_web_socket_server_port }}";
|
||||
webSocket = WS.connect(_WS_URI);
|
||||
var timer;
|
||||
|
||||
webSocket.on("socket/connect", function (session) {
|
||||
// Reinit du tps de tentative de reco
|
||||
$('#modalinfo').modal('hide');
|
||||
clearInterval(timer);
|
||||
webSocket = WS.connect(_WS_URI,{retryDelay: delayRetry});
|
||||
|
||||
webSocket.on("socket/connect", function (sess) {
|
||||
console.log("=== CONNECT ===");
|
||||
|
||||
// Sauvegardce de la session
|
||||
session=sess;
|
||||
|
||||
//the callback function in "subscribe" is called everytime an event is published in that channel.
|
||||
session.subscribe("websocket/channel/{{groupid}}", function (uri, payload) {
|
||||
sess.subscribe("websocket/channel/{{groupid}}", function (uri, payload) {
|
||||
|
||||
// Récupération lastmessage
|
||||
if(isdeco) recuplastmsg();
|
||||
|
||||
// Si action en cours
|
||||
if(tosend) sendbtn();
|
||||
if(tosendmail) sendbtnmail();
|
||||
if(todelete) delmessage(idtodelete);
|
||||
|
||||
// Il est co
|
||||
isdeco=false;
|
||||
|
||||
if(payload.msg) {
|
||||
html ="<div id='message-"+payload.msg.id+"' class='message row'>";
|
||||
html+="<div class='msgavatar'>";
|
||||
|
@ -192,107 +298,170 @@
|
|||
alert(payload.alert);
|
||||
}
|
||||
});
|
||||
|
||||
$( "#sendbtn" ).click(function() {
|
||||
var data = CKEDITOR.instances["chat_message"].getData();
|
||||
if($("#sendbymail").bootstrapSwitch('state')) {
|
||||
CKEDITOR.instances["chat_messagemail"].setData(data);
|
||||
$("#mailsuject").val("{{app.session.get('appname')}} = Notification {{ group.label }}");
|
||||
$("#mailto").val("");
|
||||
$("#listtoavatar").html("");
|
||||
|
||||
{% for user in group.users %}
|
||||
{%if(user.user!=app.user)%}
|
||||
$("#mailto").val($("#mailto").val()+";{{user.user.email}}");
|
||||
$("#listtoavatar").append($("<img class='avatar' title='{{user.user.lastname}} {{user.user.firstname}}' data-mail='{{user.user.email}}' src='\\{{alias}}\\uploads\\avatar\\{{user.user.avatar}}'>"));
|
||||
{%endif%}
|
||||
{% endfor %}
|
||||
|
||||
$('#mymodal-sendmail').modal('show');
|
||||
}
|
||||
else {
|
||||
if(data) {
|
||||
event={mykey: "{{userkey}}", type: "add", message: data};
|
||||
session.publish("websocket/channel/{{groupid}}", event);
|
||||
CKEDITOR.instances["chat_message"].setData('');
|
||||
|
||||
/* Ne plus envoyer d'event compteur on passe par de l'ajax
|
||||
event={type: "add", group:{{groupid}}};
|
||||
parent.parent.counter(event);
|
||||
*/
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$( "#sendbtnmail" ).click(function() {
|
||||
var data = CKEDITOR.instances["chat_messagemail"].getData();
|
||||
var subject=$("#mailsuject").val();
|
||||
var mailto=$("#mailto").val();
|
||||
|
||||
if(data&&subject&&mailto) {
|
||||
event={mykey: "{{userkey}}", type: "add", message: data, mail: "true", to: mailto, subject: subject};
|
||||
session.publish("websocket/channel/{{groupid}}", event);
|
||||
CKEDITOR.instances["chat_message"].setData('');
|
||||
|
||||
/* Ne plus envoyer d'event compteur on passe par de l'ajax
|
||||
event={type: "add", group:{{groupid}}};
|
||||
parent.parent.counter(event);
|
||||
*/
|
||||
|
||||
$("#sendbymail").bootstrapSwitch('state',false);
|
||||
$('#mymodal-sendmail').modal('hide');
|
||||
}
|
||||
else alert("Sujet, corps et destinataire(s) obligatoire");
|
||||
});
|
||||
|
||||
$(document).on('click', '.delmessage', function(){
|
||||
id=$(this).data("id");
|
||||
event={mykey: "{{userkey}}", type: "del", id: id};
|
||||
session.publish("websocket/channel/{{groupid}}", event);
|
||||
});
|
||||
|
||||
$(document).on('click', '#listtoavatar img', function(){
|
||||
var mail = ";"+$(this).data("mail");
|
||||
$("#mailto").val($("#mailto").val().replace(mail, ""));
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
webSocket.on("socket/disconnect", function(error){
|
||||
laberror="Disconnected for " + error.reason + " with code " + error.code;
|
||||
console.log("=== DISCONNECT ===");
|
||||
console.log(laberror);
|
||||
|
||||
$('#modalinfo').modal('show');
|
||||
if(error.code==2) {
|
||||
$('#modalinfotext').html("<center>Impossible de se connecter au serveur.<br>Vous avez dépassé le nombre de reconnexion maximum<br><br>Veuillez retentez d'ici quelques secondes<br><br><code>"+laberror+"</code><br><br><div id='modalinfotimer'></div><br><a class='btn btn-primary' onclick='location.reload(true);'>Forcer la tentative de reconnexion</a></center>");
|
||||
|
||||
clearInterval(timer);
|
||||
var nbtentative=10;
|
||||
timer=setInterval(function(){ $('#modalinfotimer').html((nbtentative--)+"s") }, 1000);
|
||||
|
||||
setTimeout(function(){ location.reload(true); }, 10000);
|
||||
}
|
||||
|
||||
if(error.code==3) {
|
||||
$('#modalinfotext').html("<center>Impossible de se connecter au serveur.<br>Veuillez retentez d'ici quelques secondes<br><br><code>"+laberror+"</code><br><br><div id='modalinfotimer'></div><br><a class='btn btn-primary' onclick='location.reload(true);'>Forcer la tentative de reconnexion</a></center>");
|
||||
|
||||
clearInterval(timer);
|
||||
var nbtentative=10;
|
||||
timer=setInterval(function(){ $('#modalinfotimer').html((nbtentative--)+"s") }, 1000);
|
||||
|
||||
setTimeout(function(){ location.reload(true); }, 10000);
|
||||
}
|
||||
|
||||
if(error.code==6 || error.code==5) {
|
||||
$('#modalinfotext').html("<center>Impossible de se connecter au serveur.<br>Vous allez être reconnecté d'ici quelques secondes<br><br><code>"+laberror+"</code><br><br><div id='modalinfotimer'></div><br><a class='btn btn-primary' onclick='location.reload(true);'>Forcer la tentative de reconnexion</a></center>");
|
||||
|
||||
clearInterval(timer);
|
||||
var nbtentative=5;
|
||||
timer=setInterval(function(){ $('#modalinfotimer').html((nbtentative--)+"s") }, 1000);
|
||||
}
|
||||
isdeco=true;
|
||||
});
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
});
|
||||
$( "#sendbtn" ).click(function() {
|
||||
sendbtn();
|
||||
});
|
||||
|
||||
function sendbtn() {
|
||||
var data = CKEDITOR.instances["chat_message"].getData();
|
||||
if($("#sendbymail").bootstrapSwitch('state')) {
|
||||
CKEDITOR.instances["chat_messagemail"].setData(data);
|
||||
$("#mailsuject").val("{{app.session.get('appname')}} = Notification {{ group.label }}");
|
||||
$("#mailto").val("");
|
||||
$("#listtoavatar").html("");
|
||||
|
||||
{% for user in group.users %}
|
||||
{%if(user.user!=app.user)%}
|
||||
$("#mailto").val($("#mailto").val()+";{{user.user.email}}");
|
||||
$("#listtoavatar").append($("<img class='avatar' title='{{user.user.lastname}} {{user.user.firstname}}' data-mail='{{user.user.email}}' src='\\{{alias}}\\uploads\\avatar\\{{user.user.avatar}}'>"));
|
||||
{%endif%}
|
||||
{% endfor %}
|
||||
|
||||
$('#mymodal-sendmail').modal('show');
|
||||
}
|
||||
else {
|
||||
if(data) {
|
||||
try {
|
||||
// Flager que l'envoit c'est fait
|
||||
tosend=false;
|
||||
$("#sendbtn").html("Envoyer");
|
||||
$("#sendbtn").removeClass("btn-danger");
|
||||
$("#sendbtn").css('cursor','auto');
|
||||
|
||||
// Push envent sur le websocket
|
||||
event={mykey: "{{userkey}}", type: "add", message: data};
|
||||
session.publish("websocket/channel/{{groupid}}", event);
|
||||
CKEDITOR.instances["chat_message"].setData('');
|
||||
}
|
||||
catch(error) {
|
||||
// L'envoi ne s'est pas fait
|
||||
console.log("Retry to send");
|
||||
|
||||
// On flag le fait de retenter l'envoit à la reconnexion
|
||||
tosend=true;
|
||||
|
||||
// On signal que l'envoi est en cours sur le bouton
|
||||
$("#sendbtn").html("Envoi en cours");
|
||||
$("#sendbtn").addClass("btn-danger");
|
||||
$("#sendbtn").css('cursor','progress');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$( "#sendbtnmail" ).click(function() {
|
||||
sendbtnmail();
|
||||
});
|
||||
|
||||
function sendbtnmail() {
|
||||
var data = CKEDITOR.instances["chat_messagemail"].getData();
|
||||
var subject=$("#mailsuject").val();
|
||||
var mailto=$("#mailto").val();
|
||||
|
||||
if(data&&subject&&mailto) {
|
||||
try {
|
||||
// Flager que l'envoit c'est fait
|
||||
tosendmail=false;
|
||||
$("#sendbtnmail").html("Envoyer");
|
||||
$("#sendbtnmail").removeClass("btn-danger");
|
||||
$("#sendbtnmail").css('cursor','auto');
|
||||
|
||||
// Push envent sur le websocket
|
||||
event={mykey: "{{userkey}}", type: "add", message: data, mail: "true", to: mailto, subject: subject};
|
||||
session.publish("websocket/channel/{{groupid}}", event);
|
||||
CKEDITOR.instances["chat_message"].setData('');
|
||||
|
||||
// Cacher la popup
|
||||
$("#sendbymail").bootstrapSwitch('state',false);
|
||||
$('#mymodal-sendmail').modal('hide');
|
||||
}
|
||||
catch(error) {
|
||||
// L'envoi ne s'est pas fait
|
||||
console.log("Retry to send");
|
||||
|
||||
// On flag le fait de retenter l'envoit à la reconnexion
|
||||
tosendmail=true;
|
||||
|
||||
// On signal que l'envoi est en cours sur le bouton
|
||||
$("#sendbtnmail").html("Envoi en cours");
|
||||
$("#sendbtnmail").addClass("btn-danger");
|
||||
$("#sendbtnmail").css('cursor','progress');
|
||||
}
|
||||
|
||||
}
|
||||
else alert("Sujet, corps et destinataire(s) obligatoire");
|
||||
}
|
||||
|
||||
$(document).on('click', '.delmessage', function(){
|
||||
delmessage($(this).data("id"));
|
||||
});
|
||||
|
||||
function delmessage(id) {
|
||||
try {
|
||||
// Flager que l'envoit c'est fait
|
||||
todelete=false;
|
||||
idtodelete=null;
|
||||
|
||||
// Push envent sur le websocket
|
||||
event={mykey: "{{userkey}}", type: "del", id: id};
|
||||
session.publish("websocket/channel/{{groupid}}", event);
|
||||
}
|
||||
catch(error){
|
||||
// Le delete ne s'est pas fait
|
||||
console.log("Retry to delete");
|
||||
|
||||
// On flag le fait de retenter l'envoit à la reconnexion
|
||||
todelete=true;
|
||||
idtodelete=id;
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on('click', '#listtoavatar img', function(){
|
||||
var mail = ";"+$(this).data("mail");
|
||||
$("#mailto").val($("#mailto").val().replace(mail, ""));
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
function recuplastmsg() {
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: "{{ path('cadoles_websocket_message_list') }}",
|
||||
data: {
|
||||
"id": {{ groupid }},
|
||||
},
|
||||
success: function(datas) {
|
||||
$.each(datas, function(i,data) {
|
||||
// Le message a-t-il été déposé durant la déconnexion ?
|
||||
if(!$("#message-"+data.id).length) {
|
||||
console.log("regen message");
|
||||
html ="<div id='message-"+data.id+"' class='message row'>";
|
||||
html+="<div class='msgavatar'>";
|
||||
html+="<img id='user_avatar_img' src='/{{ alias }}/uploads/avatar/"+data.useravatar+"' class='avatar'><br>";
|
||||
if(data.userid=={{app.user.id}} || '{{ fgmanager }}'=='1') {
|
||||
html+="<i class='delmessage fa fa-trash fa-fw' data-id='"+data.id+"' style='cursor: pointer;'></i>";
|
||||
}
|
||||
html+="</div>";
|
||||
html+="<div class='msgdiv'>"
|
||||
html+="<div class='msgtitle'>"+data.userlastname+"<br><small>"+new Date(data.submitdate.date).toLocaleDateString("fr-FR", dateoptions)+"</small></div>";
|
||||
html+="<div class='msgtopic'>"+data.message+"</div>";
|
||||
html+="</div>";
|
||||
html+="</div>";
|
||||
$(".mychat").prepend(html);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -8,7 +8,6 @@ parameters:
|
|||
masteridentity: %%ninegate_masteridentity
|
||||
|
||||
# Mode d'authentification soit SQL / LDAP / CAS / SAML
|
||||
# Pour l'instant SQL - CAS - SAML Reste à faire LDAP
|
||||
# Attention si le masteridentity est différent de SQL le mode d'authentification est forcement différent de SQL
|
||||
# Car aucune synchronisation des password n'est possible
|
||||
mode_auth: %%ninegate_mode_auth
|
||||
|
|
Loading…
Reference in New Issue