THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

Browse Source

authentication: Support Github/Gmail social login

Yona support social login, Github and Gmail.
It doesn't need to sign up anymore.

This feature was easily developed thanks to "Play Athenticate"
See: https://github.com/joscha/play-authenticate
tags/v1.3.0
Suwon Chae 3 years ago
parent
commit
2bb95f7337
29 changed files with 840 additions and 101 deletions
  1. +1
    -0
      .gitignore
  2. +77
    -4
      app/Global.java
  3. +4
    -0
      app/assets/stylesheets/less/_override.less
  4. +14
    -0
      app/assets/stylesheets/less/_page.less
  5. +40
    -21
      app/controllers/Application.java
  6. +16
    -0
      app/controllers/Restricted.java
  7. +27
    -0
      app/controllers/Secured.java
  8. +86
    -19
      app/controllers/UserApp.java
  9. +50
    -0
      app/models/LinkedAccount.java
  10. +142
    -0
      app/models/UserCredential.java
  11. +52
    -0
      app/service/YonaUserServicePlugin.java
  12. +1
    -2
      app/utils/TemplateHelper.scala
  13. +24
    -17
      app/views/common/loginDialog.scala.html
  14. +20
    -1
      app/views/common/usermenu.scala.html
  15. +5
    -17
      app/views/index/partial_intro.scala.html
  16. +2
    -1
      app/views/layout.scala.html
  17. +31
    -0
      app/views/restricted.scala.html
  18. +29
    -18
      app/views/user/login.scala.html
  19. +3
    -0
      build.sbt
  20. +10
    -1
      conf/application.conf.default
  21. +40
    -0
      conf/evolutions/default/12.sql
  22. +3
    -0
      conf/play.plugins
  23. +6
    -0
      conf/routes
  24. +67
    -0
      conf/social-login.conf
  25. +67
    -0
      conf/social-login.conf.default
  26. +17
    -0
      public/images/provider-logo/btn_google_light_normal_ios.svg
  27. BIN
      public/images/provider-logo/g-logo.png
  28. +6
    -0
      public/images/provider-logo/github.svg
  29. BIN
      public/images/yona-logo.png

+ 1
- 0
.gitignore View File

@@ -31,3 +31,4 @@ conf/generated.keystore
conf/application-logger.xml
.java-version
migration-client
conf/play-authenticate/mine.conf

+ 77
- 4
app/Global.java View File

@@ -20,6 +20,9 @@
*/

import com.avaje.ebean.Ebean;
import com.feth.play.module.pa.PlayAuthenticate;
import com.feth.play.module.pa.exceptions.AccessDeniedException;
import com.feth.play.module.pa.exceptions.AuthException;
import com.typesafe.config.ConfigFactory;
import controllers.SvnApp;
import controllers.UserApp;
@@ -34,11 +37,8 @@ import play.Play;
import play.api.mvc.Handler;
import play.data.Form;
import play.libs.F.Promise;
import play.mvc.Action;
import play.mvc.Http;
import play.mvc.*;
import play.mvc.Http.RequestHeader;
import play.mvc.Result;
import play.mvc.Results;
import utils.*;
import views.html.welcome.restart;
import views.html.welcome.secret;
@@ -63,6 +63,7 @@ import java.time.format.DateTimeFormatter;

import static play.data.Form.form;
import static play.mvc.Results.badRequest;
import static play.mvc.Results.redirect;


