diff --git a/gulpfile.js b/gulpfile.js
index 2218e846..f6cb383b 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -72,7 +72,10 @@ gulp.task('serve', function() {
root : BUILD_DIR,
port : 8082,
middleware : function() {
- return [modRewrite(['^/rest/(.*)$ http://localhost:8083/rest/$1 [P]'])];
+ var rest = '^/rest/(.*)$ http://localhost:8083/rest/$1 [P]';
+ var next = '^/next(.*)$ http://localhost:8083/next$1 [P]';
+ var logout = '^/logout(.*)$ http://localhost:8083/logout$1 [P]';
+ return [modRewrite([rest, next, logout])];
}
});
});
diff --git a/src/main/app/js/main.js b/src/main/app/js/main.js
index 05e6b3fd..3afde3d0 100644
--- a/src/main/app/js/main.js
+++ b/src/main/app/js/main.js
@@ -8,7 +8,7 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv
cfpLoadingBarProvider.includeSpinner = false;
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/);
- var interceptor = ['$rootScope', '$q', function(scope, $q) {
+ var interceptor = ['$rootScope', '$q', '$injector', function(scope, $q, $injector) {
var success = function(response) {
return response;
@@ -16,7 +16,7 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv
var error = function(response) {
var status = response.status;
if (status == 401) {
- window.location = 'logout';
+ $injector.get('$state').transitionTo('welcome');
return;
} else {
return $q.reject(response);
@@ -123,6 +123,12 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv
templateUrl : 'templates/admin.metrics.html',
controller : 'MetricsCtrl'
});
+
+ $stateProvider.state('welcome', {
+ url : '/welcome',
+ templateUrl : 'templates/welcome.html',
+ controller : 'WelcomeCtrl'
+ });
$urlRouterProvider.when('/', '/feeds/view/category/all');
$urlRouterProvider.when('/admin', '/admin/settings');
diff --git a/src/main/app/templates/welcome.html b/src/main/app/templates/welcome.html
new file mode 100644
index 00000000..1ee2e69a
--- /dev/null
+++ b/src/main/app/templates/welcome.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/main/java/com/commafeed/CommaFeedApplication.java b/src/main/java/com/commafeed/CommaFeedApplication.java
index 0050ad69..93b562bb 100644
--- a/src/main/java/com/commafeed/CommaFeedApplication.java
+++ b/src/main/java/com/commafeed/CommaFeedApplication.java
@@ -4,6 +4,7 @@ import io.dropwizard.Application;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.hibernate.HibernateBundle;
+import io.dropwizard.jersey.sessions.HttpSessionProvider;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
@@ -12,6 +13,7 @@ import java.util.Date;
import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jetty.server.session.SessionHandler;
import org.hibernate.SessionFactory;
import com.codahale.metrics.MetricRegistry;
@@ -63,6 +65,7 @@ import com.commafeed.backend.service.PubSubService;
import com.commafeed.backend.service.StartupService;
import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.auth.SecurityCheckProvider;
+import com.commafeed.frontend.auth.SecurityCheckProvider.SecurityCheckUserServiceProvider;
import com.commafeed.frontend.resource.AdminREST;
import com.commafeed.frontend.resource.CategoryREST;
import com.commafeed.frontend.resource.EntryREST;
@@ -70,6 +73,7 @@ import com.commafeed.frontend.resource.FeedREST;
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
import com.commafeed.frontend.resource.ServerREST;
import com.commafeed.frontend.resource.UserREST;
+import com.commafeed.frontend.servlet.LogoutServlet;
import com.commafeed.frontend.servlet.NextUnreadServlet;
@Slf4j
@@ -78,6 +82,8 @@ public class CommaFeedApplication extends Application {
public static final String USERNAME_ADMIN = "admin";
public static final String USERNAME_DEMO = "demo";
+ public static final String SESSION_USER = "user";
+
public static final Date STARTUP_TIME = new Date();
private HibernateBundle hibernateBundle;
@@ -115,6 +121,7 @@ public class CommaFeedApplication extends Application {
: new RedisCacheService();
log.info("using cache {}", cacheService.getClass());
+ // DAOs
FeedCategoryDAO feedCategoryDAO = new FeedCategoryDAO(sessionFactory);
FeedDAO feedDAO = new FeedDAO(sessionFactory);
FeedEntryContentDAO feedEntryContentDAO = new FeedEntryContentDAO(sessionFactory);
@@ -128,6 +135,7 @@ public class CommaFeedApplication extends Application {
FeedQueues queues = new FeedQueues(feedDAO, config, metrics);
+ // Services
ApplicationPropertiesService applicationPropertiesService = new ApplicationPropertiesService();
DatabaseCleaningService cleaningService = new DatabaseCleaningService(feedDAO, feedEntryDAO, feedEntryContentDAO,
feedEntryStatusDAO, feedSubscriptionDAO);
@@ -144,22 +152,28 @@ public class CommaFeedApplication extends Application {
config);
StartupService startupService = new StartupService(sessionFactory, userDAO, userService);
+ // OPML
OPMLImporter opmlImporter = new OPMLImporter(feedCategoryDAO, feedSubscriptionService, cacheService);
OPMLExporter opmlExporter = new OPMLExporter(feedCategoryDAO, feedSubscriptionDAO);
+ // Feed fetching/parsing
HttpGetter httpGetter = new HttpGetter();
FeedParser feedParser = new FeedParser();
FaviconFetcher faviconFetcher = new FaviconFetcher(httpGetter);
-
FeedFetcher feedFetcher = new FeedFetcher(feedParser, httpGetter);
FeedRefreshUpdater feedUpdater = new FeedRefreshUpdater(sessionFactory, feedUpdateService, pubSubService, queues, config, metrics,
feedSubscriptionDAO, cacheService);
FeedRefreshWorker feedWorker = new FeedRefreshWorker(feedUpdater, feedFetcher, queues, config, metrics);
FeedRefreshTaskGiver taskGiver = new FeedRefreshTaskGiver(sessionFactory, queues, feedDAO, feedWorker, config, metrics);
- // TODO add caching of credentials somehow
- environment.jersey().register(new SecurityCheckProvider(userService));
+ // Auth/session management
+ environment.servlets().setSessionHandler(new SessionHandler());
+ environment.jersey().register(new SecurityCheckUserServiceProvider(userService));
+ environment.jersey().register(SecurityCheckProvider.class);
+ environment.jersey().register(HttpSessionProvider.class);
+
+ // REST resources
environment.jersey().setUrlPattern("/rest/*");
environment.jersey()
.register(new AdminREST(userDAO, userRoleDAO, userService, encryptionService, cleaningService, config, metrics));
@@ -174,18 +188,20 @@ public class CommaFeedApplication extends Application {
environment.jersey().register(new ServerREST(httpGetter, config, applicationPropertiesService));
environment.jersey().register(new UserREST(userDAO, userRoleDAO, userSettingsDAO, userService, encryptionService));
+ // Servlets
NextUnreadServlet nextUnreadServlet = new NextUnreadServlet(feedSubscriptionDAO, feedEntryStatusDAO, feedCategoryDAO, userService,
config);
+ LogoutServlet logoutServlet = new LogoutServlet(config);
environment.servlets().addServlet("next", nextUnreadServlet).addMapping("/next");
+ environment.servlets().addServlet("logout", logoutServlet).addMapping("/logout");
+ // Managed objects
environment.lifecycle().manage(startupService);
environment.lifecycle().manage(taskGiver);
environment.lifecycle().manage(feedWorker);
environment.lifecycle().manage(feedUpdater);
- // TODO user registration
- // TODO user login page
- // TODO cookie support ?
+ // TODO user login + registration page
// TODO translations
}
diff --git a/src/main/java/com/commafeed/backend/service/UserService.java b/src/main/java/com/commafeed/backend/service/UserService.java
index e71592a8..83469bb5 100644
--- a/src/main/java/com/commafeed/backend/service/UserService.java
+++ b/src/main/java/com/commafeed/backend/service/UserService.java
@@ -17,6 +17,7 @@ import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role;
+import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
@RequiredArgsConstructor
@@ -30,7 +31,7 @@ public class UserService {
private final PasswordEncryptionService encryptionService;
private final CommaFeedConfiguration config;
- public User login(String name, String password) {
+ public Optional login(String name, String password) {
if (name == null || password == null) {
return null;
}
@@ -58,22 +59,22 @@ public class UserService {
if (saveUser) {
userDAO.saveOrUpdate(user);
}
- return user;
+ return Optional.fromNullable(user);
}
}
- return null;
+ return Optional.absent();
}
- public User login(String apiKey) {
+ public Optional login(String apiKey) {
if (apiKey == null) {
- return null;
+ return Optional.absent();
}
User user = userDAO.findByApiKey(apiKey);
if (user != null && !user.isDisabled()) {
- return user;
+ return Optional.fromNullable(user);
}
- return null;
+ return Optional.absent();
}
public User register(String name, String password, String email, Collection roles) {
diff --git a/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java b/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java
index 47402e27..8f7ea7b8 100644
--- a/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java
+++ b/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java
@@ -1,18 +1,23 @@
package com.commafeed.frontend.auth;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
-import lombok.extern.slf4j.Slf4j;
+import lombok.RequiredArgsConstructor;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.StringUtil;
+import com.commafeed.CommaFeedApplication;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.service.UserService;
+import com.google.common.base.Optional;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.api.model.Parameter;
import com.sun.jersey.core.spi.component.ComponentContext;
@@ -20,66 +25,93 @@ import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
+import com.sun.jersey.spi.inject.SingletonTypeInjectableProvider;
-@Slf4j
public class SecurityCheckProvider implements InjectableProvider {
- private static class SecurityCheckInjectable extends AbstractHttpContextInjectable {
- private static final String PREFIX = "Basic";
+ public static class SecurityCheckUserServiceProvider extends SingletonTypeInjectableProvider {
- private final UserService userService;
- private Role role;
- private final boolean apiKeyAllowed;
-
- private SecurityCheckInjectable(UserService userService, Role role, boolean apiKeyAllowed) {
- this.userService = userService;
- this.role = role;
- this.apiKeyAllowed = apiKeyAllowed;
- }
-
- @Override
- public User getValue(HttpContext c) {
- final String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION);
- try {
- if (header != null) {
- final int space = header.indexOf(' ');
- if (space > 0) {
- final String method = header.substring(0, space);
- if (PREFIX.equalsIgnoreCase(method)) {
- final String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1);
- final int i = decoded.indexOf(':');
- if (i > 0) {
- final String username = decoded.substring(0, i);
- final String password = decoded.substring(i + 1);
- final User user = userService.login(username, password);
- if (user != null && user.hasRole(role)) {
- return user;
- }
- }
- }
- }
- } else {
- String apiKey = c.getUriInfo().getPathParameters().getFirst("apiKey");
- if (apiKey != null && apiKeyAllowed) {
- User user = userService.login(apiKey);
- if (user != null && user.hasRole(role)) {
- return user;
- }
- }
- }
- } catch (IllegalArgumentException e) {
- log.debug("Error decoding credentials", e);
- }
-
- throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
- .header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"CommaFeed\"")
- .entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build());
+ public SecurityCheckUserServiceProvider(UserService userService) {
+ super(UserService.class, userService);
}
}
+ @RequiredArgsConstructor
+ private static class SecurityCheckInjectable extends AbstractHttpContextInjectable {
+ private static final String PREFIX = "Basic";
+
+ private final HttpServletRequest request;
+ private final UserService userService;
+ private final Role role;
+ private final boolean apiKeyAllowed;
+
+ @Override
+ public User getValue(HttpContext c) {
+ Optional user = cookieSessionLogin();
+ if (!user.isPresent()) {
+ user = basicAuthenticationLogin(c);
+ }
+ if (!user.isPresent()) {
+ user = apiKeyLogin(c);
+ }
+
+ if (user.isPresent()) {
+ return user.get();
+ } else {
+ throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
+ .entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build());
+ }
+ }
+
+ private Optional cookieSessionLogin() {
+ HttpSession session = request.getSession(false);
+ if (session != null) {
+ User user = (User) session.getAttribute(CommaFeedApplication.SESSION_USER);
+ return Optional.fromNullable(user);
+ }
+ return Optional.absent();
+ }
+
+ private Optional basicAuthenticationLogin(HttpContext c) {
+ String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION);
+ if (header != null) {
+ int space = header.indexOf(' ');
+ if (space > 0) {
+ String method = header.substring(0, space);
+ if (PREFIX.equalsIgnoreCase(method)) {
+ String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1);
+ int i = decoded.indexOf(':');
+ if (i > 0) {
+ String username = decoded.substring(0, i);
+ String password = decoded.substring(i + 1);
+ Optional user = userService.login(username, password);
+ if (user.isPresent() && user.get().hasRole(role)) {
+ return user;
+ }
+ }
+ }
+ }
+ }
+ return Optional.absent();
+ }
+
+ private Optional apiKeyLogin(HttpContext c) {
+ String apiKey = c.getUriInfo().getPathParameters().getFirst("apiKey");
+ if (apiKey != null && apiKeyAllowed) {
+ Optional user = userService.login(apiKey);
+ if (user.isPresent() && user.get().hasRole(role)) {
+ return user;
+ }
+ }
+ return Optional.absent();
+ }
+ }
+
+ private HttpServletRequest request;
private UserService userService;
- public SecurityCheckProvider(UserService userService) {
+ public SecurityCheckProvider(@Context HttpServletRequest request, @Context UserService userService) {
+ this.request = request;
this.userService = userService;
}
@@ -90,6 +122,6 @@ public class SecurityCheckProvider implements InjectableProvider getInjectable(ComponentContext ic, SecurityCheck sc, Parameter c) {
- return new SecurityCheckInjectable<>(userService, sc.value(), sc.apiKeyAllowed());
+ return new SecurityCheckInjectable<>(request, userService, sc.value(), sc.apiKeyAllowed());
}
}
diff --git a/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java b/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java
new file mode 100644
index 00000000..5584e498
--- /dev/null
+++ b/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java
@@ -0,0 +1,20 @@
+package com.commafeed.frontend.model.request;
+
+import java.io.Serializable;
+
+import lombok.Data;
+
+import com.wordnik.swagger.annotations.ApiModel;
+import com.wordnik.swagger.annotations.ApiModelProperty;
+
+@SuppressWarnings("serial")
+@Data
+@ApiModel
+public class LoginRequest implements Serializable {
+
+ @ApiModelProperty(value = "username", required = true)
+ private String name;
+
+ @ApiModelProperty(value = "password", required = true)
+ private String password;
+}
diff --git a/src/main/java/com/commafeed/frontend/resource/UserREST.java b/src/main/java/com/commafeed/frontend/resource/UserREST.java
index eb22084d..f9856b05 100644
--- a/src/main/java/com/commafeed/frontend/resource/UserREST.java
+++ b/src/main/java/com/commafeed/frontend/resource/UserREST.java
@@ -1,9 +1,11 @@
package com.commafeed.frontend.resource;
import io.dropwizard.hibernate.UnitOfWork;
+import io.dropwizard.jersey.sessions.Session;
import java.util.Arrays;
+import javax.servlet.http.HttpSession;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@@ -33,8 +35,10 @@ import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.auth.SecurityCheck;
import com.commafeed.frontend.model.Settings;
import com.commafeed.frontend.model.UserModel;
+import com.commafeed.frontend.model.request.LoginRequest;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.model.request.RegistrationRequest;
+import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
@@ -203,7 +207,20 @@ public class UserREST {
} catch (Exception e) {
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
}
+ }
+ @Path("/login")
+ @POST
+ @UnitOfWork
+ @ApiOperation(value = "Login and create a session")
+ public Response login(@ApiParam(required = true) LoginRequest req, @Session HttpSession session) {
+ Optional user = userService.login(req.getName(), req.getPassword());
+ if (user.isPresent()) {
+ session.setAttribute(CommaFeedApplication.SESSION_USER, user);
+ return Response.ok().build();
+ } else {
+ return Response.status(Response.Status.UNAUTHORIZED).build();
+ }
}
@Path("/profile/deleteAccount")
diff --git a/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java b/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java
new file mode 100644
index 00000000..6500769d
--- /dev/null
+++ b/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java
@@ -0,0 +1,25 @@
+package com.commafeed.frontend.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import lombok.RequiredArgsConstructor;
+
+import com.commafeed.CommaFeedConfiguration;
+
+@SuppressWarnings("serial")
+@RequiredArgsConstructor
+public class LogoutServlet extends HttpServlet {
+
+ private final CommaFeedConfiguration config;
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ req.getSession().invalidate();
+ resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl()));
+ }
+}
diff --git a/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java b/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java
index 37b755cc..0cd05390 100644
--- a/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java
+++ b/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java
@@ -24,6 +24,7 @@ import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.resource.CategoryREST;
+import com.google.common.base.Optional;
import com.google.common.collect.Iterables;
@SuppressWarnings("serial")
@@ -51,8 +52,8 @@ public class NextUnreadServlet extends HttpServlet {
return;
}
- User user = userService.login(apiKey);
- if (user == null) {
+ Optional user = userService.login(apiKey);
+ if (!user.isPresent()) {
resp.getWriter().write("unknown user or api key not found");
return;
}
@@ -65,14 +66,15 @@ public class NextUnreadServlet extends HttpServlet {
List statuses = null;
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
- List subs = feedSubscriptionDAO.findAll(user);
- statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, false, null);
+ List subs = feedSubscriptionDAO.findAll(user.get());
+ statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order, true, false, null);
} else {
- FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
+ FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId));
if (category != null) {
- List children = feedCategoryDAO.findAllChildrenCategories(user, category);
- List subscriptions = feedSubscriptionDAO.findByCategories(user, children);
- statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1, order, true, false, null);
+ List children = feedCategoryDAO.findAllChildrenCategories(user.get(), category);
+ List subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children);
+ statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0, 1, order, true, false,
+ null);
}
}