Denmaseno 9 年 前
コミット
244ccdff35
27 ファイル変更1430 行追加0 行削除
  1. 4 0
      .gitignore
  2. 36 0
      ngdemo-app/.classpath
  3. 1 0
      ngdemo-app/.gitignore
  4. 23 0
      ngdemo-app/.project
  5. 196 0
      ngdemo-app/pom.xml
  6. 97 0
      ngdemo-app/src/main/java/id/co/hanoman/ngdemo/Application.java
  7. 145 0
      ngdemo-app/src/main/java/id/co/hanoman/ngdemo/AuthenticationServiceImpl.java
  8. 19 0
      ngdemo-app/src/main/java/id/co/hanoman/ngdemo/BasicController.java
  9. 43 0
      ngdemo-app/src/main/java/id/co/hanoman/ngdemo/InitData.java
  10. 151 0
      ngdemo-app/src/main/java/id/co/hanoman/ngdemo/RateLimiterServiceImpl.java
  11. 84 0
      ngdemo-app/src/main/java/id/co/hanoman/ngdemo/TokenServiceImpl.java
  12. 51 0
      ngdemo-app/src/main/java/id/co/hanoman/ngdemo/UserServiceImpl.java
  13. 59 0
      ngdemo-app/src/main/resources/application.yaml
  14. 4 0
      ngdemo-app/src/main/resources/data-init/100-role.csv
  15. 5 0
      ngdemo-app/src/main/resources/data-init/101-user.csv
  16. 33 0
      ngdemo-app/src/main/resources/logback-spring.xml
  17. 108 0
      ngdemo-app/src/main/resources/static/xswagger-ui.html
  18. 12 0
      ngdemo-app/src/main/resources/templates/index.ftl
  19. 36 0
      ngdemo-domain/.classpath
  20. 1 0
      ngdemo-domain/.gitignore
  21. 23 0
      ngdemo-domain/.project
  22. 81 0
      ngdemo-domain/pom.xml
  23. 45 0
      ngdemo-domain/src/main/java/id/co/hanoman/ngdemo/domain/AppRole.java
  24. 7 0
      ngdemo-domain/src/main/java/id/co/hanoman/ngdemo/domain/AppRoleRepository.java
  25. 113 0
      ngdemo-domain/src/main/java/id/co/hanoman/ngdemo/domain/AppUser.java
  26. 7 0
      ngdemo-domain/src/main/java/id/co/hanoman/ngdemo/domain/AppUserRepository.java
  27. 46 0
      readme.md

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.recommenders
+RemoteSystemsTempFiles
+*/.settings
+*/target

+ 36 - 0
ngdemo-app/.classpath

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" output="target/classes" path="src/main/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" output="target/test-classes" path="src/test/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>

+ 1 - 0
ngdemo-app/.gitignore

@@ -0,0 +1 @@
+/target/

+ 23 - 0
ngdemo-app/.project

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>ngdemo-app</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.m2e.core.maven2Builder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.m2e.core.maven2Nature</nature>
+	</natures>
+</projectDescription>

+ 196 - 0
ngdemo-app/pom.xml