public class Global extends GlobalSettings {
@@ -76,10 +77,12 @@ public class Global extends GlobalSettings {

private ConfigFile configFile = new ConfigFile("config", "application.conf");
private ConfigFile loggerConfigFile = new ConfigFile("logger", "application-logger.xml");
private ConfigFile oAuthProviderConfFile = new ConfigFile("conf", "social-login.conf");

@Override
public Configuration onLoadConfig(play.Configuration config, File path, ClassLoader classloader) {
initLoggerConfig();
initAuthProviderConfig();
return initConfig(classloader);
}

@@ -133,6 +136,23 @@ public class Global extends GlobalSettings {
}
}

/**
* Creates play-authenticate/mine.conf by default if necessary
*/
private void initAuthProviderConfig() {
try {
if (!oAuthProviderConfFile.isLocationSpecified() && !oAuthProviderConfFile.getPath().toFile().exists()) {
try {
oAuthProviderConfFile.createByDefault();
} catch (Exception e) {
play.Logger.error("Failed to initialize social-login.conf", e);
}
}
} catch (URISyntaxException e) {
play.Logger.error("Failed to check whether the social-login.conf file exists", e);
}
}

@Override
public void onStart(Application app) {
isSecretInvalid = equalsDefaultSecret();
@@ -150,6 +170,59 @@ public class Global extends GlobalSettings {
YobiUpdate.onStart();
mailboxService.start();
}

PlayAuthenticate.setResolver(new PlayAuthenticate.Resolver() {

@Override
public Call login() {
// Your login page
return routes.Application.index();
}

@Override
public Call afterAuth() {
// The user will be redirected to this page after authentication
// if no original URL was saved
return routes.Application.index();
}

@Override
public Call afterLogout() {
return routes.Application.index();
}

@Override
public Call auth(final String provider) {
return routes.Application.oAuth(provider);
}

@Override
public Call onException(final AuthException e) {
if (e instanceof AccessDeniedException) {
return routes.Application
.oAuthDenied(((AccessDeniedException) e)
.getProviderKey());
}

// more custom problem handling here...

return super.onException(e);
}

@Override
public Call askLink() {
// We don't support moderated account linking in this sample.
// See the play-authenticate-usage project for an example
return null;
}

@Override
public Call askMerge() {
// We don't support moderated account merging in this sample.
// See the play-authenticate-usage project for an example
return null;
}
});
}

private boolean equalsDefaultSecret() {


+ 4
- 0
app/assets/stylesheets/less/_override.less View File

@@ -365,3 +365,7 @@ li.select2-result-with-children:first-of-type {
margin-bottom: -1px;
}

.oauth-login-btn{
display: block;
margin: 10px 0;
}

+ 14
- 0
app/assets/stylesheets/less/_page.less View File

@@ -6638,3 +6638,17 @@ div.diff-body[data-outdated="true"] tr:hover .icon-comment {
margin-left: 10px;
width: 400px;
}

.auth-provider-logo {
font-family: 'Roboto', sans-serif;
svg {
vertical-align: middle;
}
.github {
width: 30px;
display: inline-block;
margin-left: -4px;
margin-top: 3px;
margin-bottom: 3px;
}
}

+ 40
- 21
app/controllers/Application.java View File

@@ -1,44 +1,63 @@
/**
* Yobi, Project Hosting SW
*
* Copyright 2012 NAVER Corp.
* http://yobi.io
*
* @author Sangcheol Hwang
*
* Licensed 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.
*/
* Yona, 21st Century Project Hosting SW
* <p>
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
**/
package controllers;

import com.feth.play.module.pa.PlayAuthenticate;
import controllers.annotation.AnonymousCheck;
import jsmessages.JsMessages;
import models.Project;
import models.UserCredential;
import play.Logger;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;
import playRepository.RepositoryService;
import views.html.error.notfound_default;
import views.html.index.index;
import jsmessages.JsMessages;

import java.io.File;
import static com.feth.play.module.pa.controllers.Authenticate.*;

public class Application extends Controller {
public static final String FLASH_MESSAGE_KEY = "message";
public static final String FLASH_ERROR_KEY = "error";

@AnonymousCheck
public static Result index() {
return ok(index.render(UserApp.currentUser()));
}

public static Result oAuth(final String provider) {
return authenticate(provider);
}

public static Result oAuthLogout() {
UserApp.logout();
return logout();
}

public static Result oAuthDenied(final String providerKey) {
noCache(response());
flash(FLASH_ERROR_KEY,
"You need to accept the OAuth connection in order to use this website!");
return redirect(routes.Application.index());
}

public static UserCredential getLocalUser(final Http.Session session) {
final UserCredential localUser = UserCredential.findByAuthUserIdentity(PlayAuthenticate
.getUser(session));
return localUser;
}

public static UserCredential getLocalUser() {
final UserCredential localUser = UserCredential.findByAuthUserIdentity(PlayAuthenticate
.getUser(session()));
return localUser;
}

public static Result removeTrailer(String paths){
String path = request().path();
if( path.charAt(path.length()-1) == '/' ) {


+ 16
- 0
app/controllers/Restricted.java View File

@@ -0,0 +1,16 @@
package controllers;

import models.UserCredential;
import play.mvc.Controller;
import play.mvc.Result;
import play.mvc.Security;
import views.html.restricted;

@Security.Authenticated(Secured.class)
public class Restricted extends Controller {

public static Result index() {
final UserCredential localUser = Application.getLocalUser(session());
return ok(restricted.render(localUser));
}
}

+ 27
- 0
app/controllers/Secured.java View File

@@ -0,0 +1,27 @@
package controllers;

import com.feth.play.module.pa.PlayAuthenticate;
import com.feth.play.module.pa.user.AuthUser;
import play.mvc.Http.Context;
import play.mvc.Result;
import play.mvc.Security;

public class Secured extends Security.Authenticator {

@Override
public String getUsername(final Context ctx) {
final AuthUser u = PlayAuthenticate.getUser(ctx.session());

if (u != null) {
return u.getId();
} else {
return null;
}
}

@Override
public Result onUnauthorized(final Context ctx) {
ctx.flash().put(Application.FLASH_MESSAGE_KEY, "Nice try, but you need to log in first!");
return redirect(routes.Application.index());
}
}

+ 86
- 19
app/controllers/UserApp.java View File

@@ -1,28 +1,16 @@
/**
* Yobi, Project Hosting SW
*
* Copyright 2013 NAVER Corp.
* http://yobi.io
*
* Licensed 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.
*/

* Yona, 21st Century Project Hosting SW
* <p>
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
**/
package controllers;

import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.Page;
import com.avaje.ebean.annotation.Transactional;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.feth.play.module.pa.PlayAuthenticate;
import controllers.annotation.AnonymousCheck;
import models.*;
import models.enumeration.Operation;
@@ -121,7 +109,14 @@ public class UserApp extends Controller {
if(StringUtils.isEmpty(redirectUrl) && !StringUtils.equals(loginFormUrl, referer)) {
redirectUrl = request().getHeader("Referer");
}
return ok(login.render("title.login", form(AuthInfo.class), redirectUrl));

//Assume oAtuh is passed but not linked with existed account
if(PlayAuthenticate.isLoggedIn(session())){
UserApp.linkWithExistedOrCreateLocalUser();
return redirect(redirectUrl);
} else {
return ok(login.render("title.login", form(AuthInfo.class), redirectUrl));
}
}

public static Result logout() {
@@ -332,6 +327,56 @@ public class UserApp extends Controller {
}
}

private static String newLoginIdWithoutDup(final String candidate, int num) {
String newLoginIdSuggestion = candidate + "" + num;
if(User.findByLoginId(newLoginIdSuggestion).isAnonymous()){
return newLoginIdSuggestion;
} else {
num = num + 1;
return newLoginIdWithoutDup(newLoginIdSuggestion, num);
}
}

public static void createLocalUserWithOAuth(UserCredential userCredential){
User user = new User();
String loginIdCandidate = userCredential.email.substring(0, userCredential.email.indexOf("@"));

user.loginId = generateLoginId(user, loginIdCandidate);
user.name = userCredential.name;
user.email = userCredential.email;

RandomNumberGenerator rng = new SecureRandomNumberGenerator();
user.password = rng.nextBytes().toBase64(); // random password because created with oAuth

User created = createNewUser(user);

if (created.state == UserState.LOCKED) {
flash(Constants.INFO, "user.signup.requested");
} else {
addUserInfoToSession(created);
}

//Also, update userCredential
userCredential.loginId = created.loginId;
userCredential.user = created;
userCredential.update();
}

private static String generateLoginId(User user, String loginIdCandidate) {
String loginId = null;
User sameLoginIdUser = User.findByLoginId(loginIdCandidate);
if (sameLoginIdUser.isAnonymous()) {
return loginIdCandidate;
} else {
sameLoginIdUser = User.findByLoginId(loginIdCandidate + "-yona");
if (sameLoginIdUser.isAnonymous()) {
return loginIdCandidate + "-yona"; // first dup, then use suffix "-yona"
} else {
return newLoginIdWithoutDup(loginIdCandidate, 2);
}
}
}

@Transactional
public static Result resetUserPassword() {
Form<User> userForm = form(User.class).bindFromRequest();
@@ -936,6 +981,28 @@ public class UserApp extends Controller {
session(SESSION_KEY, key);
}

public static void linkWithExistedOrCreateLocalUser() {
final UserCredential oAuthUser = UserCredential.findByAuthUserIdentity(PlayAuthenticate
.getUser(Http.Context.current().session()));
User user = null;
if (oAuthUser.loginId == null) {
user = User.findByEmail(oAuthUser.email);
} else {
user = User.findByLoginId(oAuthUser.loginId);
}

if(PlayAuthenticate.isLoggedIn(session()) && user.isAnonymous()){
createLocalUserWithOAuth(oAuthUser);
} else {
if (oAuthUser.loginId == null) {
oAuthUser.loginId = user.loginId;
oAuthUser.user = user;
oAuthUser.update();
}
UserApp.addUserInfoToSession(user);
}
}

public static void updatePreferredLanguage() {
Http.Request request = Http.Context.current().request();
User user = UserApp.currentUser();


+ 50
- 0
app/models/LinkedAccount.java View File

@@ -0,0 +1,50 @@
package models;

import com.feth.play.module.pa.user.AuthUser;
import play.db.ebean.Model;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class LinkedAccount extends Model {

private static final long serialVersionUID = 1L;

@Id
public Long id;

@ManyToOne
public UserCredential userCredential;

public String providerUserId;
public String providerKey;

public static final Finder<Long, LinkedAccount> find = new Finder<Long, LinkedAccount>(
Long.class, LinkedAccount.class);

public static LinkedAccount findByProviderKey(final UserCredential userCredential, String key) {
return find.where().eq("userCredential", userCredential).eq("providerKey", key)
.findUnique();
}

public static LinkedAccount create(final AuthUser authUser) {
final LinkedAccount ret = new LinkedAccount();
ret.update(authUser);
return ret;
}
public void update(final AuthUser authUser) {
this.providerKey = authUser.getProvider();
this.providerUserId = authUser.getId();
}

public static LinkedAccount create(final LinkedAccount acc) {
final LinkedAccount ret = new LinkedAccount();
ret.providerKey = acc.providerKey;
ret.providerUserId = acc.providerUserId;

return ret;
}
}

+ 142
- 0
app/models/UserCredential.java View File

@@ -0,0 +1,142 @@
/**
* Yona, 21st Century Project Hosting SW
* <p>
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
**/
package models;

import com.avaje.ebean.Ebean;
import com.avaje.ebean.ExpressionList;
import com.feth.play.module.pa.user.AuthUser;
import com.feth.play.module.pa.user.AuthUserIdentity;
import com.feth.play.module.pa.user.EmailIdentity;
import com.feth.play.module.pa.user.NameIdentity;
import play.data.validation.Constraints;
import play.db.ebean.Model;

import javax.persistence.*;
import java.util.*;

@Entity
public class UserCredential extends Model {
private static final long serialVersionUID = 1L;

@Id
public Long id;

@OneToOne
public User user;

public String loginId;

@Constraints.Email
// if you make this unique, keep in mind that users *must* merge/link their
// accounts then on signup with additional providers
// @Column(unique = true)
public String email;

public String name;

public boolean active;

public boolean emailValidated;

@OneToMany(cascade = CascadeType.ALL)
public List<LinkedAccount> linkedAccounts;

public static final Finder<Long, UserCredential> find = new Finder<Long, UserCredential>(
Long.class, UserCredential.class);

public static boolean existsByAuthUserIdentity(
final AuthUserIdentity identity) {
final ExpressionList<UserCredential> exp = getAuthUserFind(identity);
return exp.findRowCount() > 0;
}

private static ExpressionList<UserCredential> getAuthUserFind(
final AuthUserIdentity identity) {
return find.where().eq("active", true)
.eq("linkedAccounts.providerUserId", identity.getId())
.eq("linkedAccounts.providerKey", identity.getProvider());
}

public static UserCredential findByAuthUserIdentity(final AuthUserIdentity identity) {
if (identity == null) {
return null;
}
return getAuthUserFind(identity).findUnique();
}

public void merge(final UserCredential otherUser) {
for (final LinkedAccount acc : otherUser.linkedAccounts) {
this.linkedAccounts.add(LinkedAccount.create(acc));
}
// do all other merging stuff here - like resources, etc.

// deactivate the merged user that got added to this one
otherUser.active = false;
Ebean.save(Arrays.asList(new UserCredential[] { otherUser, this }));
}

public static UserCredential create(final AuthUser authUser) {
final UserCredential userCredential = new UserCredential();
userCredential.active = true;
userCredential.linkedAccounts = Collections.singletonList(LinkedAccount
.create(authUser));

if (authUser instanceof EmailIdentity) {
final EmailIdentity identity = (EmailIdentity) authUser;
// Remember, even when getting them from FB & Co., emails should be
// verified within the application as a security breach there might
// break your security as well!
userCredential.email = identity.getEmail();
userCredential.emailValidated = false;
}

if (authUser instanceof NameIdentity) {
final NameIdentity identity = (NameIdentity) authUser;
final String name = identity.getName();
if (name != null) {
userCredential.name = name;
}
}

userCredential.save();
return userCredential;
}

public static void merge(final AuthUser oldUser, final AuthUser newUser) {
UserCredential.findByAuthUserIdentity(oldUser).merge(
UserCredential.findByAuthUserIdentity(newUser));
}

public Set<String> getProviders() {
final Set<String> providerKeys = new HashSet<String>(
linkedAccounts.size());
for (final LinkedAccount acc : linkedAccounts) {
providerKeys.add(acc.providerKey);
}
return providerKeys;
}

public static void addLinkedAccount(final AuthUser oldUser,
final AuthUser newUser) {
final UserCredential u = UserCredential.findByAuthUserIdentity(oldUser);
u.linkedAccounts.add(LinkedAccount.create(newUser));
u.save();
}

public static UserCredential findByEmail(final String email) {
return getEmailUserFind(email).findUnique();
}

private static ExpressionList<UserCredential> getEmailUserFind(final String email) {
return find.where().eq("active", true).eq("email", email);
}

public LinkedAccount getAccountByProvider(final String providerKey) {
return LinkedAccount.findByProviderKey(this, providerKey);
}

}

+ 52
- 0
app/service/YonaUserServicePlugin.java View File

@@ -0,0 +1,52 @@
package service;

import com.feth.play.module.pa.service.UserServicePlugin;
import com.feth.play.module.pa.user.AuthUser;
import com.feth.play.module.pa.user.AuthUserIdentity;
import models.UserCredential;
import play.Application;

public class YonaUserServicePlugin extends UserServicePlugin {

public YonaUserServicePlugin(final Application app) {
super(app);
}

@Override
public Object save(final AuthUser authUser) {
final boolean isLinked = UserCredential.existsByAuthUserIdentity(authUser);
if (!isLinked) {
return UserCredential.create(authUser).id;
} else {
// we have this user already, so return null
return null;
}
}

@Override
public Object getLocalIdentity(final AuthUserIdentity identity) {
// For production: Caching might be a good idea here...
// ...and dont forget to sync the cache when users get deactivated/deleted
final UserCredential u = UserCredential.findByAuthUserIdentity(identity);
if(u != null) {
return u.id;
} else {
return null;
}
}

@Override
public AuthUser merge(final AuthUser newUser, final AuthUser oldUser) {
if (!oldUser.equals(newUser)) {
UserCredential.merge(oldUser, newUser);
}
return oldUser;
}

@Override
public AuthUser link(final AuthUser oldUser, final AuthUser newUser) {
UserCredential.addLinkedAccount(oldUser, newUser);
return null;
}

}

+ 1
- 2
app/utils/TemplateHelper.scala View File

@@ -1,7 +1,7 @@
package utils

import org.apache.commons.lang3.StringUtils
import play.mvc.Call
import play.mvc.{Call, Http}
import org.joda.time.DateTimeConstants
import org.apache.commons.io.FilenameUtils
import play.i18n.Messages
@@ -13,7 +13,6 @@ import java.net.URI
import playRepository.DiffLine
import playRepository.DiffLineType
import models.CodeRange.Side

import views.html.partial_diff_comment_on_line
import views.html.partial_diff_line
import views.html.git.partial_pull_request_event


+ 24
- 17
app/views/common/loginDialog.scala.html View File

@@ -1,24 +1,22 @@
@**
* Yobi, Project Hosting SW
* Yona, 21st Century Project Hosting SW
*
* Copyright 2014 NAVER Corp.
* http://yobi.io
*
* @author Jihan Kim
*
* Licensed 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.
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
**@
@()
@import com.feth.play.module.pa.views.html._

@providerWithLogo(provider:String) = @{
val googleLogo = routes.Assets.at("images/provider-logo/btn_google_light_normal_ios.svg")
val githubLogo = routes.Assets.at("images/provider-logo/github.svg")
provider match {
case "github" => s"""<span class="auth-provider-logo"><span class="github"><svg aria-hidden="true" height="24" version="1.1" viewBox="0 0 16 16" width="20"><path
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59 0.4 0.07 0.55-0.17 0.55-0.38 0-0.19-0.01-0.82-0.01-1.49-2.01 0.37-2.53-0.49-2.69-0.94-0.09-0.23-0.48-0.94-0.82-1.13-0.28-0.15-0.68-0.52-0.01-0.53 0.63-0.01 1.08 0.58 1.23 0.82 0.72 1.21 1.87 0.87 2.33 0.66 0.07-0.52 0.28-0.87 0.51-1.07-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.59 0.82-2.15-0.08-0.2-0.36-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82 0.64-0.18 1.32-0.27 2-0.27 0.68 0 1.36 0.09 2 0.27 1.53-1.04 2.2-0.82 2.2-0.82 0.44 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.82 1.27 0.82 2.15 0 3.07-1.87 3.75-3.65 3.95 0.29 0.25 0.54 0.73 0.54 1.48 0 1.07-0.01 1.93-0.01 2.2 0 0.21 0.15 0.46 0.55 0.38C13.71 14.53 16 11.53 16 8 16 3.58 12.42 0 8 0z"></path></svg></span> <span class="provider-name">Sign in with Github</span></span>"""
case "google" => s"""<span class="auth-provider-logo"><img src="$googleLogo" alt="login with Google"> Sign in with Google</span>"""
case _ => ""
}
}

<div id="loginDialog" class="modal hide loginDialog" tabindex="-1" role="dialog">
<div class="modal-body">
@@ -51,6 +49,15 @@
<button type="submit" class="ybtn ybtn-primary fullsize">@Messages("button.login")</button>
</div>

<div class="btns-row nm">
@currentAuth() { auth =>
@if(auth == null) {
@forProviders() { p =>
<a href="@p.getUrl()" class="ybtn oauth-login-btn">@Html(providerWithLogo(p.getKey()))</a>
}
}
}
</div>
<div class="act-row right-txt mt20">
<div class="pull-left">
<input id="remember-meD" type="checkbox" name="rememberMe" class="checkbox" checked>


+ 20
- 1
app/views/common/usermenu.scala.html View File

@@ -6,13 +6,32 @@
**@
@(project:Project)
@import utils.TemplateHelper._
@import com.feth.play.module.pa.PlayAuthenticate._
@import com.feth.play.module.pa.views.html._
@import play.mvc.Http._;
@orderString = @{"createdDate DESC"}
@if(UserApp.currentUser().isAnonymous){
@currentAuth() { auth => @{
if(auth == null) {
val url = storeOriginalUrl(Context.current())
} else {
UserApp.linkWithExistedOrCreateLocalUser()
}
}
}
}
<div id="mySidenav" class="sidenav">
<div class="span5 right-menu span-hard-wrap">
<div class="row-fluid user-menu-wrap">
<span class="user-menu"><a href="@routes.UserApp.userInfo(UserApp.currentUser().loginId)">@Messages("userinfo.profile")</a></span>
<span class="user-menu"><a href="@routes.UserApp.editUserInfoForm()">@Messages("userinfo.accountSetting")</a></span>
<a href="@routes.UserApp.logout()"><span class="user-menu logout label">@Messages("title.logout")</span></a>
@currentAuth() { auth =>
@if(auth != null) {
<a href="@routes.Application.oAuthLogout"><span class="user-menu logout label">@Messages("title.logout")</span></a>
} else {
<a href="@routes.UserApp.logout()"><span class="user-menu logout label">@Messages("title.logout")</span></a>
}
}
</div>
<ul class="nav nav-tabs nm">
<li class="myProjectList active">


+ 5
- 17
app/views/index/partial_intro.scala.html View File

@@ -1,23 +1,11 @@
@**
* Yobi, Project Hosting SW
* Yona, 21st Century Project Hosting SW
*
* Copyright 2013 NAVER Corp.
* http://yobi.io
*
* @author Deokhong Kim
*
* Licensed 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.
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
**@
@import com.feth.play.module.pa.views.html._

<div class="siteintro-bg row">
<div class="siteintro">
<div class="siteintro-cover">


+ 2
- 1
app/views/layout.scala.html View File

@@ -22,7 +22,8 @@
<meta name="twitter:title" content="@titleArray(0)" />
<meta name="twitter:url" content="@play.mvc.Http.Context.current().request().path()" />
<meta name="twitter:description" content="@{titleArray(titleArray.length-1)}" />
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.ico")">
<link rel="shortcut icon" type="image/x-icon" href="@routes.Assets.at("images/favicon.ico")">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("bootstrap/css/bootstrap.css")">
<link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("stylesheets/yobicon/style.css")">
<link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("javascripts/lib/select2/select2.css")"/>


+ 31
- 0
app/views/restricted.scala.html View File

@@ -0,0 +1,31 @@
@(localUser: models.UserCredential = null)

@import com.feth.play.module.pa.views.html._

@siteLayout(utils.Config.getSiteName, utils.MenuType.SITE_HOME) {
<h1>Sshhh...don't tell anyone!</h1>
<p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/9bZkp7q19f0" frameborder="0" allowfullscreen></iframe>
</p>
<p>
Your name is @localUser.name and your email address is @localUser.email
<i>
@if(!localUser.emailValidated) {
(unverified)
} else {
(verified)
}</i>!
<br/>
@currentAuth() { auth =>
Logged in with provider '@auth.getProvider()' and the user ID '@auth.getId()'<br/>
Your session expires
@if(auth.expires() == -1){
never
} else {
at @auth.expires() (UNIX timestamp)
}
}
</p>
}

+ 29
- 18
app/views/user/login.scala.html View File

@@ -1,24 +1,25 @@
@**
* Yobi, Project Hosting SW
* Yona, 21st Century Project Hosting SW
*
* Copyright 2012 NAVER Corp.
* http://yobi.io
*
* @author Hwi Ahn
*
* Licensed 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.
* Copyright Yona & Yobi Authors & NAVER Corp.
* https://yona.io
**@
@(message:String, authInfoForm: play.data.Form[AuthInfo], redirectUrl:String)
@import play.data.Form
@(message:String, authInfoForm: Form[AuthInfo], redirectUrl:String)
@import com.feth.play.module.pa.views.html._
@import com.feth.play.module.pa.PlayAuthenticate._;
@import play.mvc.Http._;

@providerWithLogo(provider:String) = @{
val googleLogo = routes.Assets.at("images/provider-logo/btn_google_light_normal_ios.svg")
val githubLogo = routes.Assets.at("images/provider-logo/github.svg")
provider match {
case "github" => s"""<span class="auth-provider-logo"><span class="github"><svg aria-hidden="true" height="24" version="1.1" viewBox="0 0 16 16" width="20"><path
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59 0.4 0.07 0.55-0.17 0.55-0.38 0-0.19-0.01-0.82-0.01-1.49-2.01 0.37-2.53-0.49-2.69-0.94-0.09-0.23-0.48-0.94-0.82-1.13-0.28-0.15-0.68-0.52-0.01-0.53 0.63-0.01 1.08 0.58 1.23 0.82 0.72 1.21 1.87 0.87 2.33 0.66 0.07-0.52 0.28-0.87 0.51-1.07-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.59 0.82-2.15-0.08-0.2-0.36-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82 0.64-0.18 1.32-0.27 2-0.27 0.68 0 1.36 0.09 2 0.27 1.53-1.04 2.2-0.82 2.2-0.82 0.44 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.82 1.27 0.82 2.15 0 3.07-1.87 3.75-3.65 3.95 0.29 0.25 0.54 0.73 0.54 1.48 0 1.07-0.01 1.93-0.01 2.2 0 0.21 0.15 0.46 0.55 0.38C13.71 14.53 16 11.53 16 8 16 3.58 12.42 0 8 0z"></path></svg></span> <span class="provider-name">Sign in with Github</span></span>"""
case "google" => s"""<span class="auth-provider-logo"><img src="$googleLogo" alt="login with Google"> Sign in with Google</span>"""
case _ => ""
}
}

@siteLayout(message, utils.MenuType.NONE) {
<div class="page full">
@@ -51,6 +52,16 @@
<button type="submit" class="ybtn ybtn-primary ybtn-large ybtn-fullsize">@Messages("button.login")</button>
</div>

<div class="btns-row nm">
@currentAuth() { auth =>
@if(auth == null) {
@forProviders() { p =>
<a href="@p.getUrl()" class="ybtn oauth-login-btn">@Html(providerWithLogo(p.getKey()))</a>
}
}
}
</div>

<div class="act-row mt5">
<div class="remember-me-wrap pull-left">
<input id="remember-me" type="checkbox" name="rememberMe" class="checkbox" checked>


+ 3
- 0
build.sbt View File

@@ -12,6 +12,9 @@ libraryDependencies ++= Seq(
javaEbean,
javaWs,
cache,
// PlayAuthenticat for social login
// https://github.com/joscha/play-authenticate
"com.feth" %% "play-authenticate" % "0.6.9",
// OWASP Java HTML Sanitizer
// https://www.owasp.org/index.php/OWASP_Java_HTML_Sanitizer_Project
"com.googlecode.owasp-java-html-sanitizer" % "owasp-java-html-sanitizer" % "20160628.1",


+ 10
- 1
conf/application.conf.default View File

@@ -259,8 +259,17 @@ github.allow.migration = false
github.client.id = "TYPE YOUR GITHUB CILENT ID"
github.client.secret = "TYPE YOUR GITHUB CILENT SECRET"


# Attachment Upload File Size Limit
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 2,147,483,454 bytes = 2Gb
application.maxFileSize = 2147483454

# Social Login Support
# ~~~~~~~~~~~~~~~~~~~~
# Social login settings for Yona
# Detail settings are described at conf/play-authenticate/mine.conf

# Prevent using Yona's own login system
application.use.social.login.only = true

include "social-login.conf"

+ 40
- 0
conf/evolutions/default/12.sql View File

@@ -0,0 +1,40 @@
# --- !Ups

create table linked_account (
id bigint auto_increment not null,
user_credential_id bigint,
provider_user_id varchar(255),
provider_key varchar(255),
constraint pk_linked_account primary key (id))
row_format=compressed, key_block_size=8
;

create table user_credential (
id bigint auto_increment not null,
user_id bigint,
login_id varchar(255),
email varchar(255),
name varchar(255),
active tinyint(1) default 0,
email_validated tinyint(1) default 0,
constraint pk_users primary key (id),
CONSTRAINT fk_user_credential_user FOREIGN KEY (user_id) REFERENCES n4user (id) on DELETE CASCADE)
row_format=compressed, key_block_size=8
;

create index ix_user_credential_user_id_1 on user_credential (user_id);

alter table linked_account add constraint fk_linked_account_user_1 foreign key (user_credential_id) references user_credential (id) on delete CASCADE;

create index ix_linked_account_user_credential_1 on linked_account (user_credential_id);


# --- !Downs

SET FOREIGN_KEY_CHECKS=0;

drop table linked_account;

drop table user_credential;

SET FOREIGN_KEY_CHECKS=1;

+ 3
- 0
conf/play.plugins View File

@@ -1 +1,4 @@
10005:service.YonaUserServicePlugin
10010:com.feth.play.module.pa.providers.oauth2.google.GoogleAuthProvider
10020:com.feth.play.module.pa.providers.oauth2.github.GithubAuthProvider
15000:info.schleichardt.play2.mailplugin.MailPlugin

+ 6
- 0
conf/routes View File

@@ -10,6 +10,12 @@ GET /navitest
# Home page
GET / controllers.Application.index()

# Play!Authenticate
GET /logout controllers.Application.oAuthLogout
GET /authenticate/:provider controllers.Application.oAuth(provider: String)
GET /authenticate/:provider/denied controllers.Application.oAuthDenied(provider: String)
GET /restricted controllers.Restricted.index

# Migration support page
GET /migration controllers.MigrationApp.migration()
GET /migration/projects controllers.MigrationApp.projects()


+ 67
- 0
conf/social-login.conf View File

@@ -0,0 +1,67 @@
#####################################################################################
#
# play-authenticate settings
#
#####################################################################################

play-authenticate {

# If set to true, account merging is enabled, if set to false its disabled and accounts will never prompted to be merged
# defaults to true
accountMergeEnabled=false

# if this is set to true, accounts are automatically linked
# (e.g. if a user is logged in and uses a different authentication provider
# which has NOT yet been registered to another user, this newly used authentication
# provider gets added to the current local user
# Handle setting this to true with care
# If set to false, your resolver must not return null for askLink()
# defaults to false
accountAutoLink=true

# Settings for the google-based authentication provider
# if you are not using it, you can remove this portion of the config file
# and remove the Google provider from conf/play.plugins
google {
redirectUri {
# Whether the redirect URI scheme should be HTTP or HTTPS (HTTP by default)
secure=false

# You can use this setting to override the automatic detection
# of the host used for the redirect URI (helpful if your service is running behind a CDN for example)
# host=yourdomain.com
}

# Google credentials
# These are mandatory for using OAuth and need to be provided by you,
# if you want to use Google as an authentication provider.
# Get them here: https://code.google.com/apis/console
# Remove leading '#' after entering

# Following key values are used for test. You must set it your own values.
clientId=300340907286-8gr74ghhenrqgk2ioavjip36qm2bbvn1.apps.googleusercontent.com
clientSecret=ocFoKh7De6nDQm1x-lGxcGRO
}

github {
redirectUri {
# Whether the redirect URI scheme should be HTTP or HTTPS (HTTP by default)
secure=false

# You can use this setting to override the automatic detection
# of the host used for the redirect URI (helpful if your service is running behind a CDN for example)
# host=yourdomain.com
}

# Following three keys are used for Yona to Github Enterprise login support only
# If you just need to use Github.com login, don't uncomment it.
#
# authorizationUrl="https://your-github-enterprise/login/oauth/authorize"
# accessTokenUrl="https://your-github-enterprise/login/oauth/access_token"
# userInfoUrl="https://your-github-enterprise/api/v3/user"

# Following key values are used for test. You must set it your own values.
clientId=add60851e36488138581
clientSecret=3f9472dcd7cb4c3c09e06b03f97f1b5fe2315af3
}
}

+ 67
- 0
conf/social-login.conf.default View File

@@ -0,0 +1,67 @@
#####################################################################################
#
# play-authenticate settings
#
#####################################################################################

play-authenticate {

# If set to true, account merging is enabled, if set to false its disabled and accounts will never prompted to be merged
# defaults to true
accountMergeEnabled=false

# if this is set to true, accounts are automatically linked
# (e.g. if a user is logged in and uses a different authentication provider
# which has NOT yet been registered to another user, this newly used authentication
# provider gets added to the current local user
# Handle setting this to true with care
# If set to false, your resolver must not return null for askLink()
# defaults to false
accountAutoLink=true

# Settings for the google-based authentication provider
# if you are not using it, you can remove this portion of the config file
# and remove the Google provider from conf/play.plugins
google {
redirectUri {
# Whether the redirect URI scheme should be HTTP or HTTPS (HTTP by default)
secure=false

# You can use this setting to override the automatic detection
# of the host used for the redirect URI (helpful if your service is running behind a CDN for example)
# host=yourdomain.com
}

# Google credentials
# These are mandatory for using OAuth and need to be provided by you,
# if you want to use Google as an authentication provider.
# Get them here: https://code.google.com/apis/console
# Remove leading '#' after entering

# Following key values are used for test. You must set it your own values.
clientId=300340907286-8gr74ghhenrqgk2ioavjip36qm2bbvn1.apps.googleusercontent.com
clientSecret=ocFoKh7De6nDQm1x-lGxcGRO
}

github {
redirectUri {
# Whether the redirect URI scheme should be HTTP or HTTPS (HTTP by default)
secure=false

# You can use this setting to override the automatic detection
# of the host used for the redirect URI (helpful if your service is running behind a CDN for example)
# host=yourdomain.com
}

# Following three keys are used for Yona to Github Enterprise login support only
# If you just need to use Github.com login, don't uncomment it.
#
# authorizationUrl="https://your-github-enterprise/login/oauth/authorize"
# accessTokenUrl="https://your-github-enterprise/login/oauth/access_token"
# userInfoUrl="https://your-github-enterprise/api/v3/user"

# Following key values are used for test. You must set it your own values.
clientId=add60851e36488138581
clientSecret=3f9472dcd7cb4c3c09e06b03f97f1b5fe2315af3
}
}

+ 17
- 0
public/images/provider-logo/btn_google_light_normal_ios.svg View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="30" height="30" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
<title>btn_google_light_normal_ios</title>
<desc>Created with Sketch.</desc>
<g id="Google-Button" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="btn_google_light_normal" sketch:type="MSArtboardGroup" transform="translate(-1.000000, -1.000000)">
<g id="logo_googleg_48dp" sketch:type="MSLayerGroup" transform="translate(7.000000, 7.000000)">
<path d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z" id="Shape" fill="#4285F4" sketch:type="MSShapeGroup"></path>
<path d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z" id="Shape" fill="#34A853" sketch:type="MSShapeGroup"></path>
<path d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z" id="Shape" fill="#FBBC05" sketch:type="MSShapeGroup"></path>
<path d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z" id="Shape" fill="#EA4335" sketch:type="MSShapeGroup"></path>
<path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z" id="Shape" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
</svg>

BIN
public/images/provider-logo/g-logo.png View File

Before After
Width: 80  |  Height: 80  |  Size: 1.7 KiB

+ 6
- 0
public/images/provider-logo/github.svg View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg aria-hidden="true" height="44" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="46">
<title>btn_github</title>
<desc>Modified by Suwon Chae</desc>
<path
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59 0.4 0.07 0.55-0.17 0.55-0.38 0-0.19-0.01-0.82-0.01-1.49-2.01 0.37-2.53-0.49-2.69-0.94-0.09-0.23-0.48-0.94-0.82-1.13-0.28-0.15-0.68-0.52-0.01-0.53 0.63-0.01 1.08 0.58 1.23 0.82 0.72 1.21 1.87 0.87 2.33 0.66 0.07-0.52 0.28-0.87 0.51-1.07-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.59 0.82-2.15-0.08-0.2-0.36-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82 0.64-0.18 1.32-0.27 2-0.27 0.68 0 1.36 0.09 2 0.27 1.53-1.04 2.2-0.82 2.2-0.82 0.44 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.82 1.27 0.82 2.15 0 3.07-1.87 3.75-3.65 3.95 0.29 0.25 0.54 0.73 0.54 1.48 0 1.07-0.01 1.93-0.01 2.2 0 0.21 0.15 0.46 0.55 0.38C13.71 14.53 16 11.53 16 8 16 3.58 12.42 0 8 0z"></path></svg>

BIN
public/images/yona-logo.png View File

Before After
Width: 296  |  Height: 288  |  Size: 5.2 KiB

Loading…
Cancel
Save