This commit is contained in:
MaxKey 2020-12-18 16:20:43 +08:00
commit ba681ed7ca
56 changed files with 1924 additions and 680 deletions

View File

@ -92,17 +92,15 @@ App Management UI
Download the current version of Baidu Pan,<a href="https://maxkey.top/zh/about/download.html" target="_blank"> history version</a>
| Version | ReleaseDate | Download URL | Code |
| --------| :----- | :---- | :----: |
| v 2.3.0 GA | 2020/11/11 | <a href="https://pan.baidu.com/s/17jAatKNlM6L649992kEMBQ" target="_blank">Download</a> | **h3zw** |
| v 2.4.0 RC | 2020/12/15 | <a href="https://pan.baidu.com/s/1xUZZJrxEUmbU3thIxd2lDA" target="_blank">Download</a> | **k65z** |
# Roadmap
1. Implementation of dynamic group (based on user attribute or organization)
1. Part-time organizations
2. Director and part-time organizations
2. Zero trust scenario integration
3. Zero trust scenario integration
4. Maxkey-Cloud (micro service version)-2021
3. Maxkey-Cloud (micro service version)-2021

View File

@ -92,17 +92,15 @@ App Management UI
Download the current version of Baidu Pan,<a href="https://maxkey.top/zh/about/download.html" target="_blank"> history version</a>
| Version | Date | URL | Code |
| --------| :----- | :---- | :----: |
| v 2.3.0 GA | 2020/11/11 | <a href="https://pan.baidu.com/s/17jAatKNlM6L649992kEMBQ" target="_blank">Download</a> | **h3zw** |
| v 2.4.0 RC | 2020/12/15 | <a href="https://pan.baidu.com/s/1xUZZJrxEUmbU3thIxd2lDA" target="_blank">Download</a> | **k65z** |
# Roadmap
1. Implementation of dynamic group (based on user attribute or organization)
1. Part-time organizations
2. Director and part-time organizations
2. Zero trust scenario integration
3. Zero trust scenario integration
4. Maxkey-Cloud (micro service version)-2021
3. Maxkey-Cloud (micro service version)-2021

View File

@ -93,17 +93,15 @@ QQ交流群<b>434469201</b>
当前版本百度网盘下载,<a href="https://maxkey.top/zh/about/download.html" target="_blank"> 历史版本</a>
| 版本 | 日期 | 下载地址 | 提取码 |
| --------| :----- | :---- | :----: |
| v 2.3.0 GA | 2020/11/11 | <a href="https://pan.baidu.com/s/17jAatKNlM6L649992kEMBQ" target="_blank">链接下载</a> | **h3zw** |
| v 2.4.0 RC | 2020/12/15 | <a href="https://pan.baidu.com/s/1xUZZJrxEUmbU3thIxd2lDA" target="_blank">链接下载</a> | **k65z** |
# Roadmap
1.动态用户组实现(基于用户属性或机构)
1.兼职机构
2.主任职机构和兼职机构
2.零信任场景整合
3.零信任场景整合
4.MaxKey Cloud(微服务版)-2021年
3.MaxKey Cloud(微服务版)-2021年

View File