@@ -0,0 +1,196 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>id.co.hanoman.ngdemo</groupId>
+	<artifactId>ngdemo-app</artifactId>
+	<version>1.0.0</version>
+
+	<parent>
+		<groupId>com.senomas.boot</groupId>
+		<artifactId>senomas-boot</artifactId>
+		<version>1.0.2</version>
+	</parent>
+
+	<dependencies>
+		<dependency>
+			<groupId>id.co.hanoman.ngdemo</groupId>
+			<artifactId>ngdemo-domain</artifactId>
+			<version>1.0.0</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.senomas.boot</groupId>
+			<artifactId>senomas-boot-security</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.senomas.boot</groupId>
+			<artifactId>senomas-boot-util</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-security</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-freemarker</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.fasterxml.jackson.dataformat</groupId>
+			<artifactId>jackson-dataformat-yaml</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.fasterxml.jackson.datatype</groupId>
+			<artifactId>jackson-datatype-joda</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-swagger2</artifactId>
+			<version>${springfox.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-swagger-ui</artifactId>
+			<version>${springfox.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-jpa</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-mongodb</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-redis</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>mysql</groupId>
+			<artifactId>mysql-connector-java</artifactId>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<defaultGoal>install</defaultGoal>
+
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-antrun-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>clean dependency</id>
+						<phase>clean</phase>
+						<goals>
+							<goal>run</goal>
+						</goals>
+						<configuration>
+							<target>
+								<exec executable="/usr/local/bin/mvn" osfamily="mac"
+									failonerror="true">
+									<arg value="-f" />
+									<arg value="../ngdemo-domain/" />
+									<arg value="clean" />
+								</exec>
+								<exec executable="cmd" osfamily="windows" failonerror="true">
+									<arg value="\c" />
+									<arg value="mvn" />
+									<arg value="-f" />
+									<arg value="../ngdemo-domain/" />
+									<arg value="clean" />
+								</exec>
+							</target>
+						</configuration>
+					</execution>
+					<execution>
+						<id>install dependency</id>
+						<phase>generate-sources</phase>
+						<goals>
+							<goal>run</goal>
+						</goals>
+						<configuration>
+							<target>
+								<exec executable="/usr/local/bin/mvn" osfamily="mac"
+									failonerror="true">
+									<arg value="-f" />
+									<arg value="../ngdemo-domain/" />
+									<arg value="install" />
+								</exec>
+								<exec executable="cmd" osfamily="windows" failonerror="true">
+									<arg value="\c" />
+									<arg value="mvn" />
+									<arg value="-f" />
+									<arg value="../ngdemo-domain/" />
+									<arg value="install" />
+								</exec>
+							</target>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.8</source>
+					<target>1.8</target>
+				</configuration>
+			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-resources-plugin</artifactId>
+				<configuration>
+					<encoding>UTF-8</encoding>
+				</configuration>
+			</plugin>
+
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+				<executions>
+					<execution>
+						<goals>
+							<goal>repackage</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+
+	<repositories>
+		<repository>
+			<id>senomas</id>
+			<url>http://maven.senomas.com/content/repositories/releases/</url>
+			<releases>
+				<enabled>true</enabled>
+			</releases>
+		</repository>
+		<repository>
+			<id>senomas-snapshots</id>
+			<url>http://maven.senomas.com/content/repositories/snapshots/</url>
+			<snapshots>
+				<enabled>true</enabled>
+				<updatePolicy>always</updatePolicy>
+			</snapshots>
+		</repository>
+	</repositories>
+</project>

+ 97 - 0
ngdemo-app/src/main/java/id/co/hanoman/ngdemo/Application.java

@@ -0,0 +1,97 @@
+package id.co.hanoman.ngdemo;
+
+import java.util.Arrays;
+
+import org.joda.time.LocalDate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.orm.jpa.EntityScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.http.ResponseEntity;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import com.fasterxml.classmate.TypeResolver;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.senomas.boot.security.LoginUser;
+import com.senomas.common.RateLimit;
+
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.builders.ResponseMessageBuilder;
+import springfox.documentation.schema.AlternateTypeRules;
+import springfox.documentation.schema.ModelRef;
+import springfox.documentation.schema.WildcardType;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+@SpringBootApplication
+@EnableSwagger2
+@ComponentScan({ "id.co.hanoman.ngdemo", "com.senomas.boot", "com.senomas.common.loggerfilter",
+		"com.senomas.common.logback" })
+@EntityScan({ "id.co.hanoman.ngdemo" })
+@EnableJpaRepositories({ "id.co.hanoman.ngdemo", "com.senomas.boot" })
+@EnableMongoRepositories({ "com.senomas.common.logback" })
+@EnableAsync
+public class Application {
+
+	@Autowired
+	TypeResolver typeResolver;
+
+	@Autowired
+	JedisConnectionFactory jedisConnFactory;
+
+	@Bean
+	public Docket getApi() {
+		// @formatter:off
+		return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any()).paths(paths())
+				.build().pathMapping("/").directModelSubstitute(LocalDate.class, String.class)
+				.genericModelSubstitutes(ResponseEntity.class)
+				.alternateTypeRules(AlternateTypeRules.newRule(
+						typeResolver.resolve(DeferredResult.class,
+								typeResolver.resolve(ResponseEntity.class, WildcardType.class)),
+						typeResolver.resolve(WildcardType.class)))
+				.useDefaultResponseMessages(false)
+				.globalResponseMessage(RequestMethod.GET, Arrays.asList(new ResponseMessageBuilder().code(500)
+						.message("500 message").responseModel(new ModelRef("Error")).build()));
+		// @formatter:on
+	}
+
+	@Bean
+	public RedisTemplate<String, LoginUser> loginUserRedisTemplate() {
+		RedisTemplate<String, LoginUser> users = new RedisTemplate<>();
+		users.setConnectionFactory(jedisConnFactory);
+		users.setKeySerializer(new StringRedisSerializer());
+		users.setValueSerializer(new Jackson2JsonRedisSerializer<>(LoginUser.class));
+		return users;
+	}
+
+	@Bean
+	public RedisTemplate<String, RateLimit> rateLimitRedisTemplate() {
+		RedisTemplate<String, RateLimit> rates = new RedisTemplate<>();
+		rates.setConnectionFactory(jedisConnFactory);
+		rates.setKeySerializer(new StringRedisSerializer());
+		rates.setValueSerializer(new Jackson2JsonRedisSerializer<>(RateLimit.class));
+		return rates;
+	}
+
+	private Predicate<String> paths() {
+		return Predicates.or(PathSelectors.regex("/s/.*"), PathSelectors.regex("/api/.*"));
+	}
+
+	public static void main(String[] args) {
+		SpringApplication.run(Application.class, args);
+	}
+
+}

+ 145 - 0
ngdemo-app/src/main/java/id/co/hanoman/ngdemo/AuthenticationServiceImpl.java

@@ -0,0 +1,145 @@
+package id.co.hanoman.ngdemo;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import com.senomas.boot.security.LoginRequest;
+import com.senomas.boot.security.LoginUser;
+import com.senomas.boot.security.RefreshTokenRequest;
+import com.senomas.boot.security.TokenAuthentication;
+import com.senomas.boot.security.domain.AuthToken;
+import com.senomas.boot.security.domain.SecurityUser;
+import com.senomas.boot.security.rs.AuthenticationException;
+import com.senomas.boot.security.rs.InvalidRefreshTokenException;
+import com.senomas.boot.security.rs.InvalidSaltException;
+import com.senomas.boot.security.rs.InvalidUserPasswordException;
+import com.senomas.boot.security.service.AuthenticationService;
+import com.senomas.boot.security.service.TokenService;
+import com.senomas.common.RateLimiterService;
+import com.senomas.common.U;
+import com.senomas.common.rs.InvalidTimestampException;
+
+import id.co.hanoman.ngdemo.domain.AppUser;
+import id.co.hanoman.ngdemo.domain.AppUserRepository;
+
+@Component
+public class AuthenticationServiceImpl implements AuthenticationService {
+	private static final Logger log = LoggerFactory.getLogger(AuthenticationServiceImpl.class);
+
+	@Autowired
+	protected AppUserRepository userRepo;
+	
+	@Autowired
+	protected RateLimiterService rateLimiterService;
+	
+	@Autowired
+	protected TokenService tokenService;
+	
+	@Override
+	public AuthToken login(HttpServletRequest request, LoginRequest login) {
+		if (Math.abs(System.currentTimeMillis() - login.getTimestamp().getTime()) > 300000) {
+			log.info("TIMESTAMP " + System.currentTimeMillis() + "  "
+					+ (System.currentTimeMillis() - login.getTimestamp().getTime()));
+			throw new InvalidTimestampException();
+		}
+
+		if (!rateLimiterService.acquire("/ip/auth", request.getRemoteAddr())) {
+			throw new AuthenticationException("Too many request");
+		}
+		if (!rateLimiterService.acquire("/auth", login.getLogin())) {
+			throw new AuthenticationException("Too many request");
+		}
+
+		if (login.getSalt() == null)
+			throw new InvalidSaltException(login.getTimestamp().getTime(), generateSalt(request, login),
+					"Invalid salt");
+		String salt = generateSalt(request, login);
+		if (!salt.equals(login.getSalt()))
+			throw new InvalidSaltException(login.getTimestamp().getTime(), generateSalt(request, login),
+					"Invalid salt");
+
+		AppUser user = userRepo.findOne(login.getLogin());
+
+		if (user == null) {
+			log.info("INVALID LOGIN [" + login.getLogin() + "]");
+			throw new InvalidUserPasswordException();
+		}
+
+		String securePasscode = U.digest(user.getPasscode(), "|", login.getSalt());
+
+		if (!securePasscode.equals(login.getSecurePasscode())) {
+			log.info("INVALID TOKEN ["+user.getPasscode()+"]  [" + securePasscode + "]  [" + login.getSecurePasscode() + "]");
+			throw new InvalidUserPasswordException();
+		}
+
+		LoginUser loginUser = tokenService.create(user);
+		log.info("LOGIN-USER "+U.dump(loginUser));
+		String token = loginUser.getToken();
+
+		String refreshToken = UUID.randomUUID().toString();
+		user.setLoginToken(refreshToken);
+		userRepo.save(user);
+
+		return new AuthToken(token, refreshToken, user);
+	}
+
+	protected String generateSalt(HttpServletRequest request, LoginRequest login) {
+		try {
+			MessageDigest md = MessageDigest.getInstance("SHA-256");
+			md.update(U.getBytes(login.getLogin()));
+			md.update(U.getBytes(request.getRemoteAddr()));
+			md.update(U.getBytes(login.getTimestamp().getTime()));
+			return U.encode64(md.digest());
+		} catch (NoSuchAlgorithmException e) {
+			throw new RuntimeException(e.getMessage(), e);
+		}
+	}
+
+	@Override
+	public SecurityUser getUser() {
+		AppUser user = (AppUser) ((TokenAuthentication) SecurityContextHolder.getContext().getAuthentication()).getUser();
+		return user;
+	}
+	
+	@Override
+	public AuthToken refresh(HttpServletRequest request, RefreshTokenRequest refreshToken) {
+		if (Math.abs(System.currentTimeMillis() - refreshToken.getTimestamp().getTime()) > 300000) {
+			log.info("TIMESTAMP " + System.currentTimeMillis() + "  "
+					+ (System.currentTimeMillis() - refreshToken.getTimestamp().getTime()));
+			throw new InvalidTimestampException();
+		}
+
+		if (!rateLimiterService.acquire("/ip/auth", request.getRemoteAddr())) {
+			throw new AuthenticationException("Too many request");
+		}
+		if (!rateLimiterService.acquire("/auth", refreshToken.getLogin())) {
+			throw new AuthenticationException("Too many request");
+		}
+		
+		AppUser user = userRepo.findOne(refreshToken.getLogin());
+
+		if (user == null) {
+			log.info("INVALID LOGIN [" + refreshToken.getLogin() + "]");
+			throw new InvalidRefreshTokenException("Invalid login");
+		}
+		
+		if (!U.digest(String.valueOf(refreshToken.getTimestamp().getTime()), "|", user.getLogin(), "|", user.getLoginToken()).equals(refreshToken.getRefreshTokenHash())) {
+			log.info("INVALID REFRESH-TOKEN " + U.dump(refreshToken));
+			throw new InvalidRefreshTokenException("Invalid refresh-token");
+		}
+
+		LoginUser loginUser = tokenService.create(user);
+
+		return new AuthToken(loginUser.getToken(), null, user);
+	}
+
+}

+ 19 - 0
ngdemo-app/src/main/java/id/co/hanoman/ngdemo/BasicController.java

@@ -0,0 +1,19 @@
+package id.co.hanoman.ngdemo;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+public class BasicController {
+
+	@RequestMapping("/")
+	public String homePage(Model model) {
+		return "index";
+	}
+
+	@RequestMapping("/swagger-ui.html")
+	public String swaggerUI(Model model) {
+		return "xswagger-ui.html";
+	}
+}

+ 43 - 0
ngdemo-app/src/main/java/id/co/hanoman/ngdemo/InitData.java

@@ -0,0 +1,43 @@
+package id.co.hanoman.ngdemo;
+
+import javax.annotation.PostConstruct;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.stereotype.Component;
+
+import com.senomas.common.FileDataLoader;
+import com.senomas.common.U;
+
+@Component
+@Profile("init-data")
+public class InitData extends FileDataLoader {
+	private final static Logger log = LoggerFactory.getLogger(InitData.class);
+	
+	@Autowired
+	MongoTemplate mongoTemplate;
+	
+	@PostConstruct
+	protected void populate() throws Exception {
+		log.info("Populate data");
+		loadData(getClass().getClassLoader().getResource("data-init").openConnection());
+	}
+	
+	@Override
+	protected void save(Object obj) {
+		log.info("SAVE "+U.dump(obj));
+		mongoTemplate.save(obj);
+	}
+	
+	@Override
+	protected Object find(Class<?> cl, String key, Object data) {
+		Query qry = new Query();
+		qry.addCriteria(Criteria.where(key).is(data));
+		return mongoTemplate.findOne(qry, cl);
+	}
+}

+ 151 - 0
ngdemo-app/src/main/java/id/co/hanoman/ngdemo/RateLimiterServiceImpl.java

@@ -0,0 +1,151 @@
+package id.co.hanoman.ngdemo;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PostConstruct;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import com.senomas.common.RateLimit;
+import com.senomas.common.RateLimiterService;
+import com.senomas.common.SenomasConfiguration;
+import com.senomas.common.U;
+
+@Component
+public class RateLimiterServiceImpl implements RateLimiterService {
+	private static final Logger log = LoggerFactory.getLogger(RateLimiterServiceImpl.class);
+
+	@Autowired
+	private RedisTemplate<String, RateLimit> loginLimits;
+
+	@Autowired
+	SenomasConfiguration config;
+
+	Map<String, RateLimitConfigs> configs = new LinkedHashMap<>();
+
+	@PostConstruct
+	public void init() {
+		log.info("CONFIG " + U.dump(config));
+		if (config.getRateLimiter() != null) {
+			int gcd = 0;
+			int maxLength = 0;
+			for (Map.Entry<String, String> cfg : config.getRateLimiter().entrySet()) {
+				List<RateLimitConfig> rlc = new LinkedList<>();
+				for (String sx : cfg.getValue().split("\\s+")) {
+					String vx[] = sx.split("/");
+					if (vx.length == 2) {
+						int limit = Integer.parseInt(vx[0]);
+						int length = Integer.parseInt(vx[1]);
+						maxLength = Math.max(maxLength, length);
+						gcd = gcd == 0 ? length : RateLimiterServiceImpl.gcd(gcd, length);
+						rlc.add(new RateLimitConfig(limit, length));
+					}
+				}
+				configs.put(cfg.getKey(), new RateLimitConfigs(gcd, maxLength, rlc));
+			}
+		}
+		log.info("CFG "+U.dump(configs));
+	}
+
+	@Override
+	public boolean acquire(String url) {
+		return acquire(url, SecurityContextHolder.getContext().getAuthentication().getName());
+	}
+
+	@Override
+	public boolean acquire(String url, String id) {
+		if (log.isTraceEnabled())
+			log.trace("acquire([url:" + url + "], [id:" + id + "])");
+
+		RateLimitConfigs rlc = configs.get(url);
+		log.info("RATE-LIMIT ["+url+"] "+U.dump(rlc));
+		if (rlc != null) {
+			ValueOperations<String, RateLimit> rlv = loginLimits.opsForValue();
+			String uid = url + "?" + id;
+			RateLimit rl = rlv.get(uid);
+			try {
+				if (rl == null) {
+					log.info("CREATE RATE-LIMIT ["+url+"] "+rlc.getGcd()+"  "+rlc.getLength()+"  "+U.dump(rlc.getLimits()));
+					rl = new RateLimit(rlc.getGcd(), rlc.getLength());
+				}
+				rl.add(1);
+				if (rl.isOverlimit()) {
+					return false;
+				}
+				for (RateLimitConfig limit : rlc.getLimits()) {
+					if (rl.get(limit.getLength()) > limit.getLimit()) {
+						rl.setOverlimit(true);
+						return false;
+					}
+				}
+			} finally {
+				if (rl != null) {
+					rlv.set(uid, rl, rlc.getLength(), TimeUnit.MILLISECONDS);
+				}
+			}
+		}
+		return true;
+	}
+
+	static class RateLimitConfig {
+		final int limit;
+		final int length;
+
+		public RateLimitConfig(int limit, int length) {
+			this.limit = limit;
+			this.length = length;
+		}
+
+		public int getLimit() {
+			return limit;
+		}
+
+		public int getLength() {
+			return length;
+		}
+	}
+
+	static class RateLimitConfigs {
+		final int gcd;
+		final int length;
+		final List<RateLimitConfig> limits;
+
+		public RateLimitConfigs(int gcd, int length, List<RateLimitConfig> limits) {
+			this.gcd = gcd;
+			this.length = length;
+			this.limits = limits;
+		}
+
+		public int getGcd() {
+			return gcd;
+		}
+		
+		public int getLength() {
+			return length;
+		}
+
+		public List<RateLimitConfig> getLimits() {
+			return limits;
+		}
+	}
+
+	public static int gcd(int a, int b) {
+		while (a != 0 && b != 0) {
+			int c = b;
+			b = a % b;
+			a = c;
+		}
+		return a + b;
+	}
+
+}

+ 84 - 0
ngdemo-app/src/main/java/id/co/hanoman/ngdemo/TokenServiceImpl.java

@@ -0,0 +1,84 @@
+package id.co.hanoman.ngdemo;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import com.senomas.boot.security.LoginUser;
+import com.senomas.boot.security.SecurityConfiguration;
+import com.senomas.boot.security.domain.SecurityUser;
+import com.senomas.boot.security.service.TokenService;
+import com.senomas.boot.security.service.UserService;
+
+@Component
+@EnableConfigurationProperties(SecurityConfiguration.class)
+public class TokenServiceImpl implements TokenService {
+	private static final Logger log = LoggerFactory.getLogger(TokenServiceImpl.class);
+
+	@Autowired
+	SecurityConfiguration config;
+
+	@Autowired
+	UserService userService;
+
+	@Autowired
+	RedisTemplate<String, LoginUser> users;
+
+	@Autowired
+	StringRedisTemplate tokens;
+
+	public TokenServiceImpl() {
+		log.debug("init");
+	}
+
+	@Override
+	public LoginUser get(String token) {
+		return users.opsForValue().get(token);
+	}
+
+	@Override
+	public LoginUser create(SecurityUser user) {
+		String token = UUID.randomUUID().toString();
+		for (int i = 0; i < 1000 && users.hasKey(token); i++) {
+			try {
+				Thread.sleep(10);
+			} catch (Exception e) {
+			}
+			token = UUID.randomUUID().toString();
+		}
+		if (users.hasKey(token))
+			throw new RuntimeException("Too many tokens");
+		LoginUser loginUser = new LoginUser(token, user, userService.getRoles(user));
+		String oldToken = tokens.opsForValue().get(loginUser.getUser().getLogin());
+		if (oldToken != null) {
+			if (log.isTraceEnabled())
+				log.trace("Remove old token " + oldToken + " new " + token);
+			users.delete(oldToken);
+		}
+		users.opsForValue().set(token, loginUser, config.getTokenExpiry(), TimeUnit.SECONDS);
+		tokens.opsForValue().set(loginUser.getUser().getLogin(), token);
+		return loginUser;
+	}
+
+	@Override
+	public boolean hasSalt(String salt) {
+		return true;
+	}
+
+	@Override
+	public String getSalt(String salt) {
+		return null;
+	}
+
+	@Override
+	public void setSalt(String salt, String value) {
+	}
+
+}

+ 51 - 0
ngdemo-app/src/main/java/id/co/hanoman/ngdemo/UserServiceImpl.java

@@ -0,0 +1,51 @@
+package id.co.hanoman.ngdemo;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.senomas.boot.security.domain.SecurityRole;
+import com.senomas.boot.security.domain.SecurityUser;
+import com.senomas.boot.security.service.UserService;
+
+import id.co.hanoman.ngdemo.domain.AppRole;
+import id.co.hanoman.ngdemo.domain.AppUser;
+import id.co.hanoman.ngdemo.domain.AppUserRepository;
+
+@Component
+public class UserServiceImpl implements UserService {
+	private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
+	
+	@Autowired
+	AppUserRepository userRepo;
+	
+	@Override
+	public SecurityUser findByLogin(String login) {
+		if (log.isDebugEnabled()) log.debug("findByLogin("+login+")");
+		try {
+			return userRepo.findOne(login);
+		} catch (RuntimeException e) {
+			log.error("ERROR "+e.getMessage(), e);
+			return null;
+		}
+	}
+
+	@Override
+	public List<SecurityRole> getRoles(SecurityUser user) {
+		List<SecurityRole> roles = new LinkedList<>();
+		for (AppRole r : ((AppUser) user).getRoles()) {
+			roles.add(r);
+		}
+		return roles;
+	}
+
+	@Override
+	public SecurityUser save(SecurityUser user) {
+		return userRepo.save((AppUser) user);
+	}
+
+}

+ 59 - 0
ngdemo-app/src/main/resources/application.yaml

@@ -0,0 +1,59 @@
+spring:
+   jackson:
+      time-zone: GMT+7
+      serialization.write-dates-as-timestamps: false
+
+   redis:
+      host: ${REDIS:docker}
+      port: 6379
+
+   jpa:
+      show-sql: true
+      hibernate.ddl-auto: update
+
+   datasource:
+      url: jdbc:mysql://${MYSQL:docker:3306}/demo
+      username: demo
+      password: demo
+
+   data:
+      mysql:
+         uri: mysql://${MYSQL_URI:docker:3306/demo}
+      mongodb:
+         uri: mongodb://${MONGO_URI:demo:demo@docker/admin}
+
+   main:
+      sources: id.co.hanoman.ngdemo
+
+rest:
+   version: 1
+
+   uri: /api/v${rest.version}
+
+com.senomas:
+   common.logback.nosql.collection: logs
+
+   http-logger:
+      order: 0
+      path:
+         "/": BASIC
+         "/v2/api-docs": OFF
+         "/swagger-": OFF
+         "/configuration/": OFF
+         "/api/": ALL
+
+   security:
+      token-expiry: 900
+
+   rate-limiter:
+      "/ip/auth": "2/1000 10/60000"
+      "/auth": "2/1000 10/60000"
+      "/service": "100/1000 200/60000"
+
+logging:
+   level:
+      '*': DEBUG
+      org.hibernate.SQL: DEBUG
+      com.senomas: DEBUG
+      com.senomas.boot.security: DEBUG
+      id.co.hanoman.echannel.service.impl.RateLimiterServiceImpl: TRACE

+ 4 - 0
ngdemo-app/src/main/resources/data-init/100-role.csv

@@ -0,0 +1,4 @@
+id.co.hanoman.ngdemo.domain.AppRole
+*code,name,description
+user,User,Common User
+admin,Administrator,Administrator

+ 5 - 0
ngdemo-app/src/main/resources/data-init/101-user.csv

@@ -0,0 +1,5 @@
+id.co.hanoman.ngdemo.domain.AppUser
+*login,name,plainPasscode,accounts,destinationAccounts
+adi,Adi Adi,dodol123,[{"@type": ".SavingSourceAccount","accountNo": "10000001","name": "Adi Adi"}]
+badu,Badu,dodol123,[{"@type": ".SavingSourceAccount","accountNo": "10000002","name": "Badu"}]
+cici,Cici,dodol123,[{"@type": ".SavingSourceAccount","accountNo": "10000003","name": "Cici"}]

+ 33 - 0
ngdemo-app/src/main/resources/logback-spring.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+	<include resource="org/springframework/boot/logging/logback/defaults.xml" />
+	<property name="LOG_FILE"
+		value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}" />
+	<include
+		resource="org/springframework/boot/logging/logback/console-appender.xml" />
+	<appender name="FILE"
+		class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<level>ERROR</level>
+		</filter>
+		<encoder>
+			<pattern>${FILE_LOG_PATTERN}</pattern>
+		</encoder>
+		<file>${LOG_FILE}</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
+			<fileNamePattern>${LOG_FILE}.%i</fileNamePattern>
+		</rollingPolicy>
+		<triggeringPolicy
+			class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+			<MaxFileSize>10MB</MaxFileSize>
+		</triggeringPolicy>
+	</appender>
+	<appender name="NOSQL" class="com.senomas.common.logback.NoSQLAppender">
+		<uri>mongodb://demo:demo@docker/admin.logs</uri>
+	</appender>
+	<root level="INFO">
+		<appender-ref ref="CONSOLE" />
+		<appender-ref ref="NOSQL" />
+		<appender-ref ref="FILE" />
+	</root>
+</configuration>

