summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.xml4
-rw-r--r--ivy.xml1
-rw-r--r--src/com/p4square/f1oauth/F1OAuthHelper.java128
-rw-r--r--src/com/p4square/f1oauth/SecondPartyAuthenticator.java52
-rw-r--r--src/com/p4square/f1oauth/SecondPartyVerifier.java60
-rw-r--r--src/com/p4square/grow/frontend/ErrorPage.java25
-rw-r--r--src/com/p4square/grow/frontend/GrowFrontend.java71
-rw-r--r--src/com/p4square/grow/frontend/LoginFormAuthenticator.java122
-rw-r--r--src/com/p4square/grow/frontend/LoginPageResource.java40
-rw-r--r--src/com/p4square/grow/frontend/NewAccountResource.java115
-rw-r--r--src/com/p4square/grow/frontend/SurveyPageResource.java6
-rw-r--r--src/com/p4square/grow/frontend/TrainingPageResource.java6
-rw-r--r--src/com/p4square/grow/frontend/VideosResource.java4
-rw-r--r--src/com/p4square/grow/frontend/session/Session.java55
-rw-r--r--src/com/p4square/grow/frontend/session/SessionAuthenticator.java (renamed from src/com/p4square/grow/frontend/LoginAuthenticator.java)28
-rw-r--r--src/com/p4square/grow/frontend/session/SessionCheckingAuthenticator.java38
-rw-r--r--src/com/p4square/grow/frontend/session/SessionCookieAuthenticator.java59
-rw-r--r--src/com/p4square/grow/frontend/session/SessionCreatingAuthenticator.java45
-rw-r--r--src/com/p4square/grow/frontend/session/Sessions.java80
-rw-r--r--src/com/p4square/restlet/oauth/OAuthAuthenticator.java95
-rw-r--r--src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java177
-rw-r--r--src/com/p4square/restlet/oauth/OAuthException.java25
-rw-r--r--src/com/p4square/restlet/oauth/OAuthHelper.java143
-rw-r--r--src/com/p4square/restlet/oauth/OAuthUser.java50
-rw-r--r--src/com/p4square/restlet/oauth/Token.java52
-rw-r--r--src/grow.properties5
-rw-r--r--src/templates/macros/common.ftl1
-rw-r--r--src/templates/macros/textcontent.ftl8
-rw-r--r--src/templates/pages/about.html.ftl69
-rw-r--r--src/templates/pages/assessment.html.ftl44
-rw-r--r--src/templates/pages/contact.html.ftl37
-rw-r--r--src/templates/pages/index.html.ftl15
-rw-r--r--src/templates/pages/login.html.ftl6
-rw-r--r--src/templates/pages/newaccount.html.ftl26
-rw-r--r--src/templates/pages/verification.html.ftl17
-rw-r--r--src/templates/templates/assessment-results.ftl32
-rw-r--r--src/templates/templates/getstarted-button.ftl7
-rw-r--r--src/templates/templates/index-hero.ftl3
-rw-r--r--src/templates/templates/stage-complete.ftl56
-rw-r--r--web/favicon.icobin0 -> 4286 bytes
-rw-r--r--web/style.css4
41 files changed, 1713 insertions, 98 deletions
diff --git a/build.xml b/build.xml
index 7bc76d4..02b70b7 100644
--- a/build.xml
+++ b/build.xml
@@ -3,6 +3,10 @@
<property name="main.class" value="com.p4square.grow.frontend.GrowFrontend" />
+ <property name="deploy.manager.url" value="http://californium.jesterpm.net:9090/manager/text" />
+ <property name="deploy.manager.username" value="deploymentscript" />
+ <property name="deploy.context.path" value="/grow-frontend" />
+
<property file="${user.home}/.jesterpm-build-tools.properties" />
<property name="jesterpm.buildtools.root" value="../jesterpm-build-tools" />
<import file="${jesterpm.buildtools.root}/ant/tomcat-common.xml" />
diff --git a/ivy.xml b/ivy.xml
index bfc7f06..45f843c 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -14,6 +14,7 @@
<dependencies defaultconf="default,sources">
<dependency org="net.jesterpm" name="fmfacade" rev="[1.0-SNAPSHOT,)" />
+ <dependency org="org.restlet.jse" name="org.restlet.ext.httpclient" rev="[2.1,)" />
<!-- Backend Dependencies -->
<dependency org="com.netflix.astyanax" name="astyanax-core" rev="[1.0,)" />
diff --git a/src/com/p4square/f1oauth/F1OAuthHelper.java b/src/com/p4square/f1oauth/F1OAuthHelper.java
new file mode 100644
index 0000000..d75460f
--- /dev/null
+++ b/src/com/p4square/f1oauth/F1OAuthHelper.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import java.net.URLEncoder;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Response;
+import org.restlet.Request;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Method;
+import org.restlet.engine.util.Base64;
+import org.restlet.representation.StringRepresentation;
+
+import com.p4square.restlet.oauth.OAuthException;
+import com.p4square.restlet.oauth.OAuthHelper;
+import com.p4square.restlet.oauth.OAuthUser;
+import com.p4square.restlet.oauth.Token;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class F1OAuthHelper extends OAuthHelper {
+ public enum UserType {
+ WEBLINK, PORTAL;
+ }
+
+ private static final Logger LOG = Logger.getLogger(F1OAuthHelper.class);
+
+ private static final String VERSION_STRING = "/v1/";
+ private static final String REQUESTTOKEN_URL = "Tokens/RequestToken";
+ private static final String AUTHORIZATION_URL = "Login";
+ private static final String ACCESSTOKEN_URL= "Tokens/AccessToken";
+ private static final String TRUSTED_ACCESSTOKEN_URL = "/AccessToken";
+
+ private final String mBaseUrl;
+ private final String mMethod;
+
+ /**
+ * @param method Either WeblinkUser or PortalUser.
+ */
+ public F1OAuthHelper(Context context, String consumerKey, String consumerSecret,
+ String baseUrl, String churchCode, UserType userType) {
+ super(context, consumerKey, consumerSecret);
+
+ switch (userType) {
+ case WEBLINK:
+ mMethod = "WeblinkUser";
+ break;
+ case PORTAL:
+ mMethod = "PortalUser";
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown UserType");
+ }
+
+ mBaseUrl = "https://" + churchCode + "." + baseUrl + VERSION_STRING;
+ }
+
+ /**
+ * @return the URL for the initial RequestToken request.
+ */
+ protected String getRequestTokenUrl() {
+ return mBaseUrl + REQUESTTOKEN_URL;
+ }
+
+ /**
+ * @return the URL to redirect the user to for Authentication.
+ */
+ public String getLoginUrl(Token requestToken, String callback) {
+ String loginUrl = mBaseUrl + mMethod + AUTHORIZATION_URL
+ + "?oauth_token=" + URLEncoder.encode(requestToken.getToken());
+
+ if (callback != null) {
+ loginUrl += "&oauth_callback=" + URLEncoder.encode(callback);
+ }
+
+ return loginUrl;
+ }
+
+
+ /**
+ * @return the URL for the AccessToken request.
+ */
+ protected String getAccessTokenUrl() {
+ return mBaseUrl + ACCESSTOKEN_URL;
+ }
+
+ /**
+ * Request an AccessToken for a particular username and password.
+ *
+ * This is an F1 extension to OAuth:
+ * http://developer.fellowshipone.com/docs/v1/Util/AuthDocs.help#2creds
+ */
+ public OAuthUser getAccessToken(String username, String password) throws OAuthException {
+ Request request = new Request(Method.POST, mBaseUrl + mMethod + TRUSTED_ACCESSTOKEN_URL);
+ request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH));
+
+ String base64String = Base64.encode((username + " " + password).getBytes(), false);
+ request.setEntity(new StringRepresentation(base64String));
+
+ return processAccessTokenRequest(request);
+ }
+
+ public void createAccount(String firstname, String lastname, String email, String redirect)
+ throws OAuthException {
+ String req = String.format("{\n\"account\":{\n\"firstName\":\"%s\",\n"
+ + "\"lastName\":\"%s\",\n\"email\":\"%s\",\n"
+ + "\"urlRedirect\":\"%s\"\n}\n}",
+ firstname, lastname, email, redirect);
+
+ Request request = new Request(Method.POST, mBaseUrl + "/Accounts");
+ request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH));
+ request.setEntity(new StringRepresentation(req));
+
+ Response response = getResponse(request);
+
+ if (!response.getStatus().isSuccess()) {
+ throw new OAuthException(response.getStatus());
+ }
+ }
+}
diff --git a/src/com/p4square/f1oauth/SecondPartyAuthenticator.java b/src/com/p4square/f1oauth/SecondPartyAuthenticator.java
new file mode 100644
index 0000000..1983d69
--- /dev/null
+++ b/src/com/p4square/f1oauth/SecondPartyAuthenticator.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.restlet.oauth.OAuthException;
+import com.p4square.restlet.oauth.OAuthUser;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Authenticator;
+
+/**
+ * Restlet Authenticator for 2nd
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SecondPartyAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(SecondPartyAuthenticator.class);
+
+ private final F1OAuthHelper mHelper;
+
+ public SecondPartyAuthenticator(Context context, boolean optional, F1OAuthHelper helper) {
+ super(context, optional);
+
+ mHelper = helper;
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ if (request.getChallengeResponse() == null) {
+ return false; // no credentials
+ }
+
+ String username = request.getChallengeResponse().getIdentifier();
+ String password = new String(request.getChallengeResponse().getSecret());
+
+ try {
+ OAuthUser user = mHelper.getAccessToken(username, password);
+ request.getClientInfo().setUser(user);
+
+ return true;
+
+ } catch (OAuthException e) {
+ LOG.info("OAuth Exception: " + e);
+ }
+
+ return false; // Invalid credentials
+ }
+}
diff --git a/src/com/p4square/f1oauth/SecondPartyVerifier.java b/src/com/p4square/f1oauth/SecondPartyVerifier.java
new file mode 100644
index 0000000..870fe3e
--- /dev/null
+++ b/src/com/p4square/f1oauth/SecondPartyVerifier.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.restlet.oauth.OAuthException;
+import com.p4square.restlet.oauth.OAuthUser;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Verifier;
+
+/**
+ * Restlet Verifier for F1 2nd Party Authentication
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SecondPartyVerifier implements Verifier {
+ private static final Logger LOG = Logger.getLogger(SecondPartyVerifier.class);
+
+ private final F1OAuthHelper mHelper;
+
+ public SecondPartyVerifier(F1OAuthHelper helper) {
+ if (helper == null) {
+ throw new IllegalArgumentException("Helper can not be null.");
+ }
+
+ mHelper = helper;
+ }
+
+ @Override
+ public int verify(Request request, Response response) {
+ if (request.getChallengeResponse() == null) {
+ return RESULT_MISSING; // no credentials
+ }
+
+ String username = request.getChallengeResponse().getIdentifier();
+ String password = new String(request.getChallengeResponse().getSecret());
+
+ try {
+ OAuthUser user = mHelper.getAccessToken(username, password);
+ user.setIdentifier(username);
+ user.setEmail(username);
+
+ // This seems like a hack... but it'll work
+ request.getClientInfo().setUser(user);
+
+ return RESULT_VALID;
+
+ } catch (OAuthException e) {
+ LOG.info("OAuth Exception: " + e, e);
+ }
+
+ return RESULT_INVALID; // Invalid credentials
+ }
+}
diff --git a/src/com/p4square/grow/frontend/ErrorPage.java b/src/com/p4square/grow/frontend/ErrorPage.java
new file mode 100644
index 0000000..4a9380f
--- /dev/null
+++ b/src/com/p4square/grow/frontend/ErrorPage.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import org.restlet.representation.StringRepresentation;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ErrorPage extends StringRepresentation {
+ public static final ErrorPage TEMPLATE_NOT_FOUND = new ErrorPage();
+ public static final ErrorPage RENDER_ERROR = new ErrorPage();
+
+
+ public ErrorPage() {
+ super("TODO");
+ }
+
+ public ErrorPage(String s) {
+ super(s);
+ }
+}
diff --git a/src/com/p4square/grow/frontend/GrowFrontend.java b/src/com/p4square/grow/frontend/GrowFrontend.java
index 5c49fe2..36e7544 100644
--- a/src/com/p4square/grow/frontend/GrowFrontend.java
+++ b/src/com/p4square/grow/frontend/GrowFrontend.java
@@ -7,20 +7,32 @@ package com.p4square.grow.frontend;
import java.io.File;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.UUID;
+
import org.restlet.Application;
import org.restlet.Component;
+import org.restlet.Client;
+import org.restlet.Context;
import org.restlet.Restlet;
import org.restlet.data.Protocol;
import org.restlet.resource.Directory;
import org.restlet.routing.Router;
+import org.restlet.security.Authenticator;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.FMFacade;
-import net.jesterpm.fmfacade.FreeMarkerPageResource;
+import com.p4square.fmfacade.FMFacade;
+import com.p4square.fmfacade.FreeMarkerPageResource;
import com.p4square.grow.config.Config;
+import com.p4square.f1oauth.F1OAuthHelper;
+import com.p4square.f1oauth.SecondPartyVerifier;
+
+import com.p4square.grow.frontend.session.SessionCheckingAuthenticator;
+import com.p4square.grow.frontend.session.SessionCreatingAuthenticator;
+
/**
* This is the Restlet Application implementing the Grow project front-end.
* It's implemented as an extension of FMFacade that connects interactive pages
@@ -34,6 +46,8 @@ public class GrowFrontend extends FMFacade {
private Config mConfig;
+ private F1OAuthHelper mHelper;
+
public GrowFrontend() {
mConfig = new Config();
}
@@ -62,14 +76,23 @@ public class GrowFrontend extends FMFacade {
}
}
+ F1OAuthHelper getHelper() {
+ if (mHelper == null) {
+ mHelper = new F1OAuthHelper(getContext(), mConfig.getString("f1ConsumerKey", ""),
+ mConfig.getString("f1ConsumerSecret", ""),
+ mConfig.getString("f1BaseUrl", "staging.fellowshiponeapi.com"),
+ mConfig.getString("f1ChurchCode", "pfseawa"),
+ F1OAuthHelper.UserType.WEBLINK);
+ }
+
+ return mHelper;
+ }
+
@Override
protected Router createRouter() {
Router router = new Router(getContext());
- final String loginPage = getConfig().getString("dynamicRoot", "") + "/login.html";
-
- final LoginAuthenticator defaultGuard =
- new LoginAuthenticator(getContext(), true, loginPage);
+ final Authenticator defaultGuard = new SessionCheckingAuthenticator(getContext(), true);
defaultGuard.setNext(FreeMarkerPageResource.class);
router.attachDefault(defaultGuard);
router.attach("/login.html", LoginPageResource.class);
@@ -81,14 +104,36 @@ public class GrowFrontend extends FMFacade {
accountRouter.attach("/training/{chapter}", TrainingPageResource.class);
accountRouter.attach("/training", TrainingPageResource.class);
- final LoginAuthenticator accountGuard =
- new LoginAuthenticator(getContext(), false, loginPage);
- accountGuard.setNext(accountRouter);
+ final Authenticator accountGuard = createAuthenticatorChain(accountRouter);
router.attach("/account", accountGuard);
return router;
}
+ private Authenticator createAuthenticatorChain(Restlet last) {
+ final Context context = getContext();
+ final String loginPage = getConfig().getString("dynamicRoot", "") + "/login.html";
+
+ // This is used to check for an existing session
+ SessionCheckingAuthenticator sessionChk = new SessionCheckingAuthenticator(context, true);
+
+ // This is used to authenticate the user
+ SecondPartyVerifier f1Verifier = new SecondPartyVerifier(getHelper());
+ LoginFormAuthenticator loginAuth = new LoginFormAuthenticator(context, false, f1Verifier);
+ loginAuth.setLoginFormUrl(loginPage);
+ loginAuth.setLoginPostUrl("/account/authenticate");
+
+ // This is used to create a new session for a newly authenticated user.
+ SessionCreatingAuthenticator sessionCreate = new SessionCreatingAuthenticator(context);
+
+ sessionChk.setNext(loginAuth);
+ loginAuth.setNext(sessionCreate);
+
+ sessionCreate.setNext(last);
+
+ return sessionChk;
+ }
+
/**
* Stand-alone main for testing.
*/
@@ -98,14 +143,16 @@ public class GrowFrontend extends FMFacade {
component.getServers().add(Protocol.HTTP, 8085);
component.getClients().add(Protocol.HTTP);
component.getClients().add(Protocol.FILE);
-
+ component.getClients().add(new Client(null, Arrays.asList(Protocol.HTTPS), "org.restlet.ext.httpclient.HttpClientHelper"));
+
// Static content
try {
component.getDefaultHost().attach("/images/", new FileServingApp("./build/images/"));
component.getDefaultHost().attach("/scripts", new FileServingApp("./build/scripts"));
component.getDefaultHost().attach("/style.css", new FileServingApp("./build/style.css"));
+ component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./build/favicon.ico"));
} catch (IOException e) {
- cLog.error("Could not create directory for static resources: "
+ cLog.error("Could not create directory for static resources: "
+ e.getMessage(), e);
}
@@ -139,7 +186,7 @@ public class GrowFrontend extends FMFacade {
cLog.fatal("Could not start: " + e.getMessage(), e);
}
}
-
+
private static class FileServingApp extends Application {
private final String mPath;
diff --git a/src/com/p4square/grow/frontend/LoginFormAuthenticator.java b/src/com/p4square/grow/frontend/LoginFormAuthenticator.java
new file mode 100644
index 0000000..d5a3c22
--- /dev/null
+++ b/src/com/p4square/grow/frontend/LoginFormAuthenticator.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Form;
+import org.restlet.data.Reference;
+import org.restlet.security.Authenticator;
+import org.restlet.security.Verifier;
+
+/**
+ * LoginFormAuthenticator changes
+ *
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class LoginFormAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(LoginFormAuthenticator.class);
+
+ private final Verifier mVerifier;
+
+ private String mLoginPage = "/login.html";
+ private String mLoginPostUrl = "/authenticate";
+ private String mDefaultRedirect = "/index.html";
+
+ public LoginFormAuthenticator(Context context, boolean optional, Verifier verifier) {
+ super(context, false, optional, null);
+
+ mVerifier = verifier;
+ }
+
+ public void setLoginFormUrl(String url) {
+ mLoginPage = url;
+ }
+
+ public void setLoginPostUrl(String url) {
+ mLoginPostUrl = url;
+ }
+
+ @Override
+ protected int beforeHandle(Request request, Response response) {
+ if (request.getClientInfo().isAuthenticated()) {
+ // TODO: Logout
+ LOG.debug("Already authenticated. Skipping");
+ return CONTINUE;
+
+ } else {
+ return super.beforeHandle(request, response);
+ }
+ }
+
+
+ @Override
+ protected boolean authenticate(Request request, Response response) {
+ String requestPath = request.getResourceRef().getPath();
+ boolean isLoginAttempt = mLoginPostUrl.equals(requestPath);
+
+ Form query = request.getOriginalRef().getQueryAsForm();
+ String redirect = query.getFirstValue("redirect");
+ if (redirect == null) {
+ if (isLoginAttempt) {
+ redirect = mDefaultRedirect;
+ } else {
+ redirect = request.getResourceRef().getRelativePart();
+ }
+ }
+
+ boolean authenticationFailed = false;
+
+ if (isLoginAttempt) {
+ LOG.debug("Attempting authentication");
+
+ // Process login form
+ final Form form = new Form(request.getEntity());
+ final String email = form.getFirstValue("email");
+ final String password = form.getFirstValue("password");
+
+ boolean authenticated = false;
+
+ if (email != null && !"".equals(email) &&
+ password != null && !"".equals(password)) {
+
+ LOG.debug("Got login request from " + email);
+
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, email, password.toCharArray()));
+
+ // We expect the verifier to setup the User object.
+ int result = mVerifier.verify(request, response);
+ if (result == Verifier.RESULT_VALID) {
+ // TODO: Ensure redirect is a relative url.
+ response.redirectSeeOther(redirect);
+ return true;
+ }
+ }
+
+ authenticationFailed = true;
+ }
+
+ if (!isOptional() || authenticationFailed) {
+ Reference ref = new Reference(mLoginPage);
+ ref.addQueryParameter("redirect", redirect);
+
+ if (authenticationFailed) {
+ ref.addQueryParameter("retry", "t");
+ }
+
+ LOG.debug("Redirecting to " + ref.toString());
+ response.redirectSeeOther(ref.toString());
+ }
+ LOG.debug("Failing authentication.");
+ return false;
+ }
+}
diff --git a/src/com/p4square/grow/frontend/LoginPageResource.java b/src/com/p4square/grow/frontend/LoginPageResource.java
index 70caa3e..e645c1b 100644
--- a/src/com/p4square/grow/frontend/LoginPageResource.java
+++ b/src/com/p4square/grow/frontend/LoginPageResource.java
@@ -17,7 +17,7 @@ import org.restlet.ext.freemarker.TemplateRepresentation;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.FreeMarkerPageResource;
+import com.p4square.fmfacade.FreeMarkerPageResource;
/**
* LoginPageResource presents a login page template and processes the response.
@@ -57,7 +57,11 @@ public class LoginPageResource extends FreeMarkerPageResource {
Map<String, Object> root = getRootObject();
- root.put("errorMessage", mErrorMessage);
+ Form query = getRequest().getOriginalRef().getQueryAsForm();
+ String retry = query.getFirstValue("retry");
+ if ("t".equals("retry")) {
+ root.put("errorMessage", "Invalid email or password.");
+ }
return new TemplateRepresentation(t, root, MediaType.TEXT_HTML);
@@ -68,36 +72,4 @@ public class LoginPageResource extends FreeMarkerPageResource {
}
}
- /**
- * Process login and authenticate the user.
- */
- @Override
- protected Representation post(Representation entity) {
- final Form form = new Form(entity);
- final String email = form.getFirstValue("email");
- final String password = form.getFirstValue("password");
-
- boolean authenticated = false;
-
- // TODO: Do something real here
- if (email != null && !"".equals(email)) {
- cLog.debug("Got login request from " + email);
-
- // TODO: Encrypt user info
- getResponse().getCookieSettings().add(LoginAuthenticator.COOKIE_NAME, email);
-
- authenticated = true;
- }
-
- if (authenticated) {
- // TODO: Better return url.
- getResponse().redirectSeeOther(mGrowFrontend.getConfig().getString("dynamicRoot", "") + "/index.html");
- return null;
-
- } else {
- // Send them back to the login page...
- mErrorMessage = "Incorrect Email or Password.";
- return get();
- }
- }
}
diff --git a/src/com/p4square/grow/frontend/NewAccountResource.java b/src/com/p4square/grow/frontend/NewAccountResource.java
new file mode 100644
index 0000000..b72680a
--- /dev/null
+++ b/src/com/p4square/grow/frontend/NewAccountResource.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.Map;
+
+import freemarker.template.Template;
+
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.resource.ServerResource;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.f1oauth.F1OAuthHelper;
+import com.p4square.restlet.oauth.OAuthException;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+/**
+ * This resource creates a new InFellowship account.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class NewAccountResource extends FreeMarkerPageResource {
+ private static Logger LOG = Logger.getLogger(NewAccountResource.class);
+
+ private GrowFrontend mGrowFrontend;
+ private F1OAuthHelper mHelper;
+
+ private String mErrorMessage;
+
+ private String mLoginPageUrl;
+ private String mVerificationPage;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mGrowFrontend = (GrowFrontend) getApplication();
+ mHelper = mGrowFrontend.getHelper();
+
+ mErrorMessage = null;
+
+ mLoginPageUrl = "";
+ mVerificationPage = "";
+ }
+
+ /**
+ * Return the login page.
+ */
+ @Override
+ protected Representation get() {
+ Template t = mGrowFrontend.getTemplate("pages/newaccount.html.ftl");
+
+ try {
+ if (t == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return ErrorPage.TEMPLATE_NOT_FOUND;
+ }
+
+ Map<String, Object> root = getRootObject();
+ root.put("errorMessage", mErrorMessage);
+
+ return new TemplateRepresentation(t, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ @Override
+ protected Representation post(Representation rep) {
+ Form form = new Form(rep);
+
+ String firstname = form.getFirstValue("firstname");
+ String lastname = form.getFirstValue("lastname");
+ String email = form.getFirstValue("email");
+
+ if (isEmpty(firstname)) {
+ mErrorMessage += "First Name is a required field. ";
+ }
+ if (isEmpty(lastname)) {
+ mErrorMessage += "Last Name is a required field. ";
+ }
+ if (isEmpty(email)) {
+ mErrorMessage += "Email is a required field. ";
+ }
+
+ if (mErrorMessage.length() > 0) {
+ return get();
+ }
+
+ try {
+ mHelper.createAccount(firstname, lastname, email, mLoginPageUrl);
+ getResponse().redirectSeeOther(mVerificationPage);
+ return new StringRepresentation("Redirecting to " + mVerificationPage);
+
+ } catch (OAuthException e) {
+ return new ErrorPage(e.getStatus().getDescription());
+ }
+ }
+
+ private boolean isEmpty(String s) {
+ return s != null && s.trim().length() > 0;
+ }
+}
diff --git a/src/com/p4square/grow/frontend/SurveyPageResource.java b/src/com/p4square/grow/frontend/SurveyPageResource.java
index 351eade..8a3b5a5 100644
--- a/src/com/p4square/grow/frontend/SurveyPageResource.java
+++ b/src/com/p4square/grow/frontend/SurveyPageResource.java
@@ -18,10 +18,10 @@ import org.restlet.resource.ServerResource;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.json.JsonRequestClient;
-import net.jesterpm.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
-import net.jesterpm.fmfacade.FreeMarkerPageResource;
+import com.p4square.fmfacade.FreeMarkerPageResource;
import com.p4square.grow.config.Config;
diff --git a/src/com/p4square/grow/frontend/TrainingPageResource.java b/src/com/p4square/grow/frontend/TrainingPageResource.java
index 6c89ac9..459eb9a 100644
--- a/src/com/p4square/grow/frontend/TrainingPageResource.java
+++ b/src/com/p4square/grow/frontend/TrainingPageResource.java
@@ -20,10 +20,10 @@ import org.restlet.resource.ServerResource;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.json.JsonRequestClient;
-import net.jesterpm.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
-import net.jesterpm.fmfacade.FreeMarkerPageResource;
+import com.p4square.fmfacade.FreeMarkerPageResource;
import com.p4square.grow.config.Config;
diff --git a/src/com/p4square/grow/frontend/VideosResource.java b/src/com/p4square/grow/frontend/VideosResource.java
index fed315b..cdb2fb4 100644
--- a/src/com/p4square/grow/frontend/VideosResource.java
+++ b/src/com/p4square/grow/frontend/VideosResource.java
@@ -17,8 +17,8 @@ import org.restlet.resource.ServerResource;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.json.JsonRequestClient;
-import net.jesterpm.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
import com.p4square.grow.config.Config;
diff --git a/src/com/p4square/grow/frontend/session/Session.java b/src/com/p4square/grow/frontend/session/Session.java
new file mode 100644
index 0000000..3a241ef
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/Session.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.restlet.security.User;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Session {
+ private static final long LIFETIME = 86400;
+
+ private final String mSessionId;
+ private final User mUser;
+ private final Map<String, String> mData;
+ private long mExpires;
+
+ Session(User user) {
+ mUser = user;
+ mSessionId = UUID.randomUUID().toString();
+ mExpires = System.currentTimeMillis() + LIFETIME;
+ mData = new HashMap<String, String>();
+ }
+
+ void touch() {
+ mExpires = System.currentTimeMillis() + LIFETIME;
+ }
+
+ boolean isExpired() {
+ return System.currentTimeMillis() > mExpires;
+ }
+
+ public String getId() {
+ return mSessionId;
+ }
+
+ public String get(String key) {
+ return mData.get(key);
+ }
+
+ public void put(String key, String value) {
+ mData.put(key, value);
+ }
+
+ public User getUser() {
+ return mUser;
+ }
+}
diff --git a/src/com/p4square/grow/frontend/LoginAuthenticator.java b/src/com/p4square/grow/frontend/session/SessionAuthenticator.java
index 64f5827..ac194af 100644
--- a/src/com/p4square/grow/frontend/LoginAuthenticator.java
+++ b/src/com/p4square/grow/frontend/session/SessionAuthenticator.java
@@ -2,9 +2,7 @@
* Copyright 2013 Jesse Morgan
*/
-package com.p4square.grow.frontend;
-
-import org.apache.log4j.Logger;
+package com.p4square.grow.frontend.session;
import org.restlet.Context;
import org.restlet.Request;
@@ -13,27 +11,12 @@ import org.restlet.security.Authenticator;
import org.restlet.security.User;
/**
- * LoginAuthenticator decrypts a cookie containing the user's session info
- * and makes that information available as the ClientInfo's User object.
- *
- * If this Authenticator is not optional, the user will be redirected to a
- * login page.
- *
+ *
* @author Jesse Morgan <jesse@jesterpm.net>
*/
-public class LoginAuthenticator extends Authenticator {
- private static Logger cLog = Logger.getLogger(LoginAuthenticator.class);
-
- public static final String COOKIE_NAME = "growsession";
-
- private final String mLoginPage;
-
- public LoginAuthenticator(Context context, boolean optional, String loginPage) {
- super(context, optional);
-
- mLoginPage = loginPage;
- }
-
+public class SessionAuthenticator /*extends Authenticator*/ {
+ /*
+ @Override
protected boolean authenticate(Request request, Response response) {
// Check for authentication cookie
final String cookie = request.getCookies().getFirstValue(COOKIE_NAME);
@@ -49,4 +32,5 @@ public class LoginAuthenticator extends Authenticator {
response.redirectSeeOther(mLoginPage);
return false;
}
+ */
}
diff --git a/src/com/p4square/grow/frontend/session/SessionCheckingAuthenticator.java b/src/com/p4square/grow/frontend/session/SessionCheckingAuthenticator.java
new file mode 100644
index 0000000..8382aff
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/SessionCheckingAuthenticator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Authenticator;
+
+/**
+ * Authenticator which succeeds if a valid Session exists.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SessionCheckingAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(SessionCheckingAuthenticator.class);
+
+ public SessionCheckingAuthenticator(Context context, boolean optional) {
+ super(context, optional);
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ Session s = Sessions.getInstance().get(request);
+
+ if (s != null) {
+ request.getClientInfo().setUser(s.getUser());
+ return true;
+
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/src/com/p4square/grow/frontend/session/SessionCookieAuthenticator.java b/src/com/p4square/grow/frontend/session/SessionCookieAuthenticator.java
new file mode 100644
index 0000000..789f58e
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/SessionCookieAuthenticator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Authenticator;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SessionCookieAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(SessionCookieAuthenticator.class);
+
+ private static final String COOKIE_NAME = "S";
+
+ private final Sessions mSessions;
+
+ public SessionCookieAuthenticator(Context context, boolean optional, Sessions sessions) {
+ super(context, optional);
+
+ mSessions = sessions;
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ final String cookie = request.getCookies().getFirstValue(COOKIE_NAME);
+
+ if (request.getClientInfo().isAuthenticated()) {
+ // Request is already authenticated... create session if it doesn't exist.
+ if (cookie == null) {
+ Session s = mSessions.create(request.getClientInfo().getUser());
+ response.getCookieSettings().add(COOKIE_NAME, s.getId());
+ }
+
+ return true;
+
+ } else {
+ // Check for authentication cookie
+ if (cookie != null) {
+ LOG.debug("Got cookie: " + cookie);
+
+ Session s = mSessions.get(cookie);
+ if (s != null) {
+ request.getClientInfo().setUser(s.getUser());
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+}
diff --git a/src/com/p4square/grow/frontend/session/SessionCreatingAuthenticator.java b/src/com/p4square/grow/frontend/session/SessionCreatingAuthenticator.java
new file mode 100644
index 0000000..ce6024c
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/SessionCreatingAuthenticator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Authenticator;
+import org.restlet.security.User;
+
+/**
+ * Authenticator which creates a Session for the request and adds a cookie
+ * to the response.
+ *
+ * The Request MUST be Authenticated and MUST have a User object associated.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SessionCreatingAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(SessionCreatingAuthenticator.class);
+
+ public SessionCreatingAuthenticator(Context context) {
+ super(context, true);
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ if (Sessions.getInstance().get(request) != null) {
+ return true;
+ }
+
+ User user = request.getClientInfo().getUser();
+
+ if (request.getClientInfo().isAuthenticated() && user != null) {
+ Sessions.getInstance().create(request, response);
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/com/p4square/grow/frontend/session/Sessions.java b/src/com/p4square/grow/frontend/session/Sessions.java
new file mode 100644
index 0000000..094d2f0
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/Sessions.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Map;
+
+import org.restlet.Response;
+import org.restlet.Request;
+import org.restlet.security.User;
+
+/**
+ * Singleton Session Manager.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Sessions {
+ private static final String COOKIE_NAME = "S";
+
+ private static final Sessions THE = new Sessions();
+ public static Sessions getInstance() {
+ return THE;
+ }
+
+ private final Map<String, Session> mSessions;
+
+ private Sessions() {
+ mSessions = new ConcurrentHashMap<String, Session>();
+ }
+
+ public Session get(String sessionid) {
+ Session s = mSessions.get(sessionid);
+
+ if (s != null && !s.isExpired()) {
+ s.touch();
+ return s;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the Session associated with the Request.
+ * @return A session or null if no session is found.
+ */
+ public Session get(Request request) {
+ final String cookie = request.getCookies().getFirstValue(COOKIE_NAME);
+
+ if (cookie != null) {
+ return get(cookie);
+ }
+
+ return null;
+ }
+
+ public Session create(User user) {
+ if (user == null) {
+ throw new IllegalArgumentException("Can not create session for null user.");
+ }
+
+ Session s = new Session(user);
+ mSessions.put(s.getId(), s);
+
+ return s;
+ }
+
+ /**
+ * Create a new Session and add the Session cookie to the response.
+ */
+ public Session create(Request request, Response response) {
+ Session s = create(request.getClientInfo().getUser());
+
+ request.getCookies().add(COOKIE_NAME, s.getId());
+ response.getCookieSettings().add(COOKIE_NAME, s.getId());
+
+ return s;
+ }
+}
diff --git a/src/com/p4square/restlet/oauth/OAuthAuthenticator.java b/src/com/p4square/restlet/oauth/OAuthAuthenticator.java
new file mode 100644
index 0000000..c33bb5a
--- /dev/null
+++ b/src/com/p4square/restlet/oauth/OAuthAuthenticator.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.restlet.oauth;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Authenticator;
+import org.restlet.security.User;
+
+/**
+ * Authenticator which makes an OAuth request to authenticate the user.
+ *
+ * If this Authenticator is made optional than no requests are made to the
+ * service provider.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class OAuthAuthenticator extends Authenticator {
+ private static Logger LOG = Logger.getLogger(OAuthAuthenticator.class);
+
+ private static final String OAUTH_TOKEN = "oauth_token";
+ private static final String COOKIE_NAME = "oauth_secret";
+
+ private final OAuthHelper mHelper;
+
+ /**
+ * Create a new Authenticator.
+ *
+ * @param Context the current context.
+ * @param optional If true, unauthenticated users are allowed to continue.
+ * @param helper The OAuthHelper which will help with the requests.
+ */
+ public OAuthAuthenticator(Context context, boolean optional, OAuthHelper helper) {
+ super(context, false, optional, null);
+
+ mHelper = helper;
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ /*
+ * The authentication workflow has three steps:
+ * 1. Get RequestToken
+ * 2. Authenticate the user
+ * 3. Get AccessToken
+ *
+ * The authentication workflow is broken into two stages. In the first,
+ * we generate the RequestToken (step 1) and redirect the user to the
+ * authentication page. When the user comes back, we will request the
+ * AccessToken (step 2).
+ *
+ * We determine which half we are in by the presence of the oauth_token
+ * parameter in the query string.
+ */
+
+ final String token = request.getResourceRef().getQueryAsForm().getFirstValue(OAUTH_TOKEN);
+ final String secret = request.getCookies().getFirstValue(COOKIE_NAME);
+
+ try {
+ if (token == null) {
+ if (isOptional()) {
+ return false;
+ }
+
+ // 1. Get RequestToken
+ Token requestToken = mHelper.getRequestToken();
+
+ if (requestToken == null) {
+ return false;
+ }
+
+ // 2. Redirect user
+ // TODO Encrypt cookie
+ response.getCookieSettings().add(COOKIE_NAME, requestToken.getSecret());
+ response.redirectSeeOther(mHelper.getLoginUrl(requestToken, request.getResourceRef().toString()));
+ return false;
+
+ } else {
+ // 3. Get AccessToken
+ Token requestToken = new Token(token, secret);
+ User user = mHelper.getAccessToken(requestToken);
+ request.getClientInfo().setUser(user);
+ return true;
+ }
+
+ } catch (OAuthException e) {
+ LOG.debug("Authentication failed: " + e);
+ return false;
+ }
+ }
+}
diff --git a/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java b/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java
new file mode 100644
index 0000000..76ff044
--- /dev/null
+++ b/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.restlet.oauth;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import java.net.URLEncoder;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import java.util.Collections;
+import java.util.Random;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeRequest;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.Form;
+import org.restlet.data.Method;
+import org.restlet.data.Parameter;
+import org.restlet.data.Reference;
+import org.restlet.engine.header.ChallengeWriter;
+import org.restlet.engine.header.Header;
+import org.restlet.engine.security.AuthenticatorHelper;
+import org.restlet.engine.util.Base64;
+import org.restlet.util.Series;
+
+/**
+ * Authentication helper for signing OAuth Requests.
+ *
+ * This implementation is limited to one consumer token/secret per restlet
+ * engine. In practice this means you will only be able to interact with one
+ * service provider unless you loaded/unloaded the AuthenticationHelper for
+ * each request.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class OAuthAuthenticatorHelper extends AuthenticatorHelper {
+ private static final String SIGNATURE_METHOD = "HMAC-SHA1";
+ private static final String JAVA_SIGNATURE_METHOD = "HmacSHA1";
+ private static final String ENCODING = "UTF-8";
+
+ private final Random mRandom;
+ private final Token mConsumerToken;
+
+ /**
+ * Package-private constructor.
+ *
+ * This class should only be instantiated by OAuthHelper.
+ */
+ OAuthAuthenticatorHelper(Token consumerToken) {
+ super(ChallengeScheme.HTTP_OAUTH, true, false);
+
+ mRandom = new Random();
+ mConsumerToken = consumerToken;
+ }
+
+ @Override
+ public void formatRequest(ChallengeWriter cw, ChallengeRequest cr,
+ Response response, Series<Header> httpHeaders) throws IOException {
+
+ throw new UnsupportedOperationException("OAuth Requests are not implemented");
+ }
+
+ @Override
+ public void formatResponse(ChallengeWriter cw, ChallengeResponse response,
+ Request request, Series<Header> httpHeaders) {
+
+ try {
+ Series<Parameter> authParams = new Series<Parameter>(Parameter.class);
+
+ String nonce = String.valueOf(mRandom.nextInt());
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+
+ authParams.add(new Parameter("oauth_consumer_key", mConsumerToken.getToken()));
+ authParams.add(new Parameter("oauth_nonce", nonce));
+ authParams.add(new Parameter("oauth_signature_method", SIGNATURE_METHOD));
+ authParams.add(new Parameter("oauth_timestamp", timestamp));
+ authParams.add(new Parameter("oauth_version", "1.0"));
+
+ String accessToken = response.getIdentifier();
+ if (accessToken != null) {
+ authParams.add(new Parameter("oauth_token", accessToken));
+ }
+
+ // Generate Signature
+ String signature = generateSignature(response, request, authParams);
+ authParams.add(new Parameter("oauth_signature", signature));
+
+ // Write Header
+ for (Parameter p : authParams) {
+ cw.appendQuotedChallengeParameter(encode(p.getName()), encode(p.getValue()));
+ }
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+
+ } catch (InvalidKeyException e) {
+ throw new RuntimeException(e);
+
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Helper method to generate an OAuth Signature.
+ */
+ private String generateSignature(ChallengeResponse response, Request request,
+ Series<Parameter> authParams)
+ throws NoSuchAlgorithmException, InvalidKeyException, IOException,
+ UnsupportedEncodingException {
+
+ // HTTP Request Method
+ String httpMethod = request.getMethod().getName();
+
+ // Request Url
+ Reference url = request.getResourceRef();
+ String requestUrl = encode(url.getScheme() + ":" + url.getHierarchicalPart());
+
+ // Normalized parameters
+ Series<Parameter> params = new Series<Parameter>(Parameter.class);
+
+ // OAUTH Params
+ params.addAll(authParams);
+
+ // Query Params
+ Form query = url.getQueryAsForm();
+ params.addAll(query);
+
+ // Sort it
+ Collections.sort(params);
+
+ StringBuilder normalizedParamsBuilder = new StringBuilder();
+ for (Parameter p : params) {
+ normalizedParamsBuilder.append('&');
+ normalizedParamsBuilder.append(p.encode(CharacterSet.UTF_8));
+ }
+ String normalizedParams = encode(normalizedParamsBuilder.substring(1)); // remove the first &
+
+ // Generate signature base
+ String sigBase = httpMethod + "&" + requestUrl + "&" + normalizedParams.toString();
+
+ // Sign the signature base
+ Mac mac = Mac.getInstance(JAVA_SIGNATURE_METHOD);
+
+ String accessTokenSecret = "";
+ if (response.getIdentifier() != null) {
+ accessTokenSecret = new String(response.getSecret());
+ }
+
+ byte[] keyBytes = (encode(mConsumerToken.getSecret()) + "&" + encode(accessTokenSecret)).getBytes(ENCODING);
+ SecretKey key = new SecretKeySpec(keyBytes, JAVA_SIGNATURE_METHOD);
+ mac.init(key);
+
+ byte[] signature = mac.doFinal(sigBase.getBytes(ENCODING));
+
+ return Base64.encode(signature, false).trim();
+ }
+
+ /**
+ * Helper method to URL Encode Strings.
+ */
+ private String encode(String input) throws UnsupportedEncodingException {
+ return URLEncoder.encode(input, ENCODING);
+ }
+}
diff --git a/src/com/p4square/restlet/oauth/OAuthException.java b/src/com/p4square/restlet/oauth/OAuthException.java
new file mode 100644
index 0000000..dd326d3
--- /dev/null
+++ b/src/com/p4square/restlet/oauth/OAuthException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.restlet.oauth;
+
+import org.restlet.data.Status;
+
+/**
+ * Exception throw when the service provider returns an error.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class OAuthException extends Exception {
+ private final Status mStatus;
+
+ public OAuthException(Status status) {
+ super("Service provider failed request: " + status.getDescription());
+ mStatus = status;
+ }
+
+ public Status getStatus() {
+ return mStatus;
+ }
+}
diff --git a/src/com/p4square/restlet/oauth/OAuthHelper.java b/src/com/p4square/restlet/oauth/OAuthHelper.java
new file mode 100644
index 0000000..544e4e3
--- /dev/null
+++ b/src/com/p4square/restlet/oauth/OAuthHelper.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.restlet.oauth;
+
+import java.net.URLEncoder;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Form;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+import org.restlet.engine.Engine;
+
+/**
+ * Helper Class for OAuth 1.0 Authentication.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public abstract class OAuthHelper {
+ private final Restlet mDispatcher;
+ private final Token mConsumerToken;
+
+ /**
+ * Create a new OAuth Helper.
+ * As currently implemented, there can only be one OAuthHelper per Restlet
+ * Engine since this class registers its own provider for the OAuth
+ * authentication protocol.
+ *
+ * FIXME: This could be improved by making OAuthAuthenticationHelper and
+ * maybe Token aware of multiple service providers.
+ *
+ * @param context The restlet context which provides a ClientDispatcher.
+ * @param consumerKey The OAuth consumer key for this application.
+ * @param consumerSecret the OAuth consumer secret for this application.
+ */
+ public OAuthHelper(Context context, String consumerKey, String consumerSecret) {
+ mDispatcher = context.getClientDispatcher();
+ mConsumerToken = new Token(consumerKey, consumerSecret);
+
+ Engine.getInstance().getRegisteredAuthenticators().add(new OAuthAuthenticatorHelper(mConsumerToken));
+ }
+
+ /**
+ * @return the URL for the initial RequestToken request.
+ */
+ protected abstract String getRequestTokenUrl();
+
+ /**
+ * Request a RequestToken.
+ *
+ * @return a Token containing the RequestToken.
+ * @throws OAuthException if the request fails.
+ */
+ public Token getRequestToken() throws OAuthException {
+ Request request = new Request(Method.GET, getRequestTokenUrl());
+ request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH));
+
+ Response response = mDispatcher.handle(request);
+
+ return processTokenRequest(response);
+ }
+
+ /**
+ * @return the URL to redirect the user to for Authentication.
+ */
+ public abstract String getLoginUrl(Token requestToken, String callback);
+
+ /**
+ * @return the URL for the AccessToken request.
+ */
+ protected abstract String getAccessTokenUrl();
+
+ /**
+ * Request an AccessToken for a previously authenticated RequestToken.
+ *
+ * @return an OAuthUser object containing the AccessToken.
+ * @throws OAuthException if the request fails.
+ */
+ public OAuthUser getAccessToken(Token requestToken) throws OAuthException {
+ Request request = new Request(Method.GET, getAccessTokenUrl());
+ request.setChallengeResponse(requestToken.getChallengeResponse());
+
+ return processAccessTokenRequest(request);
+ }
+
+ /**
+ * Helper method to decode the token returned from an OAuth Request.
+ *
+ * @param response The Response object from the Request.
+ * @return the Token from the oauth_token and oauth_token_secret parameters.
+ * @throws OAuthException is the server reported an error.
+ */
+ protected Token processTokenRequest(Response response) throws OAuthException {
+ Status status = response.getStatus();
+
+ if (status.isSuccess()) {
+ Form form = new Form(response.getEntity());
+ String token = form.getFirstValue("oauth_token");
+ String secret = form.getFirstValue("oauth_token_secret");
+
+ return new Token(token, secret);
+
+ } else {
+ throw new OAuthException(status);
+ }
+ }
+
+ /**
+ * Helper method to create an OAuthUser from the AccessToken request.
+ *
+ * The User's identifier is set to the Content-Location header, if present.
+ *
+ * @param response The Response to the AccessToken Request.
+ * @return An OAuthUser object wrapping the AccessToken.
+ * @throws OAuthException if the request failed.
+ */
+ protected OAuthUser processAccessTokenRequest(Request request) throws OAuthException {
+ Response response = getResponse(request);
+ Token accessToken = processTokenRequest(response);
+
+ Reference ref = response.getEntity().getLocationRef();
+ if (ref != null) {
+ return new OAuthUser(ref.toString(), accessToken);
+
+ } else {
+ return new OAuthUser(accessToken);
+ }
+ }
+
+ /**
+ * Helper method to get a Response for a Request.
+ */
+ protected Response getResponse(Request request) {
+ return mDispatcher.handle(request);
+ }
+}
diff --git a/src/com/p4square/restlet/oauth/OAuthUser.java b/src/com/p4square/restlet/oauth/OAuthUser.java
new file mode 100644
index 0000000..11dbac1
--- /dev/null
+++ b/src/com/p4square/restlet/oauth/OAuthUser.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.restlet.oauth;
+
+import org.restlet.data.ChallengeResponse;
+import org.restlet.security.User;
+
+/**
+ * Simple User object which also contains an OAuth AccessToken.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class OAuthUser extends User {
+ private final Token mToken;
+ private final String mContentLocation;
+
+ public OAuthUser(Token token) {
+ this(null, token);
+ }
+
+ public OAuthUser(String location, Token token) {
+ super();
+ mToken = token;
+ mContentLocation = location;
+ }
+
+ /**
+ * @return the Location associated with the user.
+ */
+ public String getLocation() {
+ return mContentLocation;
+ }
+
+ /**
+ * @return The AccessToken.
+ */
+ public Token getToken() {
+ return mToken;
+ }
+
+ /**
+ * Convenience method for getToken().getChallengeResponse().
+ * @return A ChallengeResponse based upon the access token.
+ */
+ public ChallengeResponse getChallengeResponse() {
+ return mToken.getChallengeResponse();
+ }
+}
diff --git a/src/com/p4square/restlet/oauth/Token.java b/src/com/p4square/restlet/oauth/Token.java
new file mode 100644
index 0000000..51a9087
--- /dev/null
+++ b/src/com/p4square/restlet/oauth/Token.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.restlet.oauth;
+
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+
+/**
+ * Token wraps the two Strings which make up an OAuth Token: the public
+ * component and the private component.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Token {
+ private final String mToken;
+ private final String mSecret;
+
+ public Token(String token, String secret) {
+ mToken = token;
+ mSecret = secret;
+ }
+
+ /**
+ * @return the public component.
+ */
+ public String getToken() {
+ return mToken;
+ }
+
+ /**
+ * @return the secret component.
+ */
+ public String getSecret() {
+ return mSecret;
+ }
+
+ @Override
+ public String toString() {
+ return mToken + "&" + mSecret;
+ }
+
+ /**
+ * Generate a ChallengeResponse based on this Token.
+ *
+ * @return a ChallengeResponse object using the OAUTH ChallengeScheme.
+ */
+ public ChallengeResponse getChallengeResponse() {
+ return new ChallengeResponse(ChallengeScheme.HTTP_OAUTH, mToken, mSecret);
+ }
+}
diff --git a/src/grow.properties b/src/grow.properties
index 1f76237..88bee42 100644
--- a/src/grow.properties
+++ b/src/grow.properties
@@ -7,6 +7,11 @@ dev.dynamicRoot =
*.staticRoot = /grow-frontend
*.dynamicRoot = /grow-frontend
+*.f1ConsumerKey = 123
+*.f1ConsumerSecret = password-here
+*.f1BaseUrl = staging.fellowshiponeapi.com
+*.f1ChurchCode = pfseawa
+
# Backend Settings
dev.clusterName = Dev Cluster
diff --git a/src/templates/macros/common.ftl b/src/templates/macros/common.ftl
index 513fc57..966afdb 100644
--- a/src/templates/macros/common.ftl
+++ b/src/templates/macros/common.ftl
@@ -1,4 +1,5 @@
<#include "content.ftl">
+<#include "textcontent.ftl">
<#include "noticebox.ftl">
<#assign dynamicRoot = "">
diff --git a/src/templates/macros/textcontent.ftl b/src/templates/macros/textcontent.ftl
new file mode 100644
index 0000000..408c05c
--- /dev/null
+++ b/src/templates/macros/textcontent.ftl
@@ -0,0 +1,8 @@
+<#macro textcontent>
+ <div id="content">
+ <article class="text">
+ <#nested>
+ </article>
+ </div>
+</#macro>
+
diff --git a/src/templates/pages/about.html.ftl b/src/templates/pages/about.html.ftl
index 3ab2bc0..63cd366 100644
--- a/src/templates/pages/about.html.ftl
+++ b/src/templates/pages/about.html.ftl
@@ -3,22 +3,69 @@
<@commonpage>
<@noticebox>
- The Grow Process focuses on the topic that you want to learn
- about. Out 'Assessment' test will give you the right courses
- fit for your level.
</@noticebox>
- <@content>
+ <@textcontent>
<h1>About</h1>
+
+ <p>
+ GROW is a comprehensive spiritual formation tool that is
+ specifically designed to help you engage in the discipleship
+ process.
+ </p>
+
+ <p>
+ Disciple means “learner” and our goal is to help you be an
+ effective learner and grow in the knowledge of Jesus Christ, the
+ Word of God, the essentials of your faith that will cause the
+ transformation God promises you will experience. And that
+ transformation will in turn change your life!
+ </p>
+
+ <p>
+ Every individual is in one of four stages of spiritual formation:
+ <ul>
+ <li>Seeker</li>
+ <li>Believer</li>
+ <li>Disciple</li>
+ <li>Teacher</li>
+ </ul>
+ </p>
+
+ <p>
+ Our goal is to help you identify where you are, and to help you get
+ to the next stage of development and spiritual maturity. Welcome
+ to GROW.
+ </p>
+
+ <p>
+ GROW is an innovative process that incorporates multiple media
+ elements for learning; video, written material, interactive tools,
+ community forums, etc.
+ </p>
+
+ <p>
+ GROW is web-based process designed to be experienced in a variety
+ of ways to meet all the possible scenarios for learning:
+ <ul>
+ <li>In a community group environment</li>
+ <li>In a small discipleship group of a couple of people</li>
+ <li>Direct one-on-one discipleship</li>
+ <li>Friends with friends</li>
+ <li>Husband and wife</li>
+ <li>At the coffee shop</li>
+ <li>In your kitchen or living room</li>
+ <li>By the pool</li>
+ </ul>
+ </p>
+
<p>
- Curabitur mattis molestie ligula, ac vestibulum Curabitur
- mattis facillisis vel. Iacus facillisis vel. Nam dignissim
- massa luctus ipsum adipiscing dignissim.
+ Now it’s your turn to act! Get started now in your journey by
+ taking the GROW assessment. Identify your starting place as a
+ Seeker, Believer, Disciple or Teacher and be a disciple.
</p>
- </@content>
+ </@textcontent>
- <div id="getstarted">
- <a class="greenbutton" href="index.html">Get Started! &#x2799;</a>
- </div>
+ <#include "/templates/getstarted-button.ftl">
</@commonpage>
diff --git a/src/templates/pages/assessment.html.ftl b/src/templates/pages/assessment.html.ftl
new file mode 100644
index 0000000..7903382
--- /dev/null
+++ b/src/templates/pages/assessment.html.ftl
@@ -0,0 +1,44 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@textcontent>
+ <h1>Assessment</h1>
+
+ <p>
+ Welcome to the start of GROW and the GROW personal assessment. The
+ purpose of this 18-question assessment will identify where you are
+ in the spiritual development process:
+ </p>
+
+ <p>
+ <!-- TODO: Insert Image Here -->
+ **************** Insert the image here. *************************
+ </p>
+
+ <p>
+ Upon completion of the assessment, you will be invited to join the
+ process at the appropriate stage. For example if the assessment
+ returns a “believer” result, you will be invited to start the
+ Believer level of this process.
+ </p>
+
+ <p>
+ Once you have completed this level, you will be invited to begin the next stage of GROW.
+ </p>
+
+ <p>
+ Let’s get with your personal GROW assessment now, it will only take a few minutes.
+ </p>
+
+ </@textcontent>
+
+ <div id="getstarted">
+ <a class="greenbutton" href="${dynamicRoot}/account/assessment">Begin Assessment &#x2799;</a>
+ </div>
+</@commonpage>
+
+
diff --git a/src/templates/pages/contact.html.ftl b/src/templates/pages/contact.html.ftl
index e69de29..40499cc 100644
--- a/src/templates/pages/contact.html.ftl
+++ b/src/templates/pages/contact.html.ftl
@@ -0,0 +1,37 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@textcontent>
+ <h1>Contact Us</h1>
+
+ <p>
+ If you have any questions about GROW, please complete the following
+ form, and we will be in touch soon, or call us at 253-848-9111.
+ </p>
+
+ <form action="${dynamicRoot}/contactus" method="post">
+ <p><label for="firstnameField">First Name:</label> <input id="firstnameField" type="text" name="firstname" /></p>
+ <p><label for="lastnameField">Last Name:</label> <input id="lastnameField" type="text" name="lastname" /></p>
+ <p><label for="emailField">Email:</label> <input id="emailField" type="text" name="email" /></p>
+ <p><label for="phoneField">Phone:</label> <input id="phoneField" type="text" name="phone" /></p>
+ <p>
+ Foursquare Member:
+ <label><input type="radio" name="member" value="yes" id="memberYes"> Yes</label>
+ <label><input type="radio" name="member" value="no" id="memberNo"> No</label>
+ </p>
+ <p>
+ Question:<br />
+ <textarea name="question" rows="10" cols="80"></textarea>
+ </p>
+ <p><input type="submit" value="Send" /></p>
+ </form>
+
+ </@textcontent>
+
+</@commonpage>
+
+
diff --git a/src/templates/pages/index.html.ftl b/src/templates/pages/index.html.ftl
index 4e6ea73..2788e8f 100644
--- a/src/templates/pages/index.html.ftl
+++ b/src/templates/pages/index.html.ftl
@@ -15,13 +15,16 @@
<h1>Grow "Buckets"</h1>
<p>
- Curabitur mattis molestie ligula, ac vestibulum Curabitur
- mattis facillisis vel. Iacus facillisis vel. Nam dignissim
- massa luctus ipsum adipiscing dignissim.
+ We want to help you GROW
+ </p>
+ <p>
+ GROW process is an on-line application and network to help you GROW
+ as a follower of Jesus Christ.
+ </p>
+ Let’s join together in a spiritual formation journey and begin the
+ discipleship process that will transform your life.
</p>
</@content>
- <div id="getstarted">
- <a class="greenbutton" href="index.html">Get Started! &#x2799;</a>
- </div>
+ <#include "/templates/getstarted-button.ftl">
</@commonpage>
diff --git a/src/templates/pages/login.html.ftl b/src/templates/pages/login.html.ftl
index 590649c..2a27858 100644
--- a/src/templates/pages/login.html.ftl
+++ b/src/templates/pages/login.html.ftl
@@ -4,7 +4,7 @@
<@commonpage>
<@noticebox>
<#if errorMessage??>
- ${errorMessage}
+ ${errorMessage?html}
<#else>
Welcome!
</#if>
@@ -12,7 +12,9 @@
<@content>
<p>Welcome! You will need to login with your Foursquare Church InFellowship login.</p>
- <form action="${dynamicRoot}/login.html" method="post">
+ <p>If you do not already have an account,
+ <a href="${dynamicRoot}/newaccount.html">create one here</a>.</p>
+ <form action="${dynamicRoot}/account/authenticate?redirect=${redirect!""}" method="post">
<p><label for="emailField">Email:</label> <input id="emailField" type="text" name="email" /></p>
<p><label for="passwordField">Password:</label> <input id="passwordField" type="password" name="password" /></p>
<p><input type="submit" value="Login" /></p>
diff --git a/src/templates/pages/newaccount.html.ftl b/src/templates/pages/newaccount.html.ftl
new file mode 100644
index 0000000..780a5c8
--- /dev/null
+++ b/src/templates/pages/newaccount.html.ftl
@@ -0,0 +1,26 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ <#if errorMessage??>
+ ${errorMessage?html}
+ <#else>
+ Welcome!
+ </#if>
+ </@noticebox>
+
+ <@content>
+ <p>
+ Fill out the form below to create a new Puyallup Foursquare InFellowship account.
+ </p>
+ <form action="${dynamicRoot}/createaccount" method="post">
+ <p><label for="firstnameField">First Name:</label> <input id="firstnameField" type="text" name="firstname" /></p>
+ <p><label for="lastnameField">Last Name:</label> <input id="lastnameField" type="text" name="lastname" /></p>
+ <p><label for="emailField">Email:</label> <input id="emailField" type="text" name="email" /></p>
+ <p><input type="submit" value="Create Account" /></p>
+ </form>
+ </@content>
+</@commonpage>
+
+
diff --git a/src/templates/pages/verification.html.ftl b/src/templates/pages/verification.html.ftl
new file mode 100644
index 0000000..e81b005
--- /dev/null
+++ b/src/templates/pages/verification.html.ftl
@@ -0,0 +1,17 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@content>
+ <p>
+ We have sent you a verification email.
+ You will be taken back to the login page after you activate your account.
+ </p>
+ </@content>
+</@commonpage>
+
+
+
diff --git a/src/templates/templates/assessment-results.ftl b/src/templates/templates/assessment-results.ftl
new file mode 100644
index 0000000..98918ba
--- /dev/null
+++ b/src/templates/templates/assessment-results.ftl
@@ -0,0 +1,32 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@textcontent>
+ <p>Congratulations for completing your GROW assessment!</p>
+
+ <p>Based on your responses you have been identified as a ${stage?cap_first}.</p>
+
+ <p>
+ So what’s next? Now you begin the process of GROWing. The button
+ below will take you to the ${stage?cap_first} page.
+ </p>
+
+ <p>Here you will find everything you need to begin the GROW process and start your journey.</p>
+
+ <p>
+ We are genuinely excited for you. Each phase of the GROW process
+ will produce positive quantifiable and quality results in your life, as
+ you learn, and then apply this learning in your life.
+ </p>
+ </@textcontent>
+
+ <div id="getstarted">
+ <a class="greenbutton" href="${dynamicRoot}/account/training/${stage?lower_case}">Begin GROWing &#x2799;</a>
+ </div>
+</@commonpage>
+
+
diff --git a/src/templates/templates/getstarted-button.ftl b/src/templates/templates/getstarted-button.ftl
new file mode 100644
index 0000000..b0baaee
--- /dev/null
+++ b/src/templates/templates/getstarted-button.ftl
@@ -0,0 +1,7 @@
+<div id="getstarted">
+ <#if user??>
+ <a class="greenbutton" href="${dynamicRoot}/account">Get Started! &#x2799;</a>
+ <#else>
+ <a class="greenbutton" href="login.html">Get Started! &#x2799;</a>
+ </#if>
+</div>
diff --git a/src/templates/templates/index-hero.ftl b/src/templates/templates/index-hero.ftl
index a386ac8..f447d53 100644
--- a/src/templates/templates/index-hero.ftl
+++ b/src/templates/templates/index-hero.ftl
@@ -1,7 +1,4 @@
<div id="hero">
<h1>We want to help you Grow.</h1>
- <p>Grow Process is an online application and network that exists to
- help you grow closter to God. Morbi iaculis turpis sit amet
- vehicula sollicitudin, enim dolor condimentum</p>
</div>
diff --git a/src/templates/templates/stage-complete.ftl b/src/templates/templates/stage-complete.ftl
new file mode 100644
index 0000000..ed04681
--- /dev/null
+++ b/src/templates/templates/stage-complete.ftl
@@ -0,0 +1,56 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@textcontent>
+ <h1>Congratulations!</h1>
+
+ <p>
+ Congratulations on the completion of the ${stage?cap_first} level of the GROW process.
+ </p>
+
+ <p>
+ We strongly encourage you move to the next stage of GROW. However,
+ before you do that, let’s review a key component to ensure you
+ experience the transformation and benefits of what you have already
+ learned: Implementation.
+ </p>
+
+ <p>
+ Learning is important, we should all be continuous learners,
+ otherwise we stagnate.
+ </p>
+
+ <p>
+ However to learn new information and then not implement or employ
+ that learning in our life, will make us smarter but not necessarily
+ wiser or better.
+ </p>
+
+ <p>
+ For many there is a “knowing, doing gap” in their lives. They know
+ a lot, but they don’t do anything with what they know, and they remain
+ “stuck”.
+ </p>
+
+ <p>
+ We know that for your new knowledge and learning to have an impact
+ on your life you must implement it.
+ </p>
+
+ <p>
+ You were given some tips for how to act on the new knowledge you
+ have. We encourage you to be proactive and intentional in closing the
+ gap between what you know and what you do, to experience all that God
+ has for you.
+ </p>
+
+ </@textcontent>
+
+ <div id="getstarted">
+ <a class="greenbutton" href="${dynamicRoot}/account/training/${nextstage}">Continue GROWing &#x2799;</a>
+ </div>
+</@commonpage>
diff --git a/web/favicon.ico b/web/favicon.ico
new file mode 100644
index 0000000..200f311
--- /dev/null
+++ b/web/favicon.ico
Binary files differ
diff --git a/web/style.css b/web/style.css
index 09cb5be..39d6756 100644
--- a/web/style.css
+++ b/web/style.css
@@ -118,6 +118,10 @@ nav.primary a.current {
margin: 1em auto 1em auto;
}
+#content article.text {
+ text-align: left;
+}
+
#content h1 {
color: #696969;
font-size: 24pt;