Tienson Qin 5 лет назад
Родитель
Сommit
2187e04439

+ 2 - 1
backend/project.clj

@@ -4,7 +4,7 @@
   :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
             :url "https://www.eclipse.org/legal/epl-2.0/"}
   :dependencies [[org.clojure/clojure "1.10.0"]
-                 [clj-social "0.1.5"]
+                 [clj-social "0.1.6"]
                  [org.postgresql/postgresql "42.2.8"]
                  [org.clojure/java.jdbc "0.7.10"]
                  [honeysql "0.9.8"]
@@ -23,6 +23,7 @@
                  [metosin/jsonista "0.2.5"]
                  [aero "1.1.6"]
                  [com.stuartsierra/component "0.4.0"]
+                 [com.taoensso/nippy "2.14.0"]
                  ]
   ;; :main backend.core
   :profiles {:repl {:dependencies [[io.pedestal/pedestal.service-tools "0.5.7"]]

+ 2 - 1
backend/resources/config.edn

@@ -1,7 +1,8 @@
 {:env #or [#env ENVIRONMENT "dev"]
  :port #or [#env PORT 8080]
  :oauth {:github {:app-key #env GITHUB_APP_KEY
-                  :app-secret #env GITHUB_APP_SECRET}}
+                  :app-secret #env GITHUB_APP_SECRET
+                  :redirect-uri #env GITHUB_REDIRECT_URI}}
  :jwt-secret #env JWT_SECRET
  :cookie-secret #env COOKIE_SECRET
  :log-path #or [#env LOG_PATH "/tmp/gitnotes"]

+ 10 - 0
backend/resources/migrations/20200221043329_create_table_users.edn

@@ -0,0 +1,10 @@
+{:up ["create extension if not exists \"uuid-ossp\";
+CREATE TABLE users (
+  id uuid DEFAULT uuid_generate_v4() NOT NULL UNIQUE,
+  name text NOT NULL,
+  email text NOT NULL UNIQUE,
+  created_at timestamp with time zone DEFAULT timezone('UTC'::text, now()) NOT NULL,
+  CONSTRAINT created_at_chk CHECK ((date_part('timezone'::text, created_at) = '0'::double precision))
+);
+"]
+ :down ["drop table users"]}

+ 9 - 0
backend/resources/migrations/20200221044628_create_table_repos.edn

@@ -0,0 +1,9 @@
+{:up ["CREATE TABLE repos (
+  id uuid DEFAULT uuid_generate_v4() NOT NULL UNIQUE,
+  user_id uuid NOT NULL,
+  url text NOT NULL,
+  created_at timestamp with time zone DEFAULT timezone('UTC'::text, now()) NOT NULL,
+  CONSTRAINT created_at_chk CHECK ((date_part('timezone'::text, created_at) = '0'::double precision))
+);"
+      "CREATE UNIQUE INDEX idx_repos_user_repo ON repos(user_id, url);"]
+ :down ["drop table repos"]}

+ 9 - 0
backend/resources/migrations/20200221045345_create_table_refresh_tokens.edn

@@ -0,0 +1,9 @@
+{:up ["CREATE TABLE refresh_tokens (
+    user_id uuid NOT NULL UNIQUE,
+    token uuid NOT NULL UNIQUE
+)"
+      "ALTER TABLE refresh_tokens
+      ADD CONSTRAINT refresh_tokens_users_fkey FOREIGN KEY (user_id)
+      REFERENCES users (id)
+      ON UPDATE CASCADE ON DELETE CASCADE;"]
+ :down ["drop table refresh_tokens"]}

+ 11 - 0
backend/resources/migrations/20200221072508_create_table_tokens.edn

@@ -0,0 +1,11 @@
+{:up ["CREATE TABLE tokens (
+  id uuid DEFAULT uuid_generate_v4() NOT NULL UNIQUE,
+  user_id uuid NOT NULL,
+  oauth_type text NOT NULL,
+  oauth_id text NOT NULL UNIQUE,
+  oauth_token text NOT NULL UNIQUE,
+  created_at timestamp with time zone DEFAULT timezone('UTC'::text, now()) NOT NULL,
+  CONSTRAINT created_at_chk CHECK ((date_part('timezone'::text, created_at) = '0'::double precision))
+);"
+      ]
+ :down ["drop table tokens"]}

+ 33 - 0
backend/src/backend/auth.clj

@@ -0,0 +1,33 @@
+(ns backend.auth
+  (:require [taoensso.timbre :as timbre]
+            [clj-social.core :as social]
+            [backend.config :as config]
+            [backend.util :as util]
+            [backend.db.user :as u]
+            [backend.db.token :as token]
+            [backend.cookie :as cookie]
+            [clojure.java.jdbc :as j]))
+
+(defn github [data]
+  (let [{:keys [app-key app-secret redirect-uri]} (get-in config/config [:oauth :github])
+        instance (social/make-social :github app-key app-secret redirect-uri
+                                     :state (str (util/uuid))
+                                     :scope "user:email")
+        access-token (social/getAccessToken instance (:code data))
+        info (social/getUserInfo instance access-token)
+        oauth-type "github"
+        oauth-id (str (:id info))
+        access-token (.getAccessToken access-token)]
+    (toucan.db/transaction
+      (if-let [token (token/get oauth-type oauth-id)]
+        ;; user already exists
+        (let [token (assoc token :token access-token)]
+          (some-> (u/get (:user_id token))
+                  (assoc :token token)))
+        (when-let [user (u/insert {:name (:login info)
+                                   :email (:email info)})]
+          (let [token (token/create {:user_id (:id user)
+                                     :oauth_type oauth-type
+                                     :oauth_id oauth-id
+                                     :oauth_token access-token})]
+            (assoc user :token token)))))))

+ 33 - 0
backend/src/backend/db/refresh_token.clj

@@ -0,0 +1,33 @@
+(ns backend.db.refresh-token
+  (:refer-clojure :exclude [get update])
+  (:require [toucan.db :as db]
+            [toucan.models :as model]
+            [backend.util :as util]))
+
+(model/defmodel RefreshToken :refresh_tokens
+  model/IModel
+  (primary-key [_] :user_id))
+
+(defn get-token
+  [user-id]
+  (db/select-one-field :token RefreshToken {:user_id user-id}))
+
+(defn token-exists?
+  [token]
+  (db/exists? RefreshToken {:token token}))
+
+(defn get-user-id-by-token
+  [token]
+  (db/select-one-field :user_id RefreshToken {:token token}))
+
+(defn create
+  [user-id]
+  (if-let [token (get-token user-id)]
+    token
+    (loop [token (util/uuid)]
+      (if (token-exists? token)
+        (recur (util/uuid))
+        (do
+          (db/insert! RefreshToken {:user_id user-id
+                                    :token token})
+          token)))))

+ 28 - 0
backend/src/backend/db/repo.clj

@@ -0,0 +1,28 @@
+(ns backend.db.repo
+  (:refer-clojure :exclude [get update])
+  (:require [toucan.db :as db]
+            [toucan.models :as model]
+            [ring.util.response :as resp]
+            [backend.config :as config]
+            [backend.cookie :as cookie]))
+
+(model/defmodel Repo :repos)
+
+(defn insert
+  [args]
+  (cond
+    (and
+     (:user_id args) (:url args)
+     (db/exists? Repo (select-keys args [:user_id :url])))
+    [:bad :user-repo-exists]
+
+    :else
+    [:ok (db/insert! Repo args)]))
+
+(defn get-user-repos
+  [user-id]
+  (db/select Repo {:user_id user-id}))
+
+(defn delete
+  [id]
+  (db/delete! Repo {:id id}))

+ 28 - 0
backend/src/backend/db/token.clj

@@ -0,0 +1,28 @@
+(ns backend.db.token
+  (:refer-clojure :exclude [get update])
+  (:require [toucan.db :as db]
+            [toucan.models :as model]
+            [backend.util :as util]))
+
+(model/defmodel Token :tokens)
+
+(defn get
+  [oauth-type oauth-id]
+  (db/select-one Token {:oauth_type oauth-type
+                        :oauth_id oauth-id}))
+
+(defn exists?
+  [oauth-type oauth-id]
+  (db/exists? Token {:oauth_type oauth-type
+                     :oauth_id oauth-id}))
+
+(defn delete
+  [oauth-type oauth-id]
+  (db/delete! Token {:oauth_type oauth-type
+                     :oauth_id oauth-id}))
+
+(defn create
+  [{:keys [oauth_type oauth_id] :as m}]
+  (if (exists? oauth_type oauth_id)
+    (delete oauth_type oauth_id))
+  (db/insert! Token m))

+ 45 - 0
backend/src/backend/db/user.clj

@@ -0,0 +1,45 @@
+(ns backend.db.user
+  (:refer-clojure :exclude [get update])
+  (:require [toucan.db :as db]
+            [toucan.models :as model]
+            [ring.util.response :as resp]
+            [backend.config :as config]
+            [backend.cookie :as cookie]
+            [backend.jwt :as jwt]
+            [backend.db.refresh-token :as refresh-token]))
+
+(model/defmodel User :users)
+
+;; move to handler
+(defn logout
+  []
+  (-> (resp/redirect (:website-uri config/config))
+      (assoc :cookies cookie/delete-token)))
+
+(defn get
+  [id]
+  (db/select-one User id))
+
+(defn insert
+  [{:keys [name email] :as args}]
+  (when-not (db/exists? User {:email email})
+    (db/insert! User args)))
+
+(defn delete
+  [id]
+  (db/delete! User {:id id}))
+
+(defn update-email
+  [id email]
+  (cond
+    (db/exists? User {:email email})
+    [:bad :email-address-exists]
+
+    :else
+    [:ok (db/update! User id {:email email})]))
+
+(defn generate-tokens
+  [db user-id]
+  (cookie/token-cookie
+   {:access-token  (jwt/sign {:id user-id})
+    :refresh-token (refresh-token/create user-id)}))

+ 1 - 1
backend/src/backend/jwt.clj

@@ -1,4 +1,4 @@
-(ns api.jwt
+(ns backend.jwt
   (:require [buddy.sign.jwt :as jwt]
             [clj-time.core :as time]
             [backend.config :refer [config]]))

+ 54 - 0
backend/src/backend/routes.clj

@@ -0,0 +1,54 @@
+(ns backend.routes
+  (:require [reitit.swagger :as swagger]
+            [clj-social.core :as social]
+            [backend.config :as config]
+            [backend.util :as util]
+            [backend.auth :as auth]
+            [backend.db.user :as u]
+            [ring.util.response :as resp]))
+
+(def routes
+  [["/swagger.json"
+    {:get {:no-doc true
+           :swagger {:info {:title "gitnotes api"
+                            :description "with pedestal & reitit-http"}}
+           :handler (swagger/create-swagger-handler)}}]
+
+   ["/login"
+    {:swagger {:tags ["Login"]}}
+
+    ["/github"
+     {:get {:summary "Login with github"
+            :handler
+            (fn [req]
+              (let [{:keys [app-key app-secret redirect-uri]} (get-in config/config [:oauth :github])
+                    social (social/make-social :github app-key app-secret
+                                               (str redirect-uri
+                                                    "?referer="
+                                                    (get-in req [:headers "referer"] ""))
+                                               :state (str (util/uuid))
+                                               :scope "user:email")
+                    url (social/getAuthorizationUrl social)]
+                (prn "url: " url)
+                (resp/redirect url))
+              )}}]]
+   ["/auth"
+    {:swagger {:tags ["Authenticate"]}}
+
+    ["/github"
+     {:get {:summary "Authenticate with github"
+            :handler
+            (fn [{:keys [params] :as req}]
+              (if (and (:code params)
+                       (:state params))
+                (if-let [user (auth/github params)]
+                  (resp/header
+                    (assoc :cookies
+                           (u/generate-tokens user)
+                           :status 302)
+                    "Location" config/website-uri)
+                  {:status 500
+                   :body "Internal Error"})
+                {:status 401
+                 :body "Invalid request"}))}}]]
+   ])

+ 3 - 19
backend/src/backend/system.clj

@@ -17,32 +17,16 @@
             [io.pedestal.http :as server]
             [reitit.pedestal :as pedestal]
             [clojure.core.async :as a]
-            [clojure.java.io :as io]
             [muuntaja.core :as m]
             [com.stuartsierra.component :as component]
             [backend.components.http :as component-http]
-            [backend.components.hikari :as hikari]))
+            [backend.components.hikari :as hikari]
+            [backend.routes :as routes]))
 
 (def router
   (pedestal/routing-interceptor
     (http/router
-      [["/swagger.json"
-        {:get {:no-doc true
-               :swagger {:info {:title "gitnotes api"
-                                :description "with pedestal & reitit-http"}}
-               :handler (swagger/create-swagger-handler)}}]
-
-       ["/login"
-        {:swagger {:tags ["Login"]}}
-
-        ["/github"
-         {:get {:summary "Login with github"
-                :swagger {:produces ["image/png"]}
-                :handler (fn [_]
-                           {:status 200
-                            :headers {"Content-Type" "image/png"}
-                            :body (io/input-stream
-                                    (io/resource "reitit.png"))})}}]]       ]
+     routes/routes
 
       {;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs
        ;;:validate spec/validate ;; enable spec validation for route data

+ 1 - 1
backend/src/backend/util.clj

@@ -1,4 +1,4 @@
-(ns api.util
+(ns backend.util
   (:require [clojure.string :as str]
             [clj-time
              [coerce :as tc]