+ 108 - 0
ngdemo-app/src/main/resources/static/xswagger-ui.html

@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Swagger UI (2)</title>
+    <link rel="icon" type="image/png" href="images/favicon-32x32.png" sizes="32x32" />
+    <link rel="icon" type="image/png" href="images/favicon-16x16.png" sizes="16x16" />
+    <link href='webjars/springfox-swagger-ui/css/typography.css' media='screen' rel='stylesheet' type='text/css'/>
+    <link href='webjars/springfox-swagger-ui/css/reset.css' media='screen' rel='stylesheet' type='text/css'/>
+    <link href='webjars/springfox-swagger-ui/css/screen.css' media='screen' rel='stylesheet' type='text/css'/>
+    <link href='webjars/springfox-swagger-ui/css/reset.css' media='print' rel='stylesheet' type='text/css'/>
+    <link href='webjars/springfox-swagger-ui/css/screen.css' media='print' rel='stylesheet' type='text/css'/>
+    <script src='webjars/springfox-swagger-ui/lib/jquery-1.8.0.min.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/lib/jquery.slideto.min.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/lib/jquery.wiggle.min.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/lib/jquery.ba-bbq.min.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/lib/handlebars-2.0.0.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/lib/underscore-min.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/lib/backbone-min.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/swagger-ui.min.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/springfox.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/lib/highlight.7.3.pack.js' type='text/javascript'></script>
+    <script src='webjars/springfox-swagger-ui/lib/marked.js' type='text/javascript'></script>
+
+    <!-- enabling this will enable oauth2 implicit scope support -->
+    <script src='webjars/springfox-swagger-ui/lib/swagger-oauth.js' type='text/javascript'></script>
+    <script type="text/javascript">
+        $(function() {
+
+          window.swaggerUi = new SwaggerUi({
+            dom_id: "swagger-ui-container",
+            supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
+            onComplete: function(swaggerApi, swaggerUi) {
+
+              $('pre code').each(function(i, e) {
+                hljs.highlightBlock(e)
+              });
+
+            },
+            onFailure: function(data) {
+              log("Unable to Load SwaggerUI");
+            },
+            onComplete: function(data) {
+              initializeSpringfox();
+            },
+            docExpansion: "none",
+            apisSorter: "alpha"
+          });
+
+          function addApiKeyAuthorization() {
+            var key = encodeURIComponent($('#input_apiKey')[0].value);
+            log("key: " + key);
+            if (key && key.trim() != "") {
+              var apiKeyAuth = new SwaggerClient.ApiKeyAuthorization("authorization", "Bearer "+key, "header");
+              window.swaggerUi.api.clientAuthorizations.add("api_key", apiKeyAuth);
+              log("added key " + key);
+            }
+          }
+
+          $('#input_apiKey').change(addApiKeyAuthorization);
+
+          function log() {
+            if ('console' in window) {
+              console.log.apply(console, arguments);
+            }
+          }
+
+          function initializeSpringfox() {
+            var security = {};
+            window.springfox.securityConfig(function(data) {
+              security = data;
+              if (security.apiKey) {
+                $('#input_apiKey').val(security.apiKey);
+                addApiKeyAuthorization();
+              }
+              if (typeof initOAuth == "function") {
+                if (security.clientId && security.appName && security.realm) {
+                  initOAuth(security);
+                }
+              }
+            });
+
+            window.springfox.uiConfig(function(data) {
+              window.swaggerUi.validatorUrl = data.validatorUrl;
+            });
+          }
+        });
+    </script>
+</head>
+
+<body class="swagger-section">
+<div id='header'>
+    <div class="swagger-ui-wrap">
+        <a id="logo" href="http://swagger.io">swagger</a>
+        <form id='api_selector'>
+            <div class='input'>
+                <select id="select_baseUrl" name="select_baseUrl"/>
+            </div>
+            <div class='input'><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"/></div>
+            <div class='input'><input placeholder="api_key" id="input_apiKey" name="apiKey" type="text"/></div>
+            <div class='input'><a id="explore" href="#">Explore</a></div>
+        </form>
+    </div>
+</div>
+
+<div id="message-bar" class="swagger-ui-wrap">&nbsp;</div>
+<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
+</body>
+</html>

