diff --git a/.gitignore b/.gitignore
index 13c6a34..1f5b9a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,8 @@ project/plugins/project/
# eclipse specific
bin/*
-.cache
\ No newline at end of file
+.cache
+
+# sensitive information
+stackoverflow.auth.properties
+langpop-web/src/main/resources/application.conf
\ No newline at end of file
diff --git a/langpop-aggregate/.classpath b/langpop-aggregate/.classpath
index 90523af..c753084 100644
--- a/langpop-aggregate/.classpath
+++ b/langpop-aggregate/.classpath
@@ -6,6 +6,10 @@
+
+
+
+
diff --git a/langpop-web/.classpath b/langpop-web/.classpath
index e5d9761..5679ff6 100644
--- a/langpop-web/.classpath
+++ b/langpop-web/.classpath
@@ -5,19 +5,23 @@
-
+
+
+
+
+
-
+
+
+
+
-
-
-
@@ -32,10 +36,6 @@
-
-
-
-
diff --git a/langpop-web/build.sbt b/langpop-web/build.sbt
index b9d90e3..80f4154 100644
--- a/langpop-web/build.sbt
+++ b/langpop-web/build.sbt
@@ -6,9 +6,13 @@ seq(webSettings :_*)
classpathTypes ~= (_ + "orbit")
+resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
+
libraryDependencies ++= Seq(
"org.scalatra" % "scalatra" % "2.1.1",
"org.scalatra" % "scalatra-scalate" % "2.1.1",
+ "com.typesafe" % "config" % "1.0.0",
+ "org.apache.httpcomponents" % "httpclient" % "4.2.2",
"org.scalatra" % "scalatra-specs2" % "2.1.1" % "test",
"org.scalatra" % "scalatra-scalatest" % "2.1.1" % "test",
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
diff --git a/langpop-web/src/main/resources/application.conf.sample b/langpop-web/src/main/resources/application.conf.sample
new file mode 100644
index 0000000..360299f
--- /dev/null
+++ b/langpop-web/src/main/resources/application.conf.sample
@@ -0,0 +1,18 @@
+# copy this file to application.conf
+# and edit
+
+langpop-web {
+ auth {
+ stackoverflow {
+ client_id = 123 # your app's id
+ client_secret = "your own app's super secret client secred"
+ key = "your app's not so secret key"
+ redirect_uri = "http://localhost:8080/auth/stackoverflow/redirect"
+
+ credentialsFile = "stackoverflow.auth.properties" # file will be created in execution path of server
+ }
+ github {
+ # nothing yet
+ }
+ }
+}
diff --git a/langpop-web/src/main/scala/Scalatra.scala b/langpop-web/src/main/scala/Scalatra.scala
index b137a85..9177134 100644
--- a/langpop-web/src/main/scala/Scalatra.scala
+++ b/langpop-web/src/main/scala/Scalatra.scala
@@ -2,6 +2,7 @@ import org.scalatra.LifeCycle
import com.abbink.langpop.web.ComponentRegistry
import com.abbink.langpop.web.LangpopServlet
+import com.abbink.langpop.web.AuthServlet
import javax.servlet.ServletContext
@@ -18,5 +19,6 @@ class Scalatra extends LifeCycle with ComponentRegistry {
// Mount one or more servlets
context.mount(new LangpopServlet, "/langpop/*")
+ context.mount(new AuthServlet, "/auth/*")
}
}
diff --git a/langpop-web/src/main/scala/com/abbink/langpop/web/AuthServlet.scala b/langpop-web/src/main/scala/com/abbink/langpop/web/AuthServlet.scala
new file mode 100644
index 0000000..20ef8db
--- /dev/null
+++ b/langpop-web/src/main/scala/com/abbink/langpop/web/AuthServlet.scala
@@ -0,0 +1,57 @@
+package com.abbink.langpop.web
+
+import org.scalatra.scalate.ScalateSupport
+import org.scalatra.ScalatraServlet
+import java.net.URLDecoder
+
+class AuthServlet extends ScalatraServlet with ScalateSupport with ComponentRegistry {
+
+ get("/") {
+
+
+ Login status
+
+ - StackOverflow: {stackoverflowLogin()}
+ - GitHub: {githubLogin}
+ - Issue query here
+
+
+
+ }
+
+ private def stackoverflowLogin() = {
+ if (stackOverflowAuth.isAuthenticated()) {
+ signed in
+ sign out
+ }
+ else {
+ signed out
+ sign in
+ }
+ }
+
+ private def githubLogin() = {
+ not implemented yet
+ }
+
+ get("/stackoverflow/logout") {
+ if (stackOverflowAuth.isAuthenticated())
+ stackOverflowAuth.clearAuth()
+ redirect("/auth")
+ }
+
+ get("/stackoverflow/login") {
+ if (stackOverflowAuth.isAuthenticated())
+ redirect("/auth")
+ else
+ redirect(stackOverflowAuth.buildOAuthUrl())
+ }
+
+ get("/stackoverflow/redirect") {
+ val code = URLDecoder.decode(params("code"))
+ //val state = params.get("state") map URLDecoder.decode
+
+ stackOverflowAuth.finalizeAuth(code)
+ redirect("/auth")
+ }
+}
diff --git a/langpop-web/src/main/scala/com/abbink/langpop/web/ComponentRegistry.scala b/langpop-web/src/main/scala/com/abbink/langpop/web/ComponentRegistry.scala
index e6a642f..abc8636 100644
--- a/langpop-web/src/main/scala/com/abbink/langpop/web/ComponentRegistry.scala
+++ b/langpop-web/src/main/scala/com/abbink/langpop/web/ComponentRegistry.scala
@@ -1,9 +1,11 @@
package com.abbink.langpop.web
import com.abbink.langpop.aggregate.{ComponentRegistry => AggregatorComponentRegistry}
+import com.abbink.langpop.web.auth.StackOverflowAuthComponent
trait ComponentRegistry extends
- AggregatorComponentRegistry
+ AggregatorComponentRegistry with
+ StackOverflowAuthComponent
{
-
+ val stackOverflowAuth = StackOverflowAuthImpl
}
diff --git a/langpop-web/src/main/scala/com/abbink/langpop/web/LangpopServlet.scala b/langpop-web/src/main/scala/com/abbink/langpop/web/LangpopServlet.scala
index 4cd46a4..ba65517 100644
--- a/langpop-web/src/main/scala/com/abbink/langpop/web/LangpopServlet.scala
+++ b/langpop-web/src/main/scala/com/abbink/langpop/web/LangpopServlet.scala
@@ -13,8 +13,11 @@ class LangpopServlet extends ScalatraServlet with ScalateSupport with ComponentR
get("/") {
- Hello, world!
- Say hello to Scalate.
+ langpop
+
}
diff --git a/langpop-web/src/main/scala/com/abbink/langpop/web/auth/StackOverflowAuth.scala b/langpop-web/src/main/scala/com/abbink/langpop/web/auth/StackOverflowAuth.scala
new file mode 100644
index 0000000..b19245d
--- /dev/null
+++ b/langpop-web/src/main/scala/com/abbink/langpop/web/auth/StackOverflowAuth.scala
@@ -0,0 +1,179 @@
+package com.abbink.langpop.web.auth
+
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.ArrayList
+import java.util.Date
+import java.util.Properties
+import org.apache.http.client.entity.UrlEncodedFormEntity
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.client.utils.URIBuilder
+import org.apache.http.client.HttpClient
+import org.apache.http.impl.client.DefaultHttpClient
+import org.apache.http.message.BasicNameValuePair
+import org.apache.http.HttpResponse
+import org.apache.http.NameValuePair
+import com.typesafe.config.ConfigFactory
+import org.apache.http.util.EntityUtils
+import org.apache.http.client.utils.URLEncodedUtils
+import java.net.URI
+import java.nio.charset.Charset
+
+trait StackOverflowAuth {
+
+ def isAuthenticated() : Boolean
+
+ def buildOAuthUrl() : String
+
+ def clearAuth() : Unit
+
+ def finalizeAuth(code : String) : Unit
+}
+
+trait StackOverflowAuthComponent {
+ val stackOverflowAuth:StackOverflowAuth
+
+ object StackOverflowAuthImpl extends StackOverflowAuth {
+
+ val config = ConfigFactory.load()
+ val mergedConfig = config.getConfig("langpop-web").withFallback(config)
+
+ val client_id = mergedConfig.getString("langpop-web.auth.stackoverflow.client_id")
+ val client_secret = mergedConfig.getString("langpop-web.auth.stackoverflow.client_secret")
+ val scope = "no_expiry"
+ val redirect_uri = mergedConfig.getString("langpop-web.auth.stackoverflow.redirect_uri")
+ val credentialsFileName = mergedConfig.getString("langpop-web.auth.stackoverflow.credentialsFile")
+
+ var access_token : Option[String] = None
+ var expires : Option[Date] = None
+ readAuth()
+
+ def isAuthenticated() = {
+ access_token match {
+ case None => false
+ case Some(t) => expires match {
+ case None => true
+ case Some(e) => e after new Date()
+ }
+ }
+ }
+
+ def buildOAuthUrl() = {
+ val uriBuilder = new URIBuilder();
+ uriBuilder.setScheme("https").setHost("stackexchange.com").setPath("/oauth")
+ .setParameter("client_id", client_id)
+ .setParameter("scope", scope)
+ .setParameter("redirect_uri", redirect_uri)
+ uriBuilder.build.toString()
+ }
+
+ /**
+ * reads access token and expiration date from properties file
+ */
+ private def readAuth() : Unit = {
+ access_token = None
+ expires = None
+ try {
+ val fs : InputStream = new FileInputStream(credentialsFileName);
+ val props : Properties = new Properties()
+ props.load(fs);
+ fs.close()
+
+ val token = props.getProperty("access_token")
+ val exp = props.getProperty("expires")
+ access_token = token match {
+ case null => None
+ case x => Some(x)
+ }
+ expires = exp match {case null => None case x => Some(new Date(1000 * (x.toLong)))}
+ }
+ catch {
+ case e => //TODO
+ }
+ (access_token, expires)
+ }
+
+ /**
+ * clears all auth data (i.e. signs out)
+ */
+ def clearAuth() = {
+ access_token = None
+ expires = None
+ try {
+ val props = new Properties()
+ val fs : OutputStream = new FileOutputStream(credentialsFileName)
+ props.store(fs, null)
+ fs.close()
+ }
+ catch {
+ case e => //TODO
+ }
+ }
+
+ def finalizeAuth(code : String) = {
+ val uriBuilder = new URIBuilder()
+ uriBuilder.setScheme("https").setHost("stackexchange.com").setPath("/oauth/access_token")
+ val client : HttpClient = new DefaultHttpClient()
+ val formparams : java.util.List[NameValuePair] = new ArrayList[NameValuePair]()
+ formparams.add(new BasicNameValuePair("client_id", client_id))
+ formparams.add(new BasicNameValuePair("client_secret", client_secret))
+ formparams.add(new BasicNameValuePair("code", code))
+ formparams.add(new BasicNameValuePair("redirect_uri", redirect_uri))
+ val entity : UrlEncodedFormEntity = new UrlEncodedFormEntity(formparams, "UTF-8")
+ val post = new HttpPost(uriBuilder.build())
+ post.setEntity(entity)
+ val response : HttpResponse = client.execute(post)
+
+ if (response.getStatusLine().getStatusCode() != 400) {
+ var access_token : Option[String] = None
+ var expires : Option[Date] = None
+
+ val entity = response.getEntity()
+ val entityContent = EntityUtils.toString(entity)
+ val entities : java.util.List[NameValuePair] = URLEncodedUtils.parse(
+ entityContent,
+ Charset.forName(entity.getContentEncoding() match {
+ case null => "ISO-8859-1" //default from EntityUtils
+ case x => x.getValue()
+ }))
+ val iter = entities.iterator()
+ while (iter.hasNext()) {
+ val pair : NameValuePair = iter.next()
+ pair.getName() match {
+ case "access_token" => access_token = Some(pair.getValue())
+ case "expires" => expires = Some(new Date(1000 * (pair.getValue().toLong)))
+ case _ => //ignore
+ }
+ }
+
+ writeAuth(access_token, expires)
+ }
+ }
+
+ private def writeAuth(accessToken : Option[String], expires : Option[Date]) = {
+ if (accessToken == None) {
+ clearAuth
+ }
+ else {
+ this.access_token = accessToken
+ this.expires = expires
+ try {
+ val props = new Properties()
+ props.setProperty("access_token", accessToken.get);
+ expires match {
+ case Some(date) => props.setProperty("expires", (date.getTime()/1000).toString())
+ case _ => //ignore
+ }
+ val fs : OutputStream = new FileOutputStream(credentialsFileName);
+ props.store(fs, null);
+ fs.close()
+ }
+ catch {
+ case e => //TODO
+ }
+ }
+ }
+ }
+}
diff --git a/langpop-web/src/test/scala/com/abbink/langpop/web/DateLangRequestTests.scala b/langpop-web/src/test/scala/com/abbink/langpop/web/DateLangRequestTests.scala
index d5c87cf..13f56c9 100644
--- a/langpop-web/src/test/scala/com/abbink/langpop/web/DateLangRequestTests.scala
+++ b/langpop-web/src/test/scala/com/abbink/langpop/web/DateLangRequestTests.scala
@@ -19,6 +19,8 @@ class DateLangRequestTests extends ScalatraSuite with FunSuite with TestingEnvir
// aggregator.system.shutdown()
// }
+
+
test("get date/language") {
println(":get date/language")
get("/langpop/2012-10-19/scala") {