@ -1,4 +1,4 @@
MaxKey v 2.4.0 GA 2020/12/**
MaxKey v 2.4.0 RC 2020/12/16
*(MAXKEY-201001) 动态用户组实现(基于用户属性或机构)
*(MAXKEY-201002) 任职机构和兼职机构
*(MAXKEY-201003) 登录会话切换的优化
@ -14,10 +14,14 @@
*(MAXKEY-201013) SAML 2.0 Metadata优化
*(MAXKEY-201014) HandlerInterceptorAdapter@deprecated 调整为 AsyncHandlerInterceptor
*(MAXKEY-201015) mybatis-jpa升级添加@Entity和@Transient支持优化update时字段为null的处理SQL代码优化
*(MAXKEY-201016) README中文和英文支持
*(MAXKEY-201016) README更新及中文和英文支持
*(MAXKEY-201017) 认证失败时authentication 空指针异常
*(MAXKEY-201018) SAML Metadata URL配置读取
*(MAXKEY-201020) 依赖jar引用、更新和升级
*(MAXKEY-201019) 添加组和角色时按登录名查询成员
*(MAXKEY-201020) firefox点击验证码无更新修复
*(MAXKEY-201021) 官网内容更新
*(MAXKEY-201022) 管理MGT样式调整优化
*(MAXKEY-201023) 依赖jar引用、更新和升级
not-yet-commons-ssl 0.3.9
log4j 2.14.0
spring 5.3.1
@ -26,6 +30,7 @@
springData 2.4.1
springSession 2.4.1
mybatis-jpa-extra 2.2
opensaml 2.6.6
MaxKey v 2.3.0 GA 2020/11/12

View File

@ -1,571 +0,0 @@
package org.apache.commons.ssl;
import javax.naming.InvalidNameException;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
/*
* $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/trunk/src/java/org/apache/commons/ssl/HostnameVerifier.java $
* $Revision: 121 $
* $Date: 2007-11-14 09:26:57 +0400 (Ср., 14 нояб. 2007) $
*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.security.auth.x500.X500Principal;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.TreeSet;
/**
* Interface for checking if a hostname matches the names stored inside the
* server's X.509 certificate. Correctly implements
* javax.net.ssl.HostnameVerifier, but that interface is not recommended.
* Instead we added several check() methods that take SSLSocket,
* or X509Certificate, or ultimately (they all end up calling this one),
* String. (It's easier to supply JUnit with Strings instead of mock
* SSLSession objects!)
* </p><p>Our check() methods throw exceptions if the name is
* invalid, whereas javax.net.ssl.HostnameVerifier just returns true/false.
* <p/>
* We provide the HostnameVerifier.DEFAULT, HostnameVerifier.STRICT, and
* HostnameVerifier.ALLOW_ALL implementations. We also provide the more
* specialized HostnameVerifier.DEFAULT_AND_LOCALHOST, as well as
* HostnameVerifier.STRICT_IE6. But feel free to define your own
* implementations!
* <p/>
* Inspired by Sebastian Hauer's original StrictSSLProtocolSocketFactory in the
* HttpClient "contrib" repository.
*
* @author Julius Davies
* @author <a href="mailto:hauer@psicode.com">Sebastian Hauer</a>
* @since 8-Dec-2006
*/
public interface HostnameVerifier extends javax.net.ssl.HostnameVerifier {
boolean verify(String host, SSLSession session);
void check(String host, SSLSocket ssl) throws IOException;
void check(String host, X509Certificate cert) throws SSLException;
void check(String host, String[] cns, String[] subjectAlts)
throws SSLException;
void check(String[] hosts, SSLSocket ssl) throws IOException;
void check(String[] hosts, X509Certificate cert) throws SSLException;
/**
* Checks to see if the supplied hostname matches any of the supplied CNs
* or "DNS" Subject-Alts. Most implementations only look at the first CN,
* and ignore any additional CNs. Most implementations do look at all of
* the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards
* according to RFC 2818.
*
* @param cns CN fields, in order, as extracted from the X.509
* certificate.
* @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted
* from the X.509 certificate.
* @param hosts The array of hostnames to verify.
* @throws SSLException If verification failed.
*/
void check(String[] hosts, String[] cns, String[] subjectAlts)
throws SSLException;
/**
* The DEFAULT HostnameVerifier works the same way as Curl and Firefox.
* <p/>
* The hostname must match either the first CN, or any of the subject-alts.
* A wildcard can occur in the CN, and in any of the subject-alts.
* <p/>
* The only difference between DEFAULT and STRICT is that a wildcard (such
* as "*.foo.com") with DEFAULT matches all subdomains, including
* "a.b.foo.com".
*/
public final static HostnameVerifier DEFAULT =
new AbstractVerifier() {
public final void check(final String[] hosts, final String[] cns,
final String[] subjectAlts)
throws SSLException {
check(hosts, cns, subjectAlts, false, false);
}
public final String toString() { return "DEFAULT"; }
};
/**
* The DEFAULT_AND_LOCALHOST HostnameVerifier works like the DEFAULT
* one with one additional relaxation: a host of "localhost",
* "localhost.localdomain", "127.0.0.1", "::1" will always pass, no matter
* what is in the server's certificate.
*/
public final static HostnameVerifier DEFAULT_AND_LOCALHOST =
new AbstractVerifier() {
public final void check(final String[] hosts, final String[] cns,
final String[] subjectAlts)
throws SSLException {
if (isLocalhost(hosts[0])) {
return;
}
check(hosts, cns, subjectAlts, false, false);
}
public final String toString() { return "DEFAULT_AND_LOCALHOST"; }
};
/**
* The STRICT HostnameVerifier works the same way as java.net.URL in Sun
* Java 1.4, Sun Java 5, Sun Java 6. It's also pretty close to IE6.
* This implementation appears to be compliant with RFC 2818 for dealing
* with wildcards.
* <p/>
* The hostname must match either the first CN, or any of the subject-alts.
* A wildcard can occur in the CN, and in any of the subject-alts. The
* one divergence from IE6 is how we only check the first CN. IE6 allows
* a match against any of the CNs present. We decided to follow in
* Sun Java 1.4's footsteps and only check the first CN.
* <p/>
* A wildcard such as "*.foo.com" matches only subdomains in the same
* level, for example "a.foo.com". It does not match deeper subdomains
* such as "a.b.foo.com".
*/
public final static HostnameVerifier STRICT =
new AbstractVerifier() {
public final void check(final String[] host, final String[] cns,
final String[] subjectAlts)
throws SSLException {
check(host, cns, subjectAlts, false, true);
}
public final String toString() { return "STRICT"; }
};
/**
* The STRICT_IE6 HostnameVerifier works just like the STRICT one with one
* minor variation: the hostname can match against any of the CN's in the
* server's certificate, not just the first one. This behaviour is
* identical to IE6's behaviour.
*/
public final static HostnameVerifier STRICT_IE6 =
new AbstractVerifier() {
public final void check(final String[] host, final String[] cns,
final String[] subjectAlts)
throws SSLException {
check(host, cns, subjectAlts, true, true);
}
public final String toString() { return "STRICT_IE6"; }
};
/**
* The ALLOW_ALL HostnameVerifier essentially turns hostname verification
* off. This implementation is a no-op, and never throws the SSLException.
*/
public final static HostnameVerifier ALLOW_ALL =
new AbstractVerifier() {
public final void check(final String[] host, final String[] cns,
final String[] subjectAlts) {
// Allow everything - so never blowup.
}
public final String toString() { return "ALLOW_ALL"; }
};
abstract class AbstractVerifier implements HostnameVerifier {
/**
* This contains a list of 2nd-level domains that aren't allowed to
* have wildcards when combined with country-codes.
* For example: [*.co.uk].
* <p/>
* The [*.co.uk] problem is an interesting one. Should we just hope
* that CA's would never foolishly allow such a certificate to happen?
* Looks like we're the only implementation guarding against this.
* Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
*/
private final static String[] BAD_COUNTRY_2LDS =
{"ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
"lg", "ne", "net", "or", "org"};
private final static String[] LOCALHOSTS = {"::1", "127.0.0.1",
"localhost",
"localhost.localdomain"};
static {
// Just in case developer forgot to manually sort the array. :-)
Arrays.sort(BAD_COUNTRY_2LDS);
Arrays.sort(LOCALHOSTS);
}
protected AbstractVerifier() {}
/**
* The javax.net.ssl.HostnameVerifier contract.
*
* @param host 'hostname' we used to create our socket
* @param session SSLSession with the remote server
* @return true if the host matched the one in the certificate.
*/
public boolean verify(String host, SSLSession session) {
try {
Certificate[] certs = session.getPeerCertificates();
X509Certificate x509 = (X509Certificate) certs[0];
check(new String[]{host}, x509);
return true;
}
catch (SSLException e) {
return false;
}
}
public void check(String host, SSLSocket ssl) throws IOException {
check(new String[]{host}, ssl);
}
public void check(String host, X509Certificate cert)
throws SSLException {
check(new String[]{host}, cert);
}
public void check(String host, String[] cns, String[] subjectAlts)
throws SSLException {
check(new String[]{host}, cns, subjectAlts);
}
public void check(String host[], SSLSocket ssl)
throws IOException {
if (host == null) {
throw new NullPointerException("host to verify is null");
}
SSLSession session = ssl.getSession();
if (session == null) {
// In our experience this only happens under IBM 1.4.x when
// spurious (unrelated) certificates show up in the server'
// chain. Hopefully this will unearth the real problem:
InputStream in = ssl.getInputStream();
in.available();
/*
If you're looking at the 2 lines of code above because
you're running into a problem, you probably have two
options:
#1. Clean up the certificate chain that your server
is presenting (e.g. edit "/etc/apache2/server.crt"
or wherever it is your server's certificate chain
is defined).
OR
#2. Upgrade to an IBM 1.5.x or greater JVM, or switch
to a non-IBM JVM.
*/
// If ssl.getInputStream().available() didn't cause an
// exception, maybe at least now the session is available?
session = ssl.getSession();
if (session == null) {
// If it's still null, probably a startHandshake() will
// unearth the real problem.
ssl.startHandshake();
// Okay, if we still haven't managed to cause an exception,
// might as well go for the NPE. Or maybe we're okay now?
session = ssl.getSession();
}
}
Certificate[] certs;
try {
certs = session.getPeerCertificates();
} catch (SSLPeerUnverifiedException spue) {
InputStream in = ssl.getInputStream();
in.available();
// Didn't trigger anything interesting? Okay, just throw
// original.
throw spue;
}
X509Certificate x509 = (X509Certificate) certs[0];
check(host, x509);
}
public void check(String[] host, X509Certificate cert)
throws SSLException {
String[] cns = getCNs(cert);
String[] subjectAlts = getDNSSubjectAlts(cert);
check(host, cns, subjectAlts);
}
public void check(final String[] hosts, final String[] cns,
final String[] subjectAlts, final boolean ie6,
final boolean strictWithSubDomains)
throws SSLException {
// Build up lists of allowed hosts For logging/debugging purposes.
StringBuffer buf = new StringBuffer(32);
buf.append('<');
for (int i = 0; i < hosts.length; i++) {
String h = hosts[i];
h = h != null ? h.trim().toLowerCase() : "";
hosts[i] = h;
if (i > 0) {
buf.append('/');
}
buf.append(h);
}
buf.append('>');
String hostnames = buf.toString();
// Build the list of names we're going to check. Our DEFAULT and
// STRICT implementations of the HostnameVerifier only use the
// first CN provided. All other CNs are ignored.
// (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
TreeSet names = new TreeSet();
if (cns != null && cns.length > 0 && cns[0] != null) {
names.add(cns[0]);
if (ie6) {
for (int i = 1; i < cns.length; i++) {
names.add(cns[i]);
}
}
}
if (subjectAlts != null) {
for (int i = 0; i < subjectAlts.length; i++) {
if (subjectAlts[i] != null) {
names.add(subjectAlts[i]);
}
}
}
if (names.isEmpty()) {
String msg = "Certificate for " + hosts[0] + " doesn't contain CN or DNS subjectAlt";
throw new SSLException(msg);
}
// StringBuffer for building the error message.
buf = new StringBuffer();
boolean match = false;
out:
for (Iterator it = names.iterator(); it.hasNext();) {
// Don't trim the CN, though!
String cn = (String) it.next();
cn = cn.toLowerCase();
// Store CN in StringBuffer in case we need to report an error.
buf.append(" <");
buf.append(cn);
buf.append('>');
if (it.hasNext()) {
buf.append(" OR");
}
// The CN better have at least two dots if it wants wildcard
// action. It also can't be [*.co.uk] or [*.co.jp] or
// [*.org.uk], etc...
boolean doWildcard = cn.startsWith("*.") &&
cn.lastIndexOf('.') >= 0 &&
!isIP4Address(cn) &&
acceptableCountryWildcard(cn);
for (int i = 0; i < hosts.length; i++) {
final String hostName = hosts[i].trim().toLowerCase();
if (doWildcard) {
match = hostName.endsWith(cn.substring(1));
if (match && strictWithSubDomains) {
// If we're in strict mode, then [*.foo.com] is not
// allowed to match [a.b.foo.com]
match = countDots(hostName) == countDots(cn);
}
} else {
match = hostName.equals(cn);
}
if (match) {
break out;
}
}
}
if (!match) {
throw new SSLException("hostname in certificate didn't match: " + hostnames + " !=" + buf);
}
}
public static boolean isIP4Address(final String cn) {
boolean isIP4 = true;
String tld = cn;
int x = cn.lastIndexOf('.');
// We only bother analyzing the characters after the final dot
// in the name.
if (x >= 0 && x + 1 < cn.length()) {
tld = cn.substring(x + 1);
}
for (int i = 0; i < tld.length(); i++) {
if (!Character.isDigit(tld.charAt(0))) {
isIP4 = false;
break;
}
}
return isIP4;
}
public static boolean acceptableCountryWildcard(final String cn) {
int cnLen = cn.length();
if (cnLen >= 7 && cnLen <= 9) {
// Look for the '.' in the 3rd-last position:
if (cn.charAt(cnLen - 3) == '.') {
// Trim off the [*.] and the [.XX].
String s = cn.substring(2, cnLen - 3);
// And test against the sorted array of bad 2lds:
int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s);
return x < 0;
}
}
return true;
}
public static boolean isLocalhost(String host) {
host = host != null ? host.trim().toLowerCase() : "";
if (host.startsWith("::1")) {
int x = host.lastIndexOf('%');
if (x >= 0) {
host = host.substring(0, x);
}
}
int x = Arrays.binarySearch(LOCALHOSTS, host);
return x >= 0;
}
/**
* Counts the number of dots "." in a string.
*
* @param s string to count dots from
* @return number of dots
*/
public static int countDots(final String s) {
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '.') {
count++;
}
}
return count;
}
}
//from Certificate
public static String getCN(X509Certificate cert) {
String[] cns = getCNs(cert);
boolean foundSomeCNs = cns != null && cns.length >= 1;
return foundSomeCNs ? cns[0] : null;
}
public static String[] getCNs(X509Certificate cert) {
try {
final String subjectPrincipal = cert.getSubjectX500Principal().getName(X500Principal.RFC2253);
final LinkedList<String> cnList = new LinkedList<String>();
final LdapName subjectDN = new LdapName(subjectPrincipal);
for (final Rdn rds : subjectDN.getRdns()) {
final Attributes attributes = rds.toAttributes();
final Attribute cn = attributes.get("cn");
if (cn != null) {
try {
final Object value = cn.get();
if (value != null) {
cnList.add(value.toString());
}
} catch (NoSuchElementException ignore) {
} catch (NamingException ignore) {
}
}
}
if (!cnList.isEmpty()) {
return cnList.toArray(new String[cnList.size()]);
}
} catch (InvalidNameException ignore) {
}
return null;
}
/**
* Extracts the array of SubjectAlt DNS names from an X509Certificate.
* Returns null if there aren't any.
* <p/>
* Note: Java doesn't appear able to extract international characters
* from the SubjectAlts. It can only extract international characters
* from the CN field.
* <p/>
* (Or maybe the version of OpenSSL I'm using to test isn't storing the
* international characters correctly in the SubjectAlts?).
*
* @param cert X509Certificate
* @return Array of SubjectALT DNS names stored in the certificate.
*/
public static String[] getDNSSubjectAlts(X509Certificate cert) {
LinkedList subjectAltList = new LinkedList();
Collection c = null;
try {
c = cert.getSubjectAlternativeNames();
}
catch (CertificateParsingException cpe) {
// Should probably log.debug() this?
cpe.printStackTrace();
}
if (c != null) {
Iterator it = c.iterator();
while (it.hasNext()) {
List list = (List) it.next();
int type = ((Integer) list.get(0)).intValue();
// If type is 2, then we've got a dNSName
if (type == 2) {
String s = (String) list.get(1);
subjectAltList.add(s);
}
}
}
if (!subjectAltList.isEmpty()) {
String[] subjectAlts = new String[subjectAltList.size()];
subjectAltList.toArray(subjectAlts);
return subjectAlts;
} else {
return null;
}
}
}

View File

@ -188,6 +188,12 @@ public class Groups extends JpaBaseDomain implements Serializable {
builder.append(id);
builder.append(", name=");
builder.append(name);
builder.append(", dynamic=");
builder.append(dynamic);
builder.append(", filters=");
builder.append(filters);
builder.append(", orgIdsList=");
builder.append(orgIdsList);
builder.append(", isdefault=");
builder.append(isdefault);
builder.append(", description=");

View File

@ -38,6 +38,14 @@ public class Roles extends JpaBaseDomain implements Serializable {
@Column
private String name;
@Column
String dynamic;
@Column
String filters ;
@Column
String orgIdsList;
@Column
String status;
@Column
String description;
@ -119,6 +127,30 @@ public class Roles extends JpaBaseDomain implements Serializable {
this.modifiedDate = modifiedDate;
}
public String getDynamic() {
return dynamic;
}
public void setDynamic(String dynamic) {
this.dynamic = dynamic;
}
public String getFilters() {
return filters;
}
public void setFilters(String filters) {
this.filters = filters;
}
public String getOrgIdsList() {
return orgIdsList;
}
public void setOrgIdsList(String orgIdsList) {
this.orgIdsList = orgIdsList;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
@ -126,6 +158,12 @@ public class Roles extends JpaBaseDomain implements Serializable {
builder.append(id);
builder.append(", name=");
builder.append(name);
builder.append(", dynamic=");
builder.append(dynamic);
builder.append(", filters=");
builder.append(filters);
builder.append(", orgIdsList=");
builder.append(orgIdsList);
builder.append(", status=");
builder.append(status);
builder.append(", description=");

View File

@ -518,4 +518,33 @@ public final class StringUtils {
return flag;
}
public static ArrayList<String> sqlInjection = null;
static{
sqlInjection = new ArrayList<String>();
sqlInjection.add("--");
sqlInjection.add(";");
sqlInjection.add("/");
sqlInjection.add("\\");
sqlInjection.add("#");
sqlInjection.add("drop");
sqlInjection.add("create");
sqlInjection.add("delete");
sqlInjection.add("alter");
sqlInjection.add("truncate");
sqlInjection.add("update");
sqlInjection.add("insert");
sqlInjection.add("and");
sqlInjection.add("or");
}
public static boolean filtersSQLInjection(String filters) {
for(String s : sqlInjection) {
if(filters.indexOf(s)>-1) {
return true;
}
}
return false;
}
}

View File

@ -62,6 +62,10 @@ public class InitializeContext extends HttpServlet {
_logger.info("SecurityContextHolder StrategyName " + SessionSecurityContextHolderStrategy.class.getCanonicalName());
SecurityContextHolder.setStrategyName(SessionSecurityContextHolderStrategy.class.getCanonicalName());
WebContext.applicationContext = applicationContext;
org.apache.mybatis.jpa.util.WebContext.applicationContext = applicationContext;
// List Environment Variables
listEnvVars();

View File

@ -57,6 +57,8 @@ public final class WebContext {
public static Properties properties;
public static ApplicationContext applicationContext;
public static ArrayList<String> sessionAttributeNameList = new ArrayList<String>();
static {
@ -137,23 +139,24 @@ public final class WebContext {
}
/**
* get ApplicationContext from web ServletContext configuration.
*
* get ApplicationContext from web ServletContext configuration
* @return ApplicationContext
*/
public static ApplicationContext getApplicationContext() {
return WebApplicationContextUtils.getWebApplicationContext(
getSession().getServletContext());
public static ApplicationContext getApplicationContext(){
return WebApplicationContextUtils.getWebApplicationContext(getSession().getServletContext());
}
/**
* get bean from spring configuration by bean id.
*
* @param id String
* get bean from spring configuration by bean id
* @param id
* @return Object
*/
public static Object getBean(String id) {
return getApplicationContext().getBean(id);
public static Object getBean(String id){
if(applicationContext==null) {
return getApplicationContext().getBean(id);
}else {
return applicationContext.getBean(id);
}
}
// below method is common HttpServlet method

View File

@ -24,6 +24,7 @@ import java.util.List;
import org.apache.mybatis.jpa.persistence.IJpaBaseMapper;
import org.maxkey.domain.GroupMember;
import org.maxkey.domain.Groups;
/**
* @author Crystal.sea
@ -36,4 +37,13 @@ public interface GroupMemberMapper extends IJpaBaseMapper<GroupMember> {
public List<GroupMember> memberInGroup(GroupMember entity);
public List<GroupMember> memberNotInGroup(GroupMember entity);
public List<GroupMember> groupMemberInGroup(GroupMember entity);
public int addDynamicGroupMember(Groups dynamicGroup);
public int deleteDynamicGroupMember(Groups dynamicGroup);
public int deleteByGroupId(String groupId);
}

View File

@ -20,6 +20,8 @@
*/
package org.maxkey.persistence.mapper;
import java.util.List;
import org.apache.mybatis.jpa.persistence.IJpaBaseMapper;
import org.maxkey.domain.Groups;
@ -30,4 +32,5 @@ import org.maxkey.domain.Groups;
public interface GroupsMapper extends IJpaBaseMapper<Groups> {
public List<Groups> queryDynamicGroups(Groups groups);
}

View File

@ -24,6 +24,7 @@ import java.util.List;
import org.apache.mybatis.jpa.persistence.IJpaBaseMapper;
import org.maxkey.domain.RoleMember;
import org.maxkey.domain.Roles;
/**
* @author Crystal.sea
@ -36,4 +37,10 @@ public interface RoleMemberMapper extends IJpaBaseMapper<RoleMember> {
public List<RoleMember> memberInRole(RoleMember entity);
public List<RoleMember> memberNotInRole(RoleMember entity);
public List<RoleMember> roleMemberInRole(RoleMember entity);
public int addDynamicRoleMember(Roles dynamicRole);
public int deleteDynamicRoleMember(Roles dynamicRole);
public int deleteByRoleId(String roleId);
}

View File

@ -38,4 +38,6 @@ public interface RolesMapper extends IJpaBaseMapper<Roles> {
public int logisticDeleteRolePermissions(List<RolePermissions> rolePermissionsList);
public List<RolePermissions> queryRolePermissions(RolePermissions rolePermissions);
public List<Roles> queryDynamicRoles(Roles role);
}

View File

@ -19,6 +19,7 @@ package org.maxkey.persistence.service;
import org.apache.mybatis.jpa.persistence.JpaBaseService;
import org.maxkey.domain.GroupMember;
import org.maxkey.domain.Groups;
import org.maxkey.persistence.mapper.GroupMemberMapper;
import org.springframework.stereotype.Service;
@ -37,4 +38,16 @@ public class GroupMemberService extends JpaBaseService<GroupMember>{
// TODO Auto-generated method stub
return (GroupMemberMapper)super.getMapper();
}
public int addDynamicGroupMember(Groups dynamicGroup) {
return getMapper().addDynamicGroupMember(dynamicGroup);
}
public int deleteDynamicGroupMember(Groups dynamicGroup) {
return getMapper().deleteDynamicGroupMember(dynamicGroup);
}
public int deleteByGroupId(String groupId) {
return getMapper().deleteByGroupId(groupId);
}
}

View File

@ -17,14 +17,25 @@
package org.maxkey.persistence.service;
import java.util.List;
import org.apache.mybatis.jpa.persistence.JpaBaseService;
import org.maxkey.domain.Groups;
import org.maxkey.persistence.mapper.GroupsMapper;
import org.maxkey.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class GroupsService extends JpaBaseService<Groups>{
final static Logger _logger = LoggerFactory.getLogger(GroupsService.class);
@Autowired
@Qualifier("groupMemberService")
GroupMemberService groupMemberService;
public GroupsService() {
super(GroupsMapper.class);
}
@ -37,4 +48,39 @@ public class GroupsService extends JpaBaseService<Groups>{
// TODO Auto-generated method stub
return (GroupsMapper)super.getMapper();
}
public List<Groups> queryDynamicGroups(Groups groups){
return this.getMapper().queryDynamicGroups(groups);
}
public boolean deleteById(String groupId) {
this.remove(groupId);
groupMemberService.deleteByGroupId(groupId);
return true;
}
public void refreshDynamicGroups(Groups dynamicGroup){
if(dynamicGroup.getDynamic().equals("1")) {
if(dynamicGroup.getOrgIdsList()!=null && !dynamicGroup.getOrgIdsList().equals("")) {
dynamicGroup.setOrgIdsList("'"+dynamicGroup.getOrgIdsList().replace(",", "','")+"'");
}
String filters = dynamicGroup.getFilters();
if(StringUtils.filtersSQLInjection(filters.toLowerCase())) {
_logger.info("filters include SQL Injection Attack Risk.");
return;
}
filters = filters.replace("&", " AND ");
filters = filters.replace("|", " OR ");
dynamicGroup.setFilters(filters);
groupMemberService.deleteDynamicGroupMember(dynamicGroup);
groupMemberService.addDynamicGroupMember(dynamicGroup);
}
}
}

View File

@ -19,6 +19,7 @@ package org.maxkey.persistence.service;
import org.apache.mybatis.jpa.persistence.JpaBaseService;
import org.maxkey.domain.RoleMember;
import org.maxkey.domain.Roles;
import org.maxkey.persistence.mapper.RoleMemberMapper;
import org.springframework.stereotype.Service;
@ -37,4 +38,17 @@ public class RoleMemberService extends JpaBaseService<RoleMember>{
// TODO Auto-generated method stub
return (RoleMemberMapper)super.getMapper();
}
public int addDynamicRoleMember(Roles dynamicRole) {
return getMapper().addDynamicRoleMember(dynamicRole);
}
public int deleteDynamicRoleMember(Roles dynamicRole) {
return getMapper().deleteDynamicRoleMember(dynamicRole);
}
public int deleteByRoleId(String roleId) {
return getMapper().deleteByRoleId(roleId);
}
}

View File

@ -23,11 +23,21 @@ import org.apache.mybatis.jpa.persistence.JpaBaseService;
import org.maxkey.domain.RolePermissions;
import org.maxkey.domain.Roles;
import org.maxkey.persistence.mapper.RolesMapper;
import org.maxkey.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class RolesService extends JpaBaseService<Roles>{
final static Logger _logger = LoggerFactory.getLogger(RolesService.class);
@Autowired
@Qualifier("roleMemberService")
RoleMemberService roleMemberService;
public RolesService() {
super(RolesMapper.class);
}
@ -51,4 +61,36 @@ public class RolesService extends JpaBaseService<Roles>{
public List<RolePermissions> queryRolePermissions(RolePermissions rolePermissions){
return getMapper().queryRolePermissions(rolePermissions);
}
public List<Roles> queryDynamicRoles(Roles dynamicRole){
return this.getMapper().queryDynamicRoles(dynamicRole);
}
public boolean deleteById(String roleId) {
this.remove(roleId);
roleMemberService.deleteByRoleId(roleId);
return true;
}
public void refreshDynamicRoles(Roles dynamicRole){
if(dynamicRole.getDynamic().equals("1")) {
if(dynamicRole.getOrgIdsList()!=null && !dynamicRole.getOrgIdsList().equals("")) {
dynamicRole.setOrgIdsList("'"+dynamicRole.getOrgIdsList().replace(",", "','")+"'");
}
String filters = dynamicRole.getFilters();
if(StringUtils.filtersSQLInjection(filters.toLowerCase())) {
_logger.info("filters include SQL Injection Attack Risk.");
return;
}
filters = filters.replace("&", " AND ");
filters = filters.replace("|", " OR ");
dynamicRole.setFilters(filters);
roleMemberService.deleteDynamicRoleMember(dynamicRole);
roleMemberService.addDynamicRoleMember(dynamicRole);
}
}
}

View File

@ -108,7 +108,13 @@
<if test="groupName != null and groupName != ''">
AND G.NAME = #{groupName}
</if>
AND GM.TYPE = 'USER'
<if test="username != null and username != ''">
AND U.USERNAME = #{username}
</if>
<if test="displayName != null and displayName != ''">
AND U.DISPLAYNAME LIKE '%${displayName}%'
</if>
AND GM.TYPE IN( 'USER','USER-DYNAMIC')
AND GM.GROUPID = G.ID
AND GM.MEMBERID = U.ID
</select>
@ -167,9 +173,15 @@
<if test="groupName != null and groupName != ''">
AND G.NAME = #{groupName}
</if>
AND GM.TYPE = 'USER'
AND GM.TYPE IN( 'USER','USER-DYNAMIC')
AND GM.GROUPID = G.ID
)
<if test="username != null and username != ''">
AND U.USERNAME = #{username}
</if>
<if test="displayName != null and displayName != ''">
AND U.DISPLAYNAME LIKE '%${displayName}%'
</if>
</select>
@ -195,5 +207,54 @@
</if>
</select>
<update id="addDynamicGroupMember" parameterType="Groups" >
INSERT INTO MXK_GROUP_MEMBER(
ID,
GROUPID,
MEMBERID,
TYPE
)
SELECT
CONCAT_WS('-','UD','${id}',U.ID) ID,
'${id}' GROUPID,
U.ID MEMBERID,
'USER-DYNAMIC' TYPE
FROM MXK_USERINFO U
WHERE NOT EXISTS(
SELECT 1 FROM MXK_GROUP_MEMBER GM
WHERE GM.GROUPID=#{id}
AND GM.MEMBERID=U.ID
AND GM.TYPE='USER-DYNAMIC'
)
<if test="filters != null and filters != ''">
AND (${filters})
</if>
<if test="orgIdsList != null and orgIdsList != ''">
AND U.DEPARTMENTID IN( ${orgIdsList})
</if>
</update>
<delete id="deleteDynamicGroupMember" parameterType="Groups" >
DELETE FROM MXK_GROUP_MEMBER GM
WHERE TYPE = 'USER-DYNAMIC'
AND GM.GROUPID=#{id}
AND NOT EXISTS(
SELECT 1
FROM MXK_USERINFO U
WHERE 1 = 1
AND U.ID=GM.MEMBERID
<if test="filters != null and filters != ''">
AND (${filters})
</if>
<if test="orgIdsList != null and orgIdsList != ''">
AND U.DEPARTMENTID IN ( ${orgIdsList})
</if>
)
</delete>
<delete id="deleteByGroupId" parameterType="string" >
DELETE FROM MXK_GROUP_MEMBER GM
WHERE GM.GROUPID=#{value}
</delete>
</mapper>

View File

@ -23,6 +23,16 @@
<include refid="where_statement"/>
</select>
<select id="queryDynamicGroups" parameterType="Groups" resultType="Groups">
SELECT
*
FROM
`MXK_GROUPS`
WHERE
DYNAMIC = '1'
<include refid="where_statement"/>
</select>
<update id="logisticDelete" parameterType="Groups" >
UPDATE `MXK_GROUPS` SET

View File

@ -108,7 +108,13 @@
<if test="roleName != null and roleName != ''">
AND R.NAME = #{roleName}
</if>
AND RM.TYPE = 'USER'
<if test="username != null and username != ''">
AND U.USERNAME = #{username}
</if>
<if test="displayName != null and displayName != ''">
AND U.DISPLAYNAME LIKE '%${displayName}%'
</if>
AND RM.TYPE IN( 'USER','USER-DYNAMIC')
AND RM.ROLEID = R.ID
AND RM.MEMBERID = U.ID
</select>
@ -167,14 +173,21 @@
<if test="roleName != null and roleName != ''">
AND R.NAME = #{roleName}
</if>
AND RM.TYPE = 'USER'
AND RM.TYPE IN( 'USER','USER-DYNAMIC')
AND RM.ROLEID = R.ID
)
<if test="username != null and username != ''">
AND U.USERNAME = #{username}
</if>
<if test="displayName != null and displayName != ''">
AND U.DISPLAYNAME LIKE '%${displayName}%'
</if>
</select>
<!-- ROLE_MEMBER Roles Member-->
<select id="groupMemberInRole" parameterType="RoleMember" resultType="Roles">
<select id="roleMemberInRole" parameterType="RoleMember" resultType="Roles">
SELECT DISTINCT
IR.*
FROM
@ -185,7 +198,7 @@
1 = 1
AND RM.GROUPID = R.ID
AND RM.MEMBERID = IR.ID
AND RM.TYPE = 'ROLE'
AND RM.TYPE IN( 'USER','USER-DYNAMIC')
<if test="roleId != null and roleId != ''">
AND RM.ROLEID = #{roleId}
AND R.ID = #{roleId}
@ -195,5 +208,54 @@
</if>
</select>
<update id="addDynamicRoleMember" parameterType="Roles" >
INSERT INTO MXK_ROLE_MEMBER(
ID,
ROLEID,
MEMBERID,
TYPE
)
SELECT
CONCAT_WS('-','UD','${id}',U.ID) ID,
'${id}' ROLEID,
U.ID MEMBERID,
'USER-DYNAMIC' TYPE
FROM MXK_USERINFO U
WHERE NOT EXISTS(
SELECT 1 FROM MXK_ROLE_MEMBER RM
WHERE RM.ROLEID=#{id}
AND RM.MEMBERID=U.ID
AND RM.TYPE='USER-DYNAMIC'
)
<if test="filters != null and filters != ''">
${filters}
</if>
<if test="orgIdsList != null and orgIdsList != ''">
AND U.DEPARTMENTID IN( ${orgIdsList})
</if>
</update>
<delete id="deleteDynamicRoleMember" parameterType="Roles" >
DELETE FROM MXK_ROLE_MEMBER RM
WHERE TYPE = 'USER-DYNAMIC'
AND RM.ROLEID=#{id}
AND NOT EXISTS(
SELECT 1
FROM MXK_USERINFO U
WHERE 1 = 1
AND U.ID=RM.MEMBERID
<if test="filters != null and filters != ''">
${filters}
</if>
<if test="orgIdsList != null and orgIdsList != ''">
AND U.DEPARTMENTID IN( ${orgIdsList})
</if>
)
</delete>
<delete id="deleteByRoleId" parameterType="string" >
DELETE FROM MXK_ROLE_MEMBER RM
WHERE RM.ROLEID=#{value}
</delete>
</mapper>

View File

@ -11,9 +11,17 @@
</if>
</sql>
<select id="queryDynamicRoles" parameterType="Roles" resultType="Roles">
SELECT
*
FROM
`MXK_GROUPS`
WHERE
DYNAMIC = '1'
<include refid="where_statement"/>
</select>
<select id="queryPageResults" parameterType="Groups" resultType="Groups">
<select id="queryPageResults" parameterType="Roles" resultType="Roles">
SELECT
*
FROM
@ -24,7 +32,7 @@
</select>
<update id="logisticDelete" parameterType="Groups" >
<update id="logisticDelete" parameterType="Roles" >
UPDATE MXK_ROLES SET
STATUS = '2'
WHERE 1 = 1

View File

@ -24,10 +24,20 @@ import org.maxkey.authz.oauth2.provider.token.TokenStore;
import org.maxkey.authz.oauth2.provider.token.store.InMemoryTokenStore;
import org.maxkey.authz.oauth2.provider.token.store.JdbcTokenStore;
import org.maxkey.authz.oauth2.provider.token.store.RedisTokenStore;
import org.maxkey.authz.oidc.idtoken.OIDCIdTokenEnhancer;
import org.maxkey.constants.ConstantsProperties;
import org.maxkey.crypto.password.opt.impl.TimeBasedOtpAuthn;
import org.maxkey.jobs.DynamicGroupsJob;
import org.maxkey.persistence.redis.RedisConnectionFactory;
import org.maxkey.persistence.service.GroupsService;
import org.opensaml.xml.ConfigurationException;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.maxkey.authn.realm.jdbc.JdbcAuthenticationRealm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,6 +47,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@ -44,9 +55,8 @@ import org.springframework.security.crypto.password.PasswordEncoder;
public class MaxKeyMgtConfig implements InitializingBean {
private static final Logger _logger = LoggerFactory.getLogger(MaxKeyMgtConfig.class);
@Bean(name = "oauth20JdbcClientDetailsService")
@Bean(name = "oauth20JdbcClientDetailsService")
public JdbcClientDetailsService JdbcClientDetailsService(
DataSource dataSource,PasswordEncoder passwordReciprocal) {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
@ -111,9 +121,47 @@ public class MaxKeyMgtConfig implements InitializingBean {
return tfaOptAuthn;
}
/**
* schedulerJobsInit.
* @return schedulerJobsInit
* @throws ConfigurationException
* @throws SchedulerException
*/
@Bean(name = "schedulerJobs")
public Scheduler schedulerJobs(
SchedulerFactoryBean schedulerFactoryBean,
GroupsService groupsService,
@Value("${config.job.cron.dynamicgroups}") String cronScheduleDynamicGroups
) throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
dynamicGroupsJob(scheduler,cronScheduleDynamicGroups,groupsService);
return scheduler;
}
private void dynamicGroupsJob(Scheduler scheduler ,
String cronSchedule,
GroupsService groupsService) throws SchedulerException {
JobDetail jobDetail =
JobBuilder.newJob(DynamicGroupsJob.class)
.withIdentity("DynamicGroupsJob", "DynamicGroups")
.build();
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("groupsService", groupsService);
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronSchedule);
CronTrigger cronTrigger =
TriggerBuilder.newTrigger()
.withIdentity("triggerDynamicGroups", "DynamicGroups")
.usingJobData(jobDataMap)
.withSchedule(scheduleBuilder)
.build();
scheduler.scheduleJob(jobDetail,cronTrigger);
}
@Override
public void afterPropertiesSet() throws Exception {
// TODO Auto-generated method stub
}

View File

@ -0,0 +1,56 @@
package org.maxkey.jobs;
import java.util.List;
import org.maxkey.domain.Groups;
import org.maxkey.persistence.service.GroupsService;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DynamicGroupsJob implements Job {
final static Logger _logger = LoggerFactory.getLogger(DynamicGroupsJob.class);
private static GroupsService groupsService = null;
public static class JOBSTATUS{
public static int STOP = 0;
public static int RUNNING = 1;
public static int FINISHED = 2;
}
private static int jobStatus = JOBSTATUS.STOP;
@Override
public void execute(JobExecutionContext context){
if(jobStatus == JOBSTATUS.RUNNING) {
_logger.info("DynamicGroupsJob is in running . " );
return;
}
_logger.debug("DynamicGroupsJob is running ... " );
jobStatus = JOBSTATUS.RUNNING;
try {
if(groupsService == null) {
groupsService = (GroupsService) context.getMergedJobDataMap().get("groupsService");
}
List<Groups> groupsList = groupsService.queryDynamicGroups(null);
for(Groups group : groupsList) {
_logger.debug("group " + group);
groupsService.refreshDynamicGroups(group);
}
Thread.sleep(10 *1000);
_logger.debug("DynamicGroupsJob is success " );
}catch(Exception e) {
_logger.error("Exception " ,e);
jobStatus = JOBSTATUS.STOP;
}
jobStatus = JOBSTATUS.FINISHED;
_logger.debug("DynamicGroupsJob is finished . " );
}
}

View File

@ -0,0 +1,5 @@
package org.maxkey.jobs;
public class DynamicRolesJob {
}

View File

@ -85,6 +85,7 @@ public class GroupsController {
_logger.debug("-Add :" + group);
if (groupsService.insert(group)) {
groupsService.refreshDynamicGroups(group);
return new Message(WebContext.getI18nValue(ConstantsOperateMessage.INSERT_SUCCESS),MessageType.success);
} else {
@ -122,6 +123,7 @@ public class GroupsController {
_logger.debug("-update group :" + group);
if (groupsService.update(group)) {
groupsService.refreshDynamicGroups(group);
return new Message(WebContext.getI18nValue(ConstantsOperateMessage.UPDATE_SUCCESS),MessageType.success);
} else {
@ -136,7 +138,7 @@ public class GroupsController {
public Message delete(@ModelAttribute("group") Groups group) {
_logger.debug("-delete group :" + group);
if (groupsService.remove(group.getId())) {
if (groupsService.deleteById(group.getId())) {
return new Message(WebContext.getI18nValue(ConstantsOperateMessage.DELETE_SUCCESS),MessageType.success);
} else {

View File

@ -85,6 +85,7 @@ public class RolesController {
_logger.debug("-Add :" + role);
if (rolesService.insert(role)) {
rolesService.refreshDynamicRoles(role);
return new Message(WebContext.getI18nValue(ConstantsOperateMessage.INSERT_SUCCESS),MessageType.success);
} else {
@ -122,6 +123,7 @@ public class RolesController {
_logger.debug("-update role :" + role);
if (rolesService.update(role)) {
rolesService.refreshDynamicRoles(role);
return new Message(WebContext.getI18nValue(ConstantsOperateMessage.UPDATE_SUCCESS),MessageType.success);
} else {
@ -136,7 +138,7 @@ public class RolesController {
public Message delete(@ModelAttribute("role") Roles role) {
_logger.debug("-delete role :" + role);
if (rolesService.remove(role.getId())) {
if (rolesService.deleteById(role.getId())) {
return new Message(WebContext.getI18nValue(ConstantsOperateMessage.DELETE_SUCCESS),MessageType.success);
} else {

View File

@ -66,4 +66,6 @@ config.oidc.metadata.issuer=https://${config.server.domain}/maxkey
config.oidc.metadata.authorizationEndpoint=${config.server.name}/maxkey/oauth/v20/authorize
config.oidc.metadata.tokenEndpoint=${config.server.name}/maxkey/oauth/v20/token
config.oidc.metadata.userinfoEndpoint=${config.server.name}/maxkey/api/connect/userinfo
#############################################################################
#############################################################################
#one hour for refresh dynamic groups
config.job.cron.dynamicgroups=0 0 0/1 * * ?

View File

@ -420,6 +420,9 @@ group.orgidslist=\u673a\u6784\u5217\u8868
#role
role.id=\u89d2\u8272\u7f16\u7801
role.name=\u89d2\u8272
role.dynamic=\u52a8\u6001\u7ec4
role.filters=\u8fc7\u6ee4\u5668
role.orgidslist=\u673a\u6784\u5217\u8868
resource.id=\u8d44\u6e90\u7f16\u7801
resource.name=\u8d44\u6e90\u540d\u79f0

View File

@ -419,6 +419,9 @@ group.orgidslist=orgIdsList
#role
role.id=id
role.name=name
role.dynamic=dynamic
role.filters=filters
role.orgidslist=orgIdsList
resource.id=id
resource.name=name

View File

@ -421,6 +421,9 @@ group.orgidslist=\u673a\u6784\u5217\u8868
#role
role.id=\u89d2\u8272\u7f16\u7801
role.name=\u89d2\u8272
role.dynamic=\u52a8\u6001\u7ec4
role.filters=\u8fc7\u6ee4\u5668
role.orgidslist=\u673a\u6784\u5217\u8868
resource.id=\u8d44\u6e90\u7f16\u7801
resource.name=\u8d44\u6e90\u540d\u79f0

View File

@ -148,7 +148,7 @@ header .header-container .nav-left>li, .header .header-container .nav-right>li {
}
.page-container .main-content {
padding: calc(50px + 35px) 15px 15px;
padding: calc(35px + 35px) 15px 15px;
min-height: calc(100vh - 65px);
background: #e6e8ea;
width: 100%;
@ -178,13 +178,17 @@ header .header-container .nav-left>li, .header .header-container .nav-right>li {
}
.breadcrumb-wrapper {
margin-bottom: 20px;
margin-bottom: 10px;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.content-wrapper {
padding-top: 15px;
}
.breadcrumb-wrapper .breadcrumb li {
display: inline-block;
font-size: 14px;

View File

@ -33,7 +33,7 @@
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -113,6 +113,7 @@
</table>
</div>
</div>
</div>
</div>
<footer class="content-footer">

View File

@ -111,6 +111,7 @@
</div>
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -209,6 +210,7 @@
</div>
</div>
</div>
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>

View File

@ -57,6 +57,7 @@
</div>
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-header border-bottom">
@ -241,6 +242,7 @@
</div>
</div>
</div>
</div>
<footer class="content-footer">
<#include "../../layout/footer.ftl"/>
</footer>

View File

@ -58,6 +58,7 @@
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -140,7 +141,7 @@
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>
</div>
</div>
</div>

View File

@ -3,13 +3,13 @@
<head>
<#include "../layout/header.ftl"/>
<#include "../layout/common.cssjs.ftl"/>
<script type="text/javascript">
function dynamicFormatter(value, row, index){
return value=='0'? '<@locale code="common.text.no" />':'<@locale code="common.text.yes" />';
};
</script>
</head>
<script type="text/javascript">
function dynamicFormatter(value, row, index){
return value=='0'? '<@locale code="common.text.no" />':'<@locale code="common.text.yes" />';
};
</script>
<body>
<div class="app header-default side-nav-dark">
<div class="layout">
@ -37,6 +37,7 @@
</div>
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -113,6 +114,7 @@
</div>
</div>
</div>
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>

View File

@ -54,7 +54,7 @@
<td width="120px"><@locale code="userinfo.username"/>:</td>
<td width="374px">
<form id="basic_search_form">
<input class="form-control" type="text" name="name" style ="width:150px;float:left;">
<input class="form-control" type="text" name="username" style ="width:150px;float:left;">
<input class="button btn btn-primary mr-3" id="searchBtn" type="button" size="50" value="<@locale code="button.text.search"/>">
</form>
</td>

View File

@ -57,6 +57,7 @@
</div>
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -151,7 +152,7 @@
</footer>
</div>
</div>
</div>
</div>

View File

@ -11,28 +11,22 @@
<@locale code="global.application"/>
</ul>
<ul class="nav-right">
<li style="font-size: 18px; margin-top: 10px;">
<@locale code="global.text.welcome"/><b>
<li style="font-size: 16px; margin-top: 10px;">
<@locale code="global.text.welcome"/>:<b>
<#if Session["current_user"]?exists>
${Session["current_user"].displayName}
${Session["current_user"].displayName}
(${Session["current_user"].username})
</#if>
(
<#if Session["current_user"]?exists>
${Session["current_user"].username}
</#if>
)&nbsp;&nbsp;</b>
&nbsp;</b>
</li>
<li class="scale-left">
<a class="sidenav-fold-toggler" href="javascript:void(0);">
<img src="<@base/>/static/images/menu-left.png" alt="" style="width: 30px; height: 40px; padding-top: 10px;">
<li class="scale-left" style="margin-top: 5px;">
<a class="sidenav-fold-toggler" href="javascript:void(0);" >
<i class="fa fa-bars fa-2x" aria-hidden="true" style="border:0px"></i>
</a>
</li>
<li class="scale-left">
&nbsp;
</li>
<li class="scale-left">
<li class="scale-left" style="font-size: 18px; margin-top: 5px;">
<a href="<@base/>/logout?reLoginUrl=login">
<IMG SRC="<@base/>/static/images/exit4.png" alt="Exit" style="width: 40px; height: 45px; padding-top: 8px;">
<i class="fa fa-sign-out fa-2x" aria-hidden="true" style="border:0px;color:#e22a6f"></i>
</a>
</li>
</ul>

View File

@ -7,7 +7,7 @@
$(function(){
<#if true==isCaptcha>
$('#j_captchaimg').click(function () {//
$(this).attr("src", "<@base />/captcha");
$(this).attr("src", "<@base />/captcha?"+(new Date()).getTime());
});
</#if>
});

View File

@ -37,7 +37,7 @@
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -121,6 +121,7 @@
</table>
</div>
</div>
</div>
</div>
<footer class="content-footer">

View File

@ -37,7 +37,7 @@
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -130,6 +130,7 @@
</table>
</div>
</div>
</div>
</div>
<footer class="content-footer">

View File

@ -37,7 +37,7 @@
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -124,6 +124,7 @@
</table>
</div>
</div>
</div>
</div>
<footer class="content-footer">

View File

@ -41,10 +41,10 @@
</div>
<div class="container-fluid">
<div class="row">
<div class="row" style="height:115px; padding-top: 10px;">
<div class="col-lg-3 col-md-6 col-xs-12">
<div class="info-box bg-primary">
<div class="icon-box">
<div class="card text-white bg-primary">
<div class="card-body card-body pb-0 d-flex justify-content-between align-items-start">
<i class="lni-home"></i>
</div>
<div class="info-box-content">
@ -54,8 +54,8 @@
</div>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<div class="info-box bg-success">
<div class="icon-box">
<div class="card text-white bg-info">
<div class="card-body card-body pb-0 d-flex justify-content-between align-items-start">
<i class="lni-tag"></i>
</div>
<div class="info-box-content">
@ -65,8 +65,8 @@
</div>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<div class="info-box bg-info">
<div class="icon-box">
<div class="card text-white bg-warning">
<div class="card-body card-body pb-0 d-flex justify-content-between align-items-start">
<i class="lni-cart"></i>
</div>
<div class="info-box-content">
@ -76,8 +76,8 @@
</div>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<div class="info-box bg-purple">
<div class="icon-box">
<div class="card text-white bg-danger">
<div class="card-body card-body pb-0 d-flex justify-content-between align-items-start">
<i class="lni-wallet"></i>
</div>
<div class="info-box-content">

View File

@ -163,7 +163,7 @@ $(function () {
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -249,10 +249,11 @@ $(function () {
</div>
</div>
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>
</div>
</div>
</div>

View File

@ -232,6 +232,7 @@ $('#datagrid').on('click-row.bs.table', function (row, element, field) {
</div>
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -314,6 +315,7 @@ $('#datagrid').on('click-row.bs.table', function (row, element, field) {
</div>
</div>
</div>
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>

View File

@ -163,6 +163,7 @@ $(function () {
</div>
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -254,12 +255,12 @@ $(function () {
</div>
</div>
</div>
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>
</div>
</div>
</div>

View File

@ -9,6 +9,155 @@
vertical-align: middle;
}
</style>
<script type="text/javascript">
function onClick (event, treeId, treeNode) {
var zTree = $.fn.zTree.getZTreeObj("orgsTree");
nodes = zTree.getCheckedNodes(true);
var orgsName = "";
var orgsId = "";
for (var i=0, l=nodes.length; i<l; i++) {
orgsName += nodes[i].name + ",";
orgsId += nodes[i].id + ",";
}
$("#orgIdsListName").val(orgsName);
$("#orgIdsList").val(orgsId);
}
$(function () {
var treeSettings={
element : "orgsTree",
rootId : "1",
checkbox : true,
onClick : onClick,
onDblClick : null,
url : "<@base/>/orgs/tree"
};
function singlePath(newNode) {
if (newNode === curExpandNode) return;
if (curExpandNode && curExpandNode.open==true) {
var zTree = $.fn.zTree.getZTreeObj(treeSettings.element);
if (newNode.parentTId === curExpandNode.parentTId) {
zTree.expandNode(curExpandNode, false);
} else {
var newParents = [];
while (newNode) {
newNode = newNode.getParentNode();
if (newNode === curExpandNode) {
newParents = null;
break;
} else if (newNode) {
newParents.push(newNode);
}
}
if (newParents!=null) {
var oldNode = curExpandNode;
var oldParents = [];
while (oldNode) {
oldNode = oldNode.getParentNode();
if (oldNode) {
oldParents.push(oldNode);
}
}
if (newParents.length>0) {
for (var i = Math.min(newParents.length, oldParents.length)-1; i>=0; i--) {
if (newParents[i] !== oldParents[i]) {
zTree.expandNode(oldParents[i], false);
break;
}
}
} else {
zTree.expandNode(oldParents[oldParents.length-1], false);
}
}
}
}
curExpandNode = newNode;
};
function beforeExpand(treeId, treeNode) {
var pNode = curExpandNode ? curExpandNode.getParentNode():null;
var treeNodeP = treeNode.parentTId ? treeNode.getParentNode():null;
var zTree = $.fn.zTree.getZTreeObj(""+treeSettings.element);
for(var i=0, l=!treeNodeP ? 0:treeNodeP.children.length; i<l; i++ ) {
if (treeNode !== treeNodeP.children[i]) {
zTree.expandNode(treeNodeP.children[i], false);
}
}
while (pNode) {
if (pNode === treeNode) {
break;
}
pNode = pNode.getParentNode();
}
if (!pNode) {
singlePath(treeNode);
}
};
$.fn.zTree.init(
$("#"+treeSettings.element), //element
{//json object
check : {
enable : treeSettings.checkbox
},
async : {
enable : true,
url : treeSettings.url,
autoParam : ["id", "name=n", "level=lv"],
otherParam : {"otherParam":"zTreeAsyncTest",id:treeSettings.rootId},
dataFilter : function (treeId, parentNode, childNodes) {
if (!childNodes) return null;
for (var i=0, l=childNodes.length; i<l; i++) {
childNodes[i].name = childNodes[i].name.replace(/\.n/g, '.');
}
return childNodes;
}
},
data : {
simpleData : {
enable : true
}
},
callback: {
onClick : treeSettings.onClick,
onDblClick : treeSettings.onDblClick,
beforeAsync : function(treeId, treeNode){
$.loading();
},
onAsyncSuccess : function(event, treeId, treeNode, msg){
$.unloading();
},
//beforeExpand : beforeExpand,
onExpand : function onExpand(event, treeId, treeNode) {
curExpandNode = treeNode;
}
}
}
);//end tree
});
function onBodyDown(event) {
if (!(event.target.id == "menuBtn" || event.target.id == "orgIdsListName" || event.target.id == "orgContent" || $(event.target).parents("#orgContent").length>0)) {
$("#orgContent").fadeOut("fast");
$("body").unbind("mousedown", onBodyDown);
}
}
function showOrgsTree() {
var treeObj = $("#orgIdsListName");
var treeOffset = $("#orgIdsListName").offset();
$("#orgContent").css({left:treeOffset.left + "px", top:treeOffset.top + treeObj.outerHeight() + "px"}).slideDown("fast");
$("body").bind("mousedown", onBodyDown);
}
</script>
</head>
<body>
<form id="actionForm" method="post" type="label" autoclose="true" action="<@base/>/roles/add" class="needs-validation" novalidate>
@ -26,10 +175,32 @@
<input type="text" id="name" name="name" class="form-control" title="" value="" required="" />
</td>
</tr>
<tr>
<th><@locale code="role.dynamic" /></th>
<td nowrap>
<select id="dynamic" name="dynamic" class="form-control">
<option value="0" selected ><@locale code="common.text.no" /></option>
<option value="1" ><@locale code="common.text.yes" /></option>
</select>
</td>
</tr>
<tr>
<th><@locale code="role.orgidslist" /></th>
<td nowrap>
<input type="text" id="orgIdsListName" name="orgIdsListName" readonly class="form-control" title="" value="" onclick="showOrgsTree();"/>
<input type="hidden" id="orgIdsList" name="orgIdsList" readonly class="form-control" title="" value="" />
</td>
</tr>
<tr>
<th><@locale code="role.filters" /></th>
<td nowrap>
<textarea id="filters" name="filters" class="form-control" rows="7" cols="20"></textarea>
</td>
</tr>
<tr>
<th><@locale code="common.text.description" /></th>
<td nowrap>
<input type="text" id="description" name="description" class="form-control" title="" value="" />
<textarea id="description" name="description" class="form-control" rows="6" cols="20"></textarea>
</td>
</tr>
@ -44,5 +215,8 @@
</tbody>
</table>
</form>
<div id="orgContent" class="menuContent" style="display:none; position: absolute;">
<ul id="orgsTree" class="ztree" style="margin-top:0; width:180px; height: 300px;"></ul>
</div>
</body>
</html>

View File

@ -9,6 +9,169 @@
vertical-align: middle;
}
</style>
<script type="text/javascript">
function onClick (event, treeId, treeNode) {
var zTree = $.fn.zTree.getZTreeObj("orgsTree");
nodes = zTree.getCheckedNodes(true);
var orgsName = "";
var orgsId = "";
for (var i=0; i<nodes.length; i++) {
orgsName += nodes[i].name + ",";
orgsId += nodes[i].id + ",";
}
$("#orgIdsListName").val(orgsName);
$("#orgIdsList").val(orgsId);
}
$(function () {
var treeSettings={
element : "orgsTree",
rootId : "1",
checkbox : true,
onClick : onClick,
onDblClick : null,
url : "<@base/>/orgs/tree"
};
function singlePath(newNode) {
if (newNode === curExpandNode) return;
if (curExpandNode && curExpandNode.open==true) {
var zTree = $.fn.zTree.getZTreeObj(treeSettings.element);
if (newNode.parentTId === curExpandNode.parentTId) {
zTree.expandNode(curExpandNode, false);
} else {
var newParents = [];
while (newNode) {
newNode = newNode.getParentNode();
if (newNode === curExpandNode) {
newParents = null;
break;
} else if (newNode) {
newParents.push(newNode);
}
}
if (newParents!=null) {
var oldNode = curExpandNode;
var oldParents = [];
while (oldNode) {
oldNode = oldNode.getParentNode();
if (oldNode) {
oldParents.push(oldNode);
}
}
if (newParents.length>0) {
for (var i = Math.min(newParents.length, oldParents.length)-1; i>=0; i--) {
if (newParents[i] !== oldParents[i]) {
zTree.expandNode(oldParents[i], false);
break;
}
}
} else {
zTree.expandNode(oldParents[oldParents.length-1], false);
}
}
}
}
curExpandNode = newNode;
};
function beforeExpand(treeId, treeNode) {
var pNode = curExpandNode ? curExpandNode.getParentNode():null;
var treeNodeP = treeNode.parentTId ? treeNode.getParentNode():null;
var zTree = $.fn.zTree.getZTreeObj(""+treeSettings.element);
for(var i=0, l=!treeNodeP ? 0:treeNodeP.children.length; i<l; i++ ) {
if (treeNode !== treeNodeP.children[i]) {
zTree.expandNode(treeNodeP.children[i], false);
}
}
while (pNode) {
if (pNode === treeNode) {
break;
}
pNode = pNode.getParentNode();
}
if (!pNode) {
singlePath(treeNode);
}
};
function onLoadSuccessed(){
var zTree = $.fn.zTree.getZTreeObj("orgsTree");
var orgsIdValues = $("#orgIdsList").val().split(",") ;
var orgsName="";
for (var i=0; i<orgsIdValues.length; i++) {
var node = zTree.getNodeByParam("id",orgsIdValues[i] );
if(node != null){
zTree.checkNode(node, true, false);//将指定ID的节点选中
orgsName += node.name;
}
}
$("#orgIdsListName").val(orgsName);
}
$.fn.zTree.init(
$("#"+treeSettings.element), //element
{//json object
check : {
enable : treeSettings.checkbox
},
async : {
enable : true,
url : treeSettings.url,
autoParam : ["id", "name=n", "level=lv"],
otherParam : {"otherParam":"zTreeAsyncTest",id:treeSettings.rootId},
dataFilter : function (treeId, parentNode, childNodes) {
if (!childNodes) return null;
for (var i=0, l=childNodes.length; i<l; i++) {
childNodes[i].name = childNodes[i].name.replace(/\.n/g, '.');
}
return childNodes;
}
},
data : {
simpleData : {
enable : true
}
},
callback: {
onClick : treeSettings.onClick,
onDblClick : treeSettings.onDblClick,
beforeAsync : function(treeId, treeNode){
$.loading();
},
onAsyncSuccess : function(event, treeId, treeNode, msg){
$.unloading();
onLoadSuccessed();
},
//beforeExpand : beforeExpand,
onExpand : function onExpand(event, treeId, treeNode) {
curExpandNode = treeNode;
}
}
}
);//end tree
});
function onBodyDown(event) {
if (!(event.target.id == "menuBtn" || event.target.id == "orgIdsListName" || event.target.id == "orgContent" || $(event.target).parents("#orgContent").length>0)) {
$("#orgContent").fadeOut("fast");
$("body").unbind("mousedown", onBodyDown);
}
}
function showOrgsTree() {
var treeObj = $("#orgIdsListName");
var treeOffset = $("#orgIdsListName").offset();
$("#orgContent").css({left:treeOffset.left + "px", top:treeOffset.top + treeObj.outerHeight() + "px"}).slideDown("fast");
$("body").bind("mousedown", onBodyDown);
}
</script>
</head>
<body>
<form id="actionForm" method="post" type="label" autoclose="true" action="<@base/>/roles/update" class="needs-validation" novalidate>
@ -26,10 +189,32 @@
<input type="text" id="name" name="name" class="form-control" title="" value="${model.name!}" required="" />
</td>
</tr>
<tr>
<th><@locale code="role.dynamic" /></th>
<td nowrap>
<select id="dynamic" name="dynamic" class="form-control">
<option value="0" <#if '0'==model.dynamic>selected</#if> ><@locale code="common.text.no" /></option>
<option value="1" <#if '1'==model.dynamic>selected</#if> ><@locale code="common.text.yes" /></option>
</select>
</td>
</tr>
<tr>
<th><@locale code="role.orgidslist" /></th>
<td nowrap>
<input type="text" id="orgIdsListName" name="orgIdsListName" readonly class="form-control" title="" value="" onclick="showOrgsTree();"/>
<input type="hidden" id="orgIdsList" name="orgIdsList" readonly class="form-control" title="" value="${model.orgIdsList!}" />
</td>
</tr>
<tr>
<th><@locale code="role.filters" /></th>
<td nowrap>
<textarea id="filters" name="filters" class="form-control" rows="7" cols="20">${model.filters!}</textarea>
</td>
</tr>
<tr>
<th><@locale code="common.text.description" /></th>
<td nowrap>
<input type="text" id="description" name="description" class="form-control" title="" value="${model.description!}" />
<textarea id="description" name="description" class="form-control" rows="6" cols="20">${model.description!}</textarea>
</td>
</tr>
<tr>
@ -43,5 +228,8 @@
</tbody>
</table>
</form>
<div id="orgContent" class="menuContent" style="display:none; position: absolute;">
<ul id="orgsTree" class="ztree" style="margin-top:0; width:180px; height: 300px;"></ul>
</div>
</body>
</html>

View File

@ -3,7 +3,11 @@
<head>
<#include "../layout/header.ftl"/>
<#include "../layout/common.cssjs.ftl"/>
<script type="text/javascript">
function dynamicFormatter(value, row, index){
return value=='0'? '<@locale code="common.text.no" />':'<@locale code="common.text.yes" />';
};
</script>
</head>
<body>
<div class="app header-default side-nav-dark">
@ -32,6 +36,7 @@
</div>
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -52,13 +57,13 @@
<input class="button btn btn-success mr-3" id="addBtn" type="button" value="<@locale code="button.text.add"/>"
wurl="<@base/>/roles/forwardAdd"
wwidth="500"
wheight="200"
wheight="600"
target="window">
<input class="button btn btn-info mr-3 " id="modifyBtn" type="button" value="<@locale code="button.text.edit"/>"
wurl="<@base/>/roles/forwardUpdate"
wwidth="500"
wheight="200"
wheight="600"
target="window">
<input class="button btn btn-danger mr-3 " id="deleteBtn" type="button" value="<@locale code="button.text.delete"/>"
@ -94,11 +99,12 @@
<th data-checkbox="true"></th>
<th data-sortable="true" data-field="id" data-visible="false">Id</th>
<th data-field="name"><@locale code="role.name"/></th>
<th data-field="dynamic" data-formatter="dynamicFormatter"><@locale code="group.dynamic"/></th>
<th data-field="description"><@locale code="common.text.description"/></th>
<th data-field="createdBy"><@locale code="common.text.createdby"/></th>
<th data-field="createdDate"><@locale code="common.text.createddate"/></th>
<th data-field="modifiedBy"><@locale code="common.text.modifiedby"/></th>
<th data-field="modifiedDate"><@locale code="common.text.modifieddate"/></th>
<th data-field="createdBy" data-visible="false"><@locale code="common.text.createdby"/></th>
<th data-field="createdDate" data-visible="false"><@locale code="common.text.createddate"/></th>
<th data-field="modifiedBy" data-visible="false"><@locale code="common.text.modifiedby"/></th>
<th data-field="modifiedDate" data-visible="false"><@locale code="common.text.modifieddate"/></th>
</tr>
</thead>
@ -107,10 +113,10 @@
</div>
</div>
</div>
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>
</div>
</div>

View File

@ -54,7 +54,7 @@
<td width="120px"><@locale code="userinfo.username"/>:</td>
<td width="374px">
<form id="basic_search_form">
<input class="form-control" type="text" name="name" style ="width:150px;float:left;">
<input class="form-control" type="text" name="username" style ="width:150px;float:left;">
<input class="button btn btn-primary mr-3" id="searchBtn" type="button" size="50" value="<@locale code="button.text.search"/>">
</form>
</td>

View File

@ -57,6 +57,7 @@
</div>
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -146,10 +147,11 @@
</div>
</div>
</div>
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>
</div>
</div>
</div>

View File

@ -177,7 +177,7 @@ $(function () {
</div>
<div class="container-fluid">
<div class="content-wrapper row">
<div class="col-12 grid-margin">
<div class="card">
<div class="card-body">
@ -292,6 +292,7 @@ $(function () {
</div>
</div>
</div>
<footer class="content-footer">
<#include "../layout/footer.ftl"/>
</footer>

View File

@ -131,7 +131,7 @@
<#--on captcha image click ,new a captcha code-->
<#if true==isCaptcha>
$('#j_captchaimg').click(function () {//
$(this).attr("src", "<@base />/captcha"+(new Date()).getTime());
$(this).attr("src", "<@base />/captcha?"+(new Date()).getTime());
});
</#if>

949
sql/maxkey_v2.4.0.RC2.sql Normal file

File diff suppressed because one or more lines are too long