+ 12 - 0
ngdemo-app/src/main/resources/templates/index.ftl

@@ -0,0 +1,12 @@
+<html>
+<head>
+<meta charset="utf-8">
+<title>Autodebet</title>
+<meta name="viewport" content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width">
+<meta property="og:locale" content="en-us">
+<link rel="stylesheet" href="main.css">
+</head>
+<body style="background-color:#FF7C00">
+	Its Work!!!!
+</body>
+</html>

+ 36 - 0
ngdemo-domain/.classpath

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" output="target/classes" path="src/main/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" output="target/test-classes" path="src/test/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>

+ 1 - 0
ngdemo-domain/.gitignore

@@ -0,0 +1 @@
+/target/

+ 23 - 0
ngdemo-domain/.project

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>ngdemo-domain</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.m2e.core.maven2Builder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.m2e.core.maven2Nature</nature>
+	</natures>
+</projectDescription>

+ 81 - 0
ngdemo-domain/pom.xml

@@ -0,0 +1,81 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>id.co.hanoman.ngdemo</groupId>
+	<artifactId>ngdemo-domain</artifactId>
+	<version>1.0.0</version>
+
+	<parent>
+		<groupId>com.senomas.boot</groupId>
+		<artifactId>senomas-boot</artifactId>
+		<version>1.0.2</version>
+	</parent>
+
+	<dependencies>
+		<dependency>
+			<groupId>com.senomas.boot</groupId>
+			<artifactId>senomas-boot-security</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.senomas.boot</groupId>
+			<artifactId>senomas-boot-util</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-jpa</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-mongodb</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.mongodb</groupId>
+			<artifactId>mongo-java-driver</artifactId>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<defaultGoal>install</defaultGoal>
+
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.8</source>
+					<target>1.8</target>
+				</configuration>
+			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-resources-plugin</artifactId>
+				<configuration>
+					<encoding>UTF-8</encoding>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+	<repositories>
+		<repository>
+			<id>senomas</id>
+			<url>http://maven.senomas.com/content/repositories/releases/</url>
+			<releases>
+				<enabled>true</enabled>
+			</releases>
+		</repository>
+		<repository>
+			<id>senomas-snapshots</id>
+			<url>http://maven.senomas.com/content/repositories/snapshots/</url>
+			<snapshots>
+				<enabled>true</enabled>
+				<updatePolicy>always</updatePolicy>
+			</snapshots>
+		</repository>
+	</repositories>
+</project>

+ 45 - 0
ngdemo-domain/src/main/java/id/co/hanoman/ngdemo/domain/AppRole.java

@@ -0,0 +1,45 @@
+package id.co.hanoman.ngdemo.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+import com.senomas.boot.security.domain.SecurityRole;
+
+@Entity
+public class AppRole implements SecurityRole {
+
+	@Id
+	@Column(length=64)
+	private String code;
+	
+	@Column(nullable=false, unique=true, length=256)
+	private String name;
+	
+	@Column(length=1024)
+	private String description;
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+}

+ 7 - 0
ngdemo-domain/src/main/java/id/co/hanoman/ngdemo/domain/AppRoleRepository.java

@@ -0,0 +1,7 @@
+package id.co.hanoman.ngdemo.domain;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface AppRoleRepository extends CrudRepository<AppRole, String> {
+
+}

+ 113 - 0
ngdemo-domain/src/main/java/id/co/hanoman/ngdemo/domain/AppUser.java

@@ -0,0 +1,113 @@
+package id.co.hanoman.ngdemo.domain;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+
+import com.senomas.boot.security.domain.SecurityUser;
+
+@Entity
+public class AppUser implements SecurityUser {
+
+	@Id
+	@Column(length = 64)
+	private String login;
+
+	@Column(nullable = false, unique = true, length = 256)
+	private String fullName;
+
+	@Column(nullable = false, length = 256)
+	private String passcode;
+
+	@Column(nullable = true, length = 256)
+	private String loginToken;
+
+	@Column(nullable = true)
+	private Date lastLogin;
+
+	@ManyToMany(cascade = CascadeType.ALL)
+	@JoinTable(name = "app_user_role", joinColumns = @JoinColumn(name = "user", referencedColumnName = "login"), inverseJoinColumns = @JoinColumn(name = "role", referencedColumnName = "code"))
+	private List<AppRole> roles = new LinkedList<>();
+
+	public String getLogin() {
+		return login;
+	}
+
+	public void setLogin(String login) {
+		this.login = login;
+	}
+
+	public String getFullName() {
+		return fullName;
+	}
+
+	public void setFullName(String fullName) {
+		this.fullName = fullName;
+	}
+
+	public String getPasscode() {
+		return passcode;
+	}
+
+	public void setPasscode(String passcode) {
+		this.passcode = passcode;
+	}
+
+	public String getLoginToken() {
+		return loginToken;
+	}
+
+	public void setLoginToken(String loginToken) {
+		this.loginToken = loginToken;
+		this.lastLogin = new Date();
+	}
+
+	public Date getLastLogin() {
+		return lastLogin;
+	}
+
+	public Collection<AppRole> getRoles() {
+		return roles;
+	}
+
+	public void setRoles(Collection<AppRole> roles) {
+		this.roles.clear();
+		this.roles.addAll(roles);
+		sortRole();
+	}
+
+	public void addRole(AppRole role) {
+		this.roles.add(role);
+		sortRole();
+	}
+
+	public void removeRole(AppRole role) {
+		for (Iterator<AppRole> ir = roles.iterator(); ir.hasNext();) {
+			AppRole r = ir.next();
+			if (r.getCode().equals(role.getCode())) {
+				ir.remove();
+			}
+		}
+	}
+
+	protected void sortRole() {
+		Collections.sort(this.roles, new Comparator<AppRole>() {
+			@Override
+			public int compare(AppRole r1, AppRole r2) {
+				return r1.getCode().compareTo(r2.getCode());
+			}
+		});
+	}
+}

+ 7 - 0
ngdemo-domain/src/main/java/id/co/hanoman/ngdemo/domain/AppUserRepository.java

@@ -0,0 +1,7 @@
+package id.co.hanoman.ngdemo.domain;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface AppUserRepository extends CrudRepository<AppUser, String> {
+
+}

+ 46 - 0
readme.md

@@ -0,0 +1,46 @@
+
+
+```
+docker run --name ngdemo_mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=dodol123 \
+    -e MYSQL_DATABASE=demo -e MYSQL_USER=demo -e MYSQL_PASSWORD=demo mysql/mysql-server
+```
+
+
+```
+docker run --name ngdemo_mysql_admin -d --link ngdemo_mysql:db -p 8306:80 phpmyadmin/phpmyadmin
+```
+
+
+```
+docker run --name ngdemo_redis -p 6379:6379 -d redis
+```
+
+
+docker rm -f ngdemo_mongodb
+
+docker run --name ngdemo_mongodb -d -p 27017:27017 -p 28017:28017 mongo
+
+docker exec -it ngdemo_mongodb mongo admin --eval "db.createUser({user:'demo',pwd:'demo',roles:[{role:'dbOwner',db:'admin'},'root']});"
+
+```
+docker rm -f ngdemo_mongoexpress
+
+docker run --name ngdemo_mongoexpress -d --link ngdemo_mongodb:mongo -p 7081:8081 \
+   -e ME_CONFIG_MONGODB_ADMINUSERNAME=demo \
+   -e ME_CONFIG_MONGODB_ADMINPASSWORD=demo \
+   mongo-express
+```
+
+```
+docker run --rm --name ngdemo_mongoexpress --link ngdemo_mongodb:mongo -p 7081:8081 \
+   -e ME_CONFIG_MONGODB_ADMINUSERNAME=admin \
+   -e ME_CONFIG_MONGODB_ADMINPASSWORD=demo \
+   mongo-express
+
+```
+
+docker rm -f ngdemo_mongoexpress
+
+
+TODO:
+ - add log4j-nosql to mongodb