Browse Source

Rename to logseq

Tienson Qin 5 years ago
parent
commit
4ce50391b5
58 changed files with 1332 additions and 67 deletions
  1. 1 1
      .gitignore
  2. 0 5
      .nowignore
  3. 2 2
      .projectile
  4. 1 1
      api/src/server/main.cljs
  5. 1 1
      api/src/server/router.cljs
  6. 12 0
      backend/.gitignore
  7. 24 0
      backend/CHANGELOG.md
  8. 277 0
      backend/LICENSE
  9. 22 0
      backend/README.md
  10. 89 0
      backend/dev/user.clj
  11. 3 0
      backend/doc/intro.md
  12. 36 0
      backend/project.clj
  13. 24 0
      backend/resources/config.edn
  14. 52 0
      backend/resources/logback.xml
  15. 10 0
      backend/resources/migrations/20200221043329_create_table_users.edn
  16. 9 0
      backend/resources/migrations/20200221044628_create_table_repos.edn
  17. 9 0
      backend/resources/migrations/20200221045345_create_table_refresh_tokens.edn
  18. 11 0
      backend/resources/migrations/20200221072508_create_table_tokens.edn
  19. 1 0
      backend/resources/public
  20. 35 0
      backend/src/backend/auth.clj
  21. 24 0
      backend/src/backend/components/hikari.clj
  22. 26 0
      backend/src/backend/components/http.clj
  23. 12 0
      backend/src/backend/config.clj
  24. 58 0
      backend/src/backend/cookie.clj
  25. 21 0
      backend/src/backend/core.clj
  26. 33 0
      backend/src/backend/db/refresh_token.clj
  27. 32 0
      backend/src/backend/db/repo.clj
  28. 36 0
      backend/src/backend/db/token.clj
  29. 45 0
      backend/src/backend/db/user.clj
  30. 16 0
      backend/src/backend/db_migrate.clj
  31. 29 0
      backend/src/backend/interceptors.clj
  32. 23 0
      backend/src/backend/jwt.clj
  33. 103 0
      backend/src/backend/routes.clj
  34. 101 0
      backend/src/backend/system.clj
  35. 82 0
      backend/src/backend/util.clj
  36. 29 0
      backend/src/backend/views/home.clj
  37. 7 0
      backend/test/backend/core_test.clj
  38. 0 20
      now.json
  39. 0 1
      public
  40. 3 3
      readme.org
  41. 3 3
      web/.gitignore
  42. 2 2
      web/package.json
  43. 0 0
      web/public/css/highlight.css
  44. 0 0
      web/public/css/org.css
  45. 3 3
      web/public/css/style.css
  46. 0 0
      web/public/css/tailwind.min.css
  47. 0 0
      web/public/img/angled-background.svg
  48. 0 0
      web/public/img/hero-pattern-lg.png
  49. 0 0
      web/public/img/logo.png
  50. 8 8
      web/public/index.html
  51. 0 0
      web/public/js/highlight.pack.js
  52. 0 0
      web/public/static/js/manifest.edn
  53. 4 4
      web/shadow-cljs.edn
  54. 4 4
      web/src/frontend/components/home.cljs
  55. 4 4
      web/src/frontend/components/sidebar.cljs
  56. 3 3
      web/src/frontend/config.cljs
  57. 1 1
      web/src/frontend/db.cljs
  58. 1 1
      web/src/frontend/handler.cljs

+ 1 - 1
.gitignore

@@ -1,5 +1,5 @@
 node_modules/
 node_modules/
-web/public/static/js/main.js
+web/public/js/main.js
 
 
 /.cpcache
 /.cpcache
 /target
 /target

+ 0 - 5
.nowignore

@@ -1,5 +0,0 @@
-web
-api/shadow-cljs.edn
-api/.shadow-cljs
-api/src
-.env

+ 2 - 2
.projectile

@@ -5,5 +5,5 @@
 -/web/.cpcache
 -/web/.cpcache
 -/web/.shadow-cljs/
 -/web/.shadow-cljs/
 -/web/node_modules/
 -/web/node_modules/
--/web/public/static/js/cljs-runtime/
--/web/public/static/js/main.js
+-/web/public/js/cljs-runtime/
+-/web/public/js/main.js

+ 1 - 1
api/src/server/main.cljs

@@ -7,7 +7,7 @@
 (defonce server (atom nil))
 (defonce server (atom nil))
 
 
 (defonce config
 (defonce config
-  {:allowed-origins #{"http://localhost:8080" "https://gitnotes.now.sh"}})
+  {:allowed-origins #{"http://localhost:8080" "https://logseq.now.sh"}})
 
 
 (def cors-options
 (def cors-options
   {:origin (fn [origin callback]
   {:origin (fn [origin callback]

+ 1 - 1
api/src/server/router.cljs

@@ -13,7 +13,7 @@
 ;; utils
 ;; utils
 (def dev? ^boolean goog.DEBUG)
 (def dev? ^boolean goog.DEBUG)
 
 
-(def cookie-domain (if dev? "" ".gitnotes.now.sh"))
+(def cookie-domain (if dev? "" ".logseq.now.sh"))
 
 
 (def cookie-options
 (def cookie-options
   {:maxAge (* 1000 60 60 24 30)         ; 30 days
   {:maxAge (* 1000 60 60 24 30)         ; 30 days

+ 12 - 0
backend/.gitignore

@@ -0,0 +1,12 @@
+/target
+/classes
+/checkouts
+profiles.clj
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+/.lein-*
+/.nrepl-port
+.hgignore
+.hg/

+ 24 - 0
backend/CHANGELOG.md

@@ -0,0 +1,24 @@
+# Change Log
+All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).
+
+## [Unreleased]
+### Changed
+- Add a new arity to `make-widget-async` to provide a different widget shape.
+
+## [0.1.1] - 2020-02-20
+### Changed
+- Documentation on how to make the widgets.
+
+### Removed
+- `make-widget-sync` - we're all async, all the time.
+
+### Fixed
+- Fixed widget maker to keep working when daylight savings switches over.
+
+## 0.1.0 - 2020-02-20
+### Added
+- Files from the new template.
+- Widget maker public API - `make-widget-sync`.
+
+[Unreleased]: https://github.com/your-name/backend/compare/0.1.1...HEAD
+[0.1.1]: https://github.com/your-name/backend/compare/0.1.0...0.1.1

+ 277 - 0
backend/LICENSE

@@ -0,0 +1,277 @@
+Eclipse Public License - v 2.0
+
+    THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+    PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+    OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+  a) in the case of the initial Contributor, the initial content
+     Distributed under this Agreement, and
+
+  b) in the case of each subsequent Contributor:
+     i) changes to the Program, and
+     ii) additions to the Program;
+  where such changes and/or additions to the Program originate from
+  and are Distributed by that particular Contributor. A Contribution
+  "originates" from a Contributor if it was added to the Program by
+  such Contributor itself or anyone acting on such Contributor's behalf.
+  Contributions do not include changes or additions to the Program that
+  are not Modified Works.
+
+"Contributor" means any person or entity that Distributes the Program.
+
+"Licensed Patents" mean patent claims licensable by a Contributor which
+are necessarily infringed by the use or sale of its Contribution alone
+or when combined with the Program.
+
+"Program" means the Contributions Distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement
+or any Secondary License (as applicable), including Contributors.
+
+"Derivative Works" shall mean any work, whether in Source Code or other
+form, that is based on (or derived from) the Program and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship.
+
+"Modified Works" shall mean any work in Source Code or other form that
+results from an addition to, deletion from, or modification of the
+contents of the Program, including, for purposes of clarity any new file
+in Source Code form that contains any contents of the Program. Modified
+Works shall not include works that contain only declarations,
+interfaces, types, classes, structures, or files of the Program solely
+in each case in order to link to, bind by name, or subclass the Program
+or Modified Works thereof.
+
+"Distribute" means the acts of a) distributing or b) making available
+in any manner that enables the transfer of a copy.
+
+"Source Code" means the form of a Program preferred for making
+modifications, including but not limited to software source code,
+documentation source, and configuration files.
+
+"Secondary License" means either the GNU General Public License,
+Version 2.0, or any later versions of that license, including any
+exceptions or additional permissions as identified by the initial
+Contributor.
+
+2. GRANT OF RIGHTS
+
+  a) Subject to the terms of this Agreement, each Contributor hereby
+  grants Recipient a non-exclusive, worldwide, royalty-free copyright
+  license to reproduce, prepare Derivative Works of, publicly display,
+  publicly perform, Distribute and sublicense the Contribution of such
+  Contributor, if any, and such Derivative Works.
+
+  b) Subject to the terms of this Agreement, each Contributor hereby
+  grants Recipient a non-exclusive, worldwide, royalty-free patent
+  license under Licensed Patents to make, use, sell, offer to sell,
+  import and otherwise transfer the Contribution of such Contributor,
+  if any, in Source Code or other form. This patent license shall
+  apply to the combination of the Contribution and the Program if, at
+  the time the Contribution is added by the Contributor, such addition
+  of the Contribution causes such combination to be covered by the
+  Licensed Patents. The patent license shall not apply to any other
+  combinations which include the Contribution. No hardware per se is
+  licensed hereunder.
+
+  c) Recipient understands that although each Contributor grants the
+  licenses to its Contributions set forth herein, no assurances are
+  provided by any Contributor that the Program does not infringe the
+  patent or other intellectual property rights of any other entity.
+  Each Contributor disclaims any liability to Recipient for claims
+  brought by any other entity based on infringement of intellectual
+  property rights or otherwise. As a condition to exercising the
+  rights and licenses granted hereunder, each Recipient hereby
+  assumes sole responsibility to secure any other intellectual
+  property rights needed, if any. For example, if a third party
+  patent license is required to allow Recipient to Distribute the
+  Program, it is Recipient's responsibility to acquire that license
+  before distributing the Program.
+
+  d) Each Contributor represents that to its knowledge it has
+  sufficient copyright rights in its Contribution, if any, to grant
+  the copyright license set forth in this Agreement.
+
+  e) Notwithstanding the terms of any Secondary License, no
+  Contributor makes additional grants to any Recipient (other than
+  those set forth in this Agreement) as a result of such Recipient's
+  receipt of the Program under the terms of a Secondary License
+  (if permitted under the terms of Section 3).
+
+3. REQUIREMENTS
+
+3.1 If a Contributor Distributes the Program in any form, then:
+
+  a) the Program must also be made available as Source Code, in
+  accordance with section 3.2, and the Contributor must accompany
+  the Program with a statement that the Source Code for the Program
+  is available under this Agreement, and informs Recipients how to
+  obtain it in a reasonable manner on or through a medium customarily
+  used for software exchange; and
+
+  b) the Contributor may Distribute the Program under a license
+  different than this Agreement, provided that such license:
+     i) effectively disclaims on behalf of all other Contributors all
+     warranties and conditions, express and implied, including
+     warranties or conditions of title and non-infringement, and
+     implied warranties or conditions of merchantability and fitness
+     for a particular purpose;
+
+     ii) effectively excludes on behalf of all other Contributors all
+     liability for damages, including direct, indirect, special,
+     incidental and consequential damages, such as lost profits;
+
+     iii) does not attempt to limit or alter the recipients' rights
+     in the Source Code under section 3.2; and
+
+     iv) requires any subsequent distribution of the Program by any
+     party to be under a license that satisfies the requirements
+     of this section 3.
+
+3.2 When the Program is Distributed as Source Code:
+
+  a) it must be made available under this Agreement, or if the
+  Program (i) is combined with other material in a separate file or
+  files made available under a Secondary License, and (ii) the initial
+  Contributor attached to the Source Code the notice described in
+  Exhibit A of this Agreement, then the Program may be made available
+  under the terms of such Secondary Licenses, and
+
+  b) a copy of this Agreement must be included with each copy of
+  the Program.
+
+3.3 Contributors may not remove or alter any copyright, patent,
+trademark, attribution notices, disclaimers of warranty, or limitations
+of liability ("notices") contained within the Program from any copy of
+the Program which they Distribute, provided that Contributors may add
+their own appropriate notices.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities
+with respect to end users, business partners and the like. While this
+license is intended to facilitate the commercial use of the Program,
+the Contributor who includes the Program in a commercial product
+offering should do so in a manner which does not create potential
+liability for other Contributors. Therefore, if a Contributor includes
+the Program in a commercial product offering, such Contributor
+("Commercial Contributor") hereby agrees to defend and indemnify every
+other Contributor ("Indemnified Contributor") against any losses,
+damages and costs (collectively "Losses") arising from claims, lawsuits
+and other legal actions brought by a third party against the Indemnified
+Contributor to the extent caused by the acts or omissions of such
+Commercial Contributor in connection with its distribution of the Program
+in a commercial product offering. The obligations in this section do not
+apply to any claims or Losses relating to any actual or alleged
+intellectual property infringement. In order to qualify, an Indemnified
+Contributor must: a) promptly notify the Commercial Contributor in
+writing of such claim, and b) allow the Commercial Contributor to control,
+and cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial
+product offering, Product X. That Contributor is then a Commercial
+Contributor. If that Commercial Contributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Contributor's responsibility
+alone. Under this section, the Commercial Contributor would have to
+defend claims against the other Contributors related to those performance
+claims and warranties, and if a court requires any other Contributor to
+pay any damages as a result, the Commercial Contributor must pay
+those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
+PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
+BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
+TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
+PURPOSE. Each Recipient is solely responsible for determining the
+appropriateness of using and distributing the Program and assumes all
+risks associated with its exercise of rights under this Agreement,
+including but not limited to the risks and costs of program errors,
+compliance with applicable laws, damage to or loss of data, programs
+or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
+PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
+SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
+PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
+EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed to the
+minimum extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging that the
+Program itself (excluding combinations of the Program with other software
+or hardware) infringes such Recipient's patent(s), then such Recipient's
+rights granted under Section 2(b) shall terminate as of the date such
+litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it
+fails to comply with any of the material terms or conditions of this
+Agreement and does not cure such failure in a reasonable period of
+time after becoming aware of such noncompliance. If all Recipient's
+rights under this Agreement terminate, Recipient agrees to cease use
+and distribution of the Program as soon as reasonably practicable.
+However, Recipient's obligations under this Agreement and any licenses
+granted by Recipient relating to the Program shall continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement,
+but in order to avoid inconsistency the Agreement is copyrighted and
+may only be modified in the following manner. The Agreement Steward
+reserves the right to publish new versions (including revisions) of
+this Agreement from time to time. No one other than the Agreement
+Steward has the right to modify this Agreement. The Eclipse Foundation
+is the initial Agreement Steward. The Eclipse Foundation may assign the
+responsibility to serve as the Agreement Steward to a suitable separate
+entity. Each new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+Distributed subject to the version of the Agreement under which it was
+received. In addition, after a new version of the Agreement is published,
+Contributor may elect to Distribute the Program (including its
+Contributions) under the new version.
+
+Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
+receives no rights or licenses to the intellectual property of any
+Contributor under this Agreement, whether expressly, by implication,
+estoppel or otherwise. All rights in the Program not expressly granted
+under this Agreement are reserved. Nothing in this Agreement is intended
+to be enforceable by any entity that is not a Contributor or Recipient.
+No third-party beneficiary rights are created under this Agreement.
+
+Exhibit A - Form of Secondary Licenses Notice
+
+"This Source Code may also be made available under the following 
+Secondary Licenses when the conditions for such availability set forth 
+in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
+version(s), and exceptions or additional permissions here}."
+
+  Simply including a copy of this Agreement, including this Exhibit A
+  is not sufficient to license the Source Code under Secondary Licenses.
+
+  If it is not possible or desirable to put the notice in a particular
+  file, then You may include the notice in a location (such as a LICENSE
+  file in a relevant directory) where a recipient would be likely to
+  look for such a notice.
+
+  You may add additional accurate notices of copyright ownership.

+ 22 - 0
backend/README.md

@@ -0,0 +1,22 @@
+# backend
+
+A Clojure library designed to ... well, that part is up to you.
+
+## Usage
+
+FIXME
+
+## License
+
+Copyright © 2020 FIXME
+
+This program and the accompanying materials are made available under the
+terms of the Eclipse Public License 2.0 which is available at
+http://www.eclipse.org/legal/epl-2.0.
+
+This Source Code may also be made available under the following Secondary
+Licenses when the conditions for such availability set forth in the Eclipse
+Public License, v. 2.0 are satisfied: GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or (at your
+option) any later version, with the GNU Classpath Exception which is available
+at https://www.gnu.org/software/classpath/license.html.

+ 89 - 0
backend/dev/user.clj

@@ -0,0 +1,89 @@
+(ns user
+  (:require [com.stuartsierra.component :as component]
+            [clojure.tools.namespace.repl :as namespace]
+            [backend.config :as config]
+            [backend.db-migrate :as migrate]
+            [io.pedestal.service-tools.dev :as dev]
+            [clj-time
+             [coerce :as tc]
+             [core :as t]]
+            [clojure.java.io :as io]
+            [clojure.string :as string]))
+
+(namespace/disable-reload!)
+(namespace/set-refresh-dirs "src" "dev")
+(defonce *system (atom nil))
+(defonce *db (atom nil))
+
+(defn migrate []
+  (migrate/migrate @*db))
+
+(defn rollback []
+  (migrate/rollback @*db))
+
+(defn stop []
+  (some-> @*system (component/stop))
+  (reset! *system nil))
+
+(defn refresh []
+  (let [res (namespace/refresh)]
+    (when (not= res :ok)
+      (throw res))
+    :ok))
+
+(defn go
+  []
+  (require 'backend.core)
+  (dev/watch)
+  (when-some [f (resolve 'backend.system/new-system)]
+    (when-some [system (f config/config)]
+      (when-some [system' (component/start system)]
+        (reset! *system system')
+        (reset! *db {:datasource (get-in @*system [:hikari :datasource])}))))
+  (migrate))
+
+(defn reset []
+  (stop)
+  (refresh)
+  (go))
+
+(defn get-unix-timestamp []
+  (tc/to-long (t/now)))
+
+(def date-format
+  "Format for DateTime"
+  "yyyyMMddHHmmss")
+(def migrations-dir
+  "Default migrations directory"
+  "resources/migrations/")
+(def ragtime-format-edn
+  "EDN template for SQL migrations"
+  "{:up [\"\"]\n :down [\"\"]}")
+
+(defn migrations-dir-exist?
+  "Checks if 'resources/migrations' directory exists"
+  []
+  (.isDirectory (io/file migrations-dir)))
+
+(defn now
+  "Gets the current DateTime"  []
+  (.format (java.text.SimpleDateFormat. date-format) (new java.util.Date)))
+
+(defn migration-file-path
+  "Complete migration file path"
+  [name]
+  (str migrations-dir (now) "_" (string/replace name #"\s+|-+|_+" "_") ".edn"))
+
+(defn create-migration
+  "Creates a migration file with the current DateTime"
+  [name]
+  (let [migration-file (migration-file-path name)]
+    (if-not (migrations-dir-exist?)
+      (io/make-parents migration-file))
+    (spit migration-file ragtime-format-edn)))
+
+(defn reset-db
+  []
+  (dotimes [i 100]
+    (rollback))
+  (migrate))

+ 3 - 0
backend/doc/intro.md

@@ -0,0 +1,3 @@
+# Introduction to backend
+
+TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)

+ 36 - 0
backend/project.clj

@@ -0,0 +1,36 @@
+(defproject backend "0.1.0-SNAPSHOT"
+  :description "FIXME: write description"
+  :url "http://example.com/FIXME"
+  :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.6"]
+                 [org.postgresql/postgresql "42.2.8"]
+                 [org.clojure/java.jdbc "0.7.10"]
+                 [honeysql "0.9.8"]
+                 [hikari-cp "2.9.0"]
+                 [toucan "1.15.0"]
+                 [ragtime "0.8.0"]
+                 [com.taoensso/timbre "4.10.0"]
+                 [org.clojure/tools.namespace "0.3.1"]
+                 [buddy/buddy-sign "3.1.0"]
+                 [buddy/buddy-hashers "1.4.0"]
+                 [enlive "1.1.6"]
+                 [io.pedestal/pedestal.service "0.5.5"]
+                 [io.pedestal/pedestal.jetty "0.5.5"]
+                 [metosin/reitit-pedestal "0.4.2"]
+                 [metosin/reitit "0.4.2"]
+                 [metosin/jsonista "0.2.5"]
+                 [aero "1.1.6"]
+                 [com.stuartsierra/component "0.4.0"]
+                 [com.taoensso/nippy "2.14.0"]
+                 [hiccup "1.0.5"]]
+  ;; :main backend.core
+  :profiles {:repl {:dependencies [[io.pedestal/pedestal.service-tools "0.5.7"]]
+                    :source-paths ["src/backend" "dev"]}
+             :uberjar {:main backend.core
+                       :aot :all}}
+  :repl-options {:init-ns user}
+  :jvm-opts ["-Duser.timezone=UTC" "-Dclojure.spec.check-asserts=true"]
+  :aliases {"migrate"  ["run" "-m" "user/migrate"]
+            "rollback" ["run" "-m" "user/rollback"]})

+ 24 - 0
backend/resources/config.edn

@@ -0,0 +1,24 @@
+{:env #or [#env ENVIRONMENT "dev"]
+ :port #or [#env PORT 8080]
+ :oauth {:github {:app-key #env GITHUB_APP_KEY
+                  :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/logseq"]
+ :hikari-spec {:auto-commit        true
+               :read-only          false
+               :connection-timeout 30000
+               :validation-timeout 5000
+               :idle-timeout       600000
+               :max-lifetime       1800000
+               :minimum-idle       10
+               :maximum-pool-size  48
+               :pool-name          "logseq-clj-db-pool"
+               :adapter            "postgresql"
+               :username           #env PG_USERNAME
+               :password           #env PG_PASSWORD
+               :database-name      "logseq"
+               :server-name        "localhost"
+               :port-number        5432
+               :register-mbeans    false}}

+ 52 - 0
backend/resources/logback.xml

@@ -0,0 +1,52 @@
+<!-- Logback configuration. See http://logback.qos.ch/manual/index.html -->
+<!-- Scanning is currently turned on; This will impact performance! -->
+<configuration scan="true" scanPeriod="10 seconds">
+  <!-- Silence Logback's own status messages about config parsing
+  <statusListener class="ch.qos.logback.core.status.NopStatusListener" /> -->
+
+  <!-- Simple file output -->
+  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
+    <encoder>
+      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %X{io.pedestal} - %msg%n</pattern>
+    </encoder>
+
+    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+      <!-- rollover daily -->
+      <fileNamePattern>logs/restream-api-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+      <!-- or whenever the file size reaches 64 MB -->
+      <maxFileSize>64 MB</maxFileSize>
+    </rollingPolicy>
+
+    <!-- Safely log to the same file from multiple JVMs. Degrades performance! -->
+    <prudent>true</prudent>
+  </appender>
+
+
+  <!-- Console output -->
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
+    <encoder>
+      <pattern>%-5level %logger{36} %X{io.pedestal} - %msg%n</pattern>
+    </encoder>
+    <!-- Only log level INFO and above -->
+    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+      <level>INFO</level>
+    </filter>
+  </appender>
+
+
+  <!-- Enable FILE and STDOUT appenders for all log messages.
+       By default, only log at level INFO and above. -->
+  <root level="INFO">
+    <appender-ref ref="FILE" />
+    <appender-ref ref="STDOUT" />
+  </root>
+
+  <!-- For loggers in the these namespaces, log at all levels. -->
+  <logger name="user" level="ALL" />
+  <!-- To log pedestal internals, enable this and change ThresholdFilter to DEBUG
+    <logger name="io.pedestal" level="ALL" />
+  -->
+
+</configuration>

+ 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"]}

+ 1 - 0
backend/resources/public

@@ -0,0 +1 @@
+/home/tienson/codes/projects/gitnotes/frontend/public

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

@@ -0,0 +1,35 @@
+(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,repo")
+        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
+        (do
+          (token/update (:id token) access-token)
+          (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)))))))

+ 24 - 0
backend/src/backend/components/hikari.clj

@@ -0,0 +1,24 @@
+(ns backend.components.hikari
+  (:require [com.stuartsierra.component :as component]
+            [hikari-cp.core :as hikari]
+            [clojure.java.jdbc :as j]
+            [toucan.db :as toucan]
+            [backend.db-migrate :as migrate]))
+
+(defrecord Hikari [db-spec datasource]
+  component/Lifecycle
+  (start [component]
+    (let [s (or datasource (hikari/make-datasource db-spec))]
+      ;; set time zone
+      (j/execute! {:datasource s} ["set time zone 'UTC'"])
+      ;; migrate
+      (migrate/migrate {:datasource s})
+      (toucan/set-default-db-connection! {:datasource s})
+      (assoc component :datasource s)))
+  (stop [component]
+    (when datasource
+      (hikari/close-datasource datasource))
+    (assoc component :datasource nil)))
+
+(defn new-hikari-cp [db-spec]
+  (map->Hikari {:db-spec db-spec}))

+ 26 - 0
backend/src/backend/components/http.clj

@@ -0,0 +1,26 @@
+(ns backend.components.http
+  (:require [com.stuartsierra.component :as component]
+            [io.pedestal.http :as http]))
+
+(defn test?
+  [service-map]
+  (= :test (:env service-map)))
+
+(defrecord Server [service-map service]
+  component/Lifecycle
+  (start [this]
+    (prn "service-map: " service-map)
+    (if service
+      this
+      (cond-> service-map
+        true                      http/create-server
+        (not (test? service-map)) http/start
+        true                      ((partial assoc this :service)))))
+  (stop [this]
+    (when (and service (not (test? service-map)))
+      (http/stop service))
+    (assoc this :service nil)))
+
+(defn new-server
+  []
+  (map->Server {}))

+ 12 - 0
backend/src/backend/config.clj

@@ -0,0 +1,12 @@
+(ns backend.config
+  (:require [aero.core :refer (read-config)]
+            [clojure.java.io :as io]))
+
+(def config (read-config (io/resource "config.edn")))
+
+(def production? (= "production" (:env config)))
+(def dev? (= "dev" (:env config)))
+(def test? (= "test" (:env config)))
+(def website-uri (if dev?
+                   "http://localhost:8080"
+                   "https://logseq.com"))

+ 58 - 0
backend/src/backend/cookie.clj

@@ -0,0 +1,58 @@
+(ns backend.cookie
+  (:require [buddy.sign.compact :as buddy]
+            [backend.util :as util]
+            [backend.config :as config]))
+
+(defn sign [token]
+  (buddy/sign token (:cookie-secret config/config)))
+
+(defn unsign [cookie]
+  (buddy/unsign cookie (:cookie-secret config/config)))
+
+;; domain path expires
+(defn token-cookie [value & {:keys [max-age path]
+                             :or {path "/"
+                                  max-age (* (* 3600 24) 30)}}]
+  (let [dev? config/dev?
+        xsrf-token (str (util/uuid))
+        domain (if-not dev?
+                 ".logseq.com"
+                 "")
+        secure (if-not dev?
+                 true
+                 false)]
+    {"x" (cond->
+           {:value   (sign value)
+            :max-age max-age
+            :http-only true
+            :path path
+            :secure secure}
+           domain
+           (assoc :domain domain))
+     "xsrf-token" (cond->
+                    {:value xsrf-token
+                     :max-age max-age
+                     :http-only true
+                     :path "/"
+                     :secure secure}
+                    domain
+                    (assoc :domain domain))}))
+
+(def delete-token
+  (let [domain (if-not config/dev?
+                 ".logseq.com"
+                 "")]
+    {"x" {:value ""
+          :path "/"
+          :expires "Thu, 01 Jan 1970 00:00:00 GMT"
+          :http-only true
+          :domain domain}
+     "xsrf-token" {:value ""
+                   :path "/"
+                   :expires "Thu, 01 Jan 1970 00:00:00 GMT"
+                   :http-only true
+                   :domain domain}}))
+
+(defn get-token [req]
+  (when-let [access-token (get-in req [:cookies "x" :value])]
+    (unsign access-token)))

+ 21 - 0
backend/src/backend/core.clj

@@ -0,0 +1,21 @@
+(ns backend.core
+  (:require [backend.config :as config]
+            [backend.system :as system]
+            [taoensso.timbre :as timbre]
+            [taoensso.timbre.appenders.core :as appenders]
+            [com.stuartsierra.component :as component]))
+
+(defn set-logger!
+  [log-path]
+  (timbre/merge-config! (cond->
+                          {:appenders {:spit (appenders/spit-appender {:fname log-path})}}
+                          config/production?
+                          (assoc :output-fn (partial timbre/default-output-fn {:stacktrace-fonts {}})))))
+
+(defn start []
+  (System/setProperty "https.protocols" "TLSv1.2,TLSv1.1,SSLv3")
+  (set-logger! (:log-path config/config))
+
+  (let [system (system/new-system config/config)]
+    (component/start system))
+  (println "server running in port 3000"))

+ 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)))))

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

@@ -0,0 +1,32 @@
+(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}))
+
+(defn update
+  [id url]
+  (db/update! Repo id {:url url}))

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

@@ -0,0 +1,36 @@
+(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 get-user-tokens
+  [user-id]
+  (db/select Token {:user_id user-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))
+
+(defn update
+  [id new-token]
+  (db/update! Token id {:oauth_token new-token}))

+ 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 config/website-uri)
+      (assoc :cookies cookie/delete-token)))
+
+(defn get
+  [id]
+  (db/select-one User :id 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
+  [user-id]
+  (cookie/token-cookie
+   {:access-token  (jwt/sign {:id user-id})
+    :refresh-token (refresh-token/create user-id)}))

+ 16 - 0
backend/src/backend/db_migrate.clj

@@ -0,0 +1,16 @@
+(ns backend.db-migrate
+  (:require [ragtime.jdbc :as jdbc]
+            [ragtime.repl :as repl]))
+
+;; db migrations
+(defn load-config
+  [db]
+  {:datastore  (jdbc/sql-database db)
+   :migrations (jdbc/load-resources "migrations")})
+
+(defn migrate [db]
+  (prn "db: " db)
+  (repl/migrate (load-config db)))
+
+(defn rollback [db]
+  (repl/rollback (load-config db)))

+ 29 - 0
backend/src/backend/interceptors.clj

@@ -0,0 +1,29 @@
+(ns backend.interceptors
+  (:require [io.pedestal.interceptor :refer [interceptor]]
+            [backend.cookie :as cookie]
+            [backend.jwt :as jwt]
+            [backend.db.user :as u]
+            [backend.util :as util]))
+
+(def cookie-interceptor
+  {:name ::cookie-authenticate
+   :enter
+   (fn [{:keys [request] :as context}]
+     (let [tokens (cookie/get-token request)]
+       (if tokens
+         (let [{:keys [access-token refresh-token]} tokens]
+           (if access-token
+             (try
+               (let [user (jwt/unsign access-token)
+                     uid (some-> (:id user) util/->uuid)
+                     user (u/get uid)]
+                 (if (:id user)
+                   (-> context
+                       (assoc-in [:request :app-context :uid] uid)
+                       (assoc-in [:request :app-context :user] user))
+                   context))
+               (catch Exception e       ; token is expired
+                 (when (= (ex-data e)
+                          {:type :validation, :cause :exp})
+                   (assoc context :response (u/logout)))))))
+         context)))})

+ 23 - 0
backend/src/backend/jwt.clj

@@ -0,0 +1,23 @@
+(ns backend.jwt
+  (:require [buddy.sign.jwt :as jwt]
+            [clj-time.core :as time]
+            [backend.config :refer [config]]))
+
+(defonce secret (:jwt-secret config))
+
+(defn sign
+  "Serialize and sign a token with defined claims"
+  ([m]
+   (sign m (* 60 60 12)))
+  ([m expire-secs]
+   (let [claims (assoc m
+                       :exp (time/plus (time/now) (time/seconds expire-secs)))]
+     (jwt/sign claims secret))))
+
+(defn unsign
+  [token]
+  (jwt/unsign token secret))
+
+(defn unsign-skip-validation
+  [token]
+  (jwt/unsign token secret {:skip-validation true}))

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

@@ -0,0 +1,103 @@
+(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]
+            [backend.db.token :as token]
+            [backend.db.repo :as repo]
+            [ring.util.response :as resp]
+            [backend.views.home :as home]
+            [backend.interceptors :as interceptors]))
+
+;; TODO: spec validate, authorization (owner?)
+
+(def routes
+  [["/swagger.json"
+    {:get {:no-doc true
+           :swagger {:info {:title "logseq api"
+                            :description "with pedestal & reitit-http"}}
+           :handler (swagger/create-swagger-handler)}}]
+
+   ["/"
+    {:get {:no-doc true
+           :handler (fn [_req]
+                      {:status 200
+                       :body (home/home)})}}]
+
+   ["/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,repo")
+                    url (social/getAuthorizationUrl social)]
+                (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/redirect config/website-uri)
+                      (assoc :cookies (u/generate-tokens (:id user))))
+                  {:status 500
+                   :body "Internal Error"})
+                {:status 401
+                 :body "Invalid request"}))}}]]
+   ["/api/v1" {:interceptors [interceptors/cookie-interceptor]}
+    ["/me"
+     {:get {:summary "Get current user's information"
+            :handler
+            (fn [{:keys [app-context] :as req}]
+              (prn "request: " req)
+              (if-let [user (:user app-context)]
+                (let [user-id (:id user)]
+                  {:status 200
+                   :body {:user user
+                          :tokens (token/get-user-tokens user-id)
+                          :repos (repo/get-user-repos user-id)}})
+                {:status 404
+                 :body "not-found"}))}}]
+
+    ["/repos"
+     {:post {:summary "Add a repo"
+             :handler
+             (fn [{:keys [app-context body-params] :as req}]
+               (let [user (:user app-context)
+                     result (repo/insert {:user_id (:id user)
+                                          :url (:url body-params)})]
+                 {:status 201
+                  :body result}))}
+      }]
+
+    ["/repos/:id"
+     {:patch {:summary "Update a repo's url"
+              :handler
+              (fn [{:keys [app-context params body-params] :as req}]
+                (let [user (:user app-context)
+                      result (repo/update (:id params)
+                                          (:url body-params))]
+                  {:status 200
+                   :body result}))}
+      :delete {:summary "Delete a repo"
+               :handler
+               (fn [{:keys [app-context params] :as req}]
+                 (let [user (:user app-context)
+                       result (repo/delete (:id params))]
+                   {:status 200
+                    :body {:result true}}))}}]]])

+ 101 - 0
backend/src/backend/system.clj

@@ -0,0 +1,101 @@
+(ns backend.system
+  (:require [io.pedestal.http :as server]
+            [reitit.ring :as ring]
+            [reitit.http :as http]
+            [reitit.coercion.spec]
+            [reitit.swagger :as swagger]
+            [reitit.swagger-ui :as swagger-ui]
+            [reitit.http.coercion :as coercion]
+            [reitit.dev.pretty :as pretty]
+            [reitit.http.interceptors.parameters :as parameters]
+            [reitit.http.interceptors.muuntaja :as muuntaja]
+            [reitit.http.interceptors.exception :as exception]
+            [reitit.http.interceptors.multipart :as multipart]
+            [reitit.http.interceptors.dev :as dev]
+            [reitit.http.spec :as spec]
+            [spec-tools.spell :as spell]
+            [io.pedestal.http :as server]
+            [reitit.pedestal :as pedestal]
+            [clojure.core.async :as a]
+            [muuntaja.core :as m]
+            [com.stuartsierra.component :as component]
+            [backend.components.http :as component-http]
+            [backend.components.hikari :as hikari]
+            [backend.routes :as routes]
+            [backend.config :as config]
+            [io.pedestal.http.ring-middlewares :as ring-middlewares]))
+
+(def router
+  (pedestal/routing-interceptor
+   (http/router
+    routes/routes
+
+    {;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs
+     ;;:validate spec/validate ;; enable spec validation for route data
+     ;;:reitit.spec/wrap spell/closed ;; strict top-level validation
+     ;; :exception pretty/exception
+     :data {:coercion reitit.coercion.spec/coercion
+            :muuntaja m/instance
+            :interceptors [;; swagger feature
+                           swagger/swagger-feature
+                           ;; query-params & form-params
+                           (parameters/parameters-interceptor)
+                           ;; content-negotiation
+                           (muuntaja/format-negotiate-interceptor)
+                           ;; encoding response body
+                           (muuntaja/format-response-interceptor)
+                           ;; exception handling
+                           ;; (exception/exception-interceptor)
+                           ;; decoding request body
+                           (muuntaja/format-request-interceptor)
+                           ;; coercing response bodys
+                           (coercion/coerce-response-interceptor)
+                           ;; coercing request parameters
+                           (coercion/coerce-request-interceptor)
+                           ;; multipart
+                           (multipart/multipart-interceptor)]}})
+
+   ;; optional default ring handler (if no routes have matched)
+   (ring/routes
+    (swagger-ui/create-swagger-ui-handler
+     {:path "/swagger"
+      :config {:validatorUrl nil
+               :operationsSorter "alpha"}})
+    (ring/create-resource-handler)
+    (ring/create-default-handler))))
+
+(defn merge-interceptors-map
+  [system-map interceptors]
+  (update system-map :io.pedestal.http/interceptors
+          (fn [old]
+            (vec (concat interceptors old)))))
+
+(defn new-system
+  [{:keys [env port hikari-spec] :as config}]
+  (let [service-map (-> {:env env
+                         ::server/type :jetty
+                         ::server/port port
+                         ::server/join? false
+                         ;; no pedestal routes
+                         ::server/routes []
+                         ;; allow serving the swagger-ui styles & scripts from self
+                         ;; ::server/secure-headers {:content-security-policy-settings
+                         ;;                          {:default-src "'self'"
+                         ;;                           :style-src "'self' 'unsafe-inline'"
+                         ;;                           :script-src "'self' 'unsafe-inline'"}}
+                         ::server/secure-headers {:content-security-policy-settings {:object-src "'none'"}}
+                         ::server/resource-path "/public"}
+                        (server/default-interceptors)
+                        ;; use the reitit router
+                        (pedestal/replace-last-interceptor router))
+        service-map (merge-interceptors-map
+                     service-map
+                     [ring-middlewares/cookies
+                      server/html-body])
+        service-map (if config/dev? (server/dev-interceptors service-map) service-map)]
+    (component/system-map :service-map service-map
+                          :hikari (hikari/new-hikari-cp hikari-spec)
+                          :http
+                          (component/using
+                           (component-http/new-server)
+                           [:service-map]))))

+ 82 - 0
backend/src/backend/util.clj

@@ -0,0 +1,82 @@
+(ns backend.util
+  (:require [clojure.string :as str]
+            [clj-time
+             [coerce :as tc]
+             [core :as t]
+             [format :as tf]])
+  (:import  [java.util UUID]
+            [java.util TimerTask Timer]))
+(defn uuid
+  "Generate uuid."
+  []
+  (UUID/randomUUID))
+
+(defn ->uuid
+  [s]
+  (if (uuid? s)
+    s
+    (UUID/fromString s)))
+
+(defn update-if
+  "Update m if k exists."
+  [m k f]
+  (if-let [v (get m k)]
+    (assoc m k (f v))
+    m))
+
+(defn dissoc-in
+  "Dissociates an entry from a nested associative structure returning a new
+  nested structure. keys is a sequence of keys. Any empty maps that result
+  will not be present in the new structure."
+  [m [k & ks :as keys]]
+  (if ks
+    (if-let [nextmap (get m k)]
+      (let [newmap (dissoc-in nextmap ks)]
+        (if (seq newmap)
+          (assoc m k newmap)
+          (dissoc m k)))
+      m)
+    (dissoc m k)))
+
+(defmacro doseq-indexed
+  "loops over a set of values, binding index-sym to the 0-based index of each value"
+  ([[val-sym values index-sym] & code]
+   `(loop [vals# (seq ~values)
+           ~index-sym (long 0)]
+      (if vals#
+        (let [~val-sym (first vals#)]
+          ~@code
+          (recur (next vals#) (inc ~index-sym)))
+        nil))))
+
+(defn indexed [coll] (map-indexed vector coll))
+
+(defn set-timeout [f interval]
+  (let [task (proxy [TimerTask] []
+               (run [] (f)))
+        timer (new Timer)]
+    (.schedule timer task (long interval))
+    timer))
+
+;; http://yellerapp.com/posts/2014-12-11-14-race-condition-in-clojure-println.html
+(defn safe-println [& more]
+  (.write *out* (str (clojure.string/join " " more) "\n")))
+
+(defn safe->int
+  [s]
+  (if (string? s)
+    (Integer/parseInt s)
+    s))
+
+(defn remove-nils
+  [m]
+  (reduce (fn [acc [k v]] (if v (assoc acc k v)
+                              acc))
+          {} m))
+
+(defn deep-merge [& maps]
+  (apply merge-with (fn [& args]
+                      (if (every? map? args)
+                        (apply deep-merge args)
+                        (last args)))
+    maps))

+ 29 - 0
backend/src/backend/views/home.clj

@@ -0,0 +1,29 @@
+(ns backend.views.home
+  (:require [hiccup.page :as html]))
+
+(defn home
+  []
+  (html/html5
+   [:head
+    [:meta {:charset "utf-8"}]
+    [:meta
+     {:content
+      "minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no",
+      :name "viewport"}]
+    [:link {:type "text/css", :href "css/tailwind.min.css", :rel "stylesheet"}]
+    [:link {:type "text/css", :href "css/style.css", :rel "stylesheet"}]
+    [:link
+     {:href
+      "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap",
+      :rel "stylesheet"}]
+    [:link {:href "css/highlight.css", :rel "stylesheet"}]
+    [:title "Logseq"]]
+   [:body
+    [:div#root]
+    [:script {:src "https://unpkg.com/@isomorphic-git/[email protected]/dist/lightning-fs.min.js"}]
+    [:script {:src "https://unpkg.com/[email protected]/dist/bundle.umd.min.js"}]
+    [:script
+     "window.fs = new LightningFS('logseq');git.plugins.set('fs', window.fs);window.pfs = window.fs.promises;"]
+    [:script {:src "https://cdn.jsdelivr.net/gh/alpinejs/[email protected]/dist/alpine.js" :defer true}]
+    [:script {:src "/js/main.js"}]
+    [:script {:src "/js/highlight.pack.js"}]]))

+ 7 - 0
backend/test/backend/core_test.clj

@@ -0,0 +1,7 @@
+(ns backend.core-test
+  (:require [clojure.test :refer :all]
+            [backend.core :refer :all]))
+
+(deftest a-test
+  (testing "FIXME, I fail."
+    (is (= 0 1))))

+ 0 - 20
now.json

@@ -1,20 +0,0 @@
-{
-  "version": 2,
-  "env": {
-    "NODE_ENV": "production",
-    "GITHUB_APP_KEY": "@github_app_key",
-    "GITHUB_APP_SECRET": "@github_app_secret",
-    "GITHUB_REDIRECT_URI": "@github_redirect_uri",
-    "COOKIE_SECRET": "@cookie_secret"
-  },
-  "builds": [
-    { "src": "api/index.js", "use": "@now/node"},
-    { "src": "/public/**", "use": "@now/static"}
-  ],
-  "routes": [
-      { "src": "/api(.*)", "dest": "api/index.js" },
-      { "src": "/static/(.*)", "dest": "/public/static/$1" },
-      { "src": "/(.+js|.+css|.+png|.+svg|.+jpg|.+ico|robots.txt|.+map)", "dest": "/public/$1" },
-      { "src": "/(.*)", "dest": "/public/index.html" }
-  ]
-}

+ 0 - 1
public

@@ -1 +0,0 @@
-web/public

+ 3 - 3
readme.org

@@ -1,17 +1,17 @@
-* Gitnotes
+* Logseq
   A client-only note app which sync with your github repo. *Use it at your own risk!!!!*
   A client-only note app which sync with your github repo. *Use it at your own risk!!!!*
   #+CAPTION: gitnote screenshot
   #+CAPTION: gitnote screenshot
   #+NAME:   fig:screenshot.png
   #+NAME:   fig:screenshot.png
   [[./images/screenshot.png]]
   [[./images/screenshot.png]]
 
 
 ** Demo
 ** Demo
-   http://gitnotes.tiensonqin.now.sh/
+   http://logseq.tiensonqin.now.sh/
 
 
 ** Setup
 ** Setup
 *** Create a github basic token
 *** Create a github basic token
     1. Go to github.com Settings / Developer settings / Personal access tokens
     1. Go to github.com Settings / Developer settings / Personal access tokens
     2. Click the button *Generate new token*
     2. Click the button *Generate new token*
-    3. Input =gitnotes= or anything in the *Note*
+    3. Input =logseq= or anything in the *Note*
     4. Select the =repo= checkbox under *Select scopes*
     4. Select the =repo= checkbox under *Select scopes*
     5. Click the *Generate token* button
     5. Click the *Generate token* button
 *** Go to the demo or your now.sh instance
 *** Go to the demo or your now.sh instance

+ 3 - 3
web/.gitignore

@@ -1,7 +1,7 @@
 node_modules/
 node_modules/
-public/static/js/cljs-runtime
-public/static/js/main.js
-public/static/js/manifest.edn
+public/js/cljs-runtime
+public/js/main.js
+public/js/manifest.edn
 
 
 /.cpcache
 /.cpcache
 /target
 /target

+ 2 - 2
web/package.json

@@ -1,5 +1,5 @@
 {
 {
-  "name": "gitnotes",
+  "name": "logseq",
   "version": "0.0.1",
   "version": "0.0.1",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
@@ -9,7 +9,7 @@
     "watch": "npx shadow-cljs watch app",
     "watch": "npx shadow-cljs watch app",
     "release": "npx shadow-cljs release app",
     "release": "npx shadow-cljs release app",
     "server": "npx shadow-cljs server;",
     "server": "npx shadow-cljs server;",
-    "clean": "rm -rf target; rm -rf public/static/js/compiled; rm -rf public/static/js/cljs-runtime"
+    "clean": "rm -rf target; rm -rf public/js/compiled; rm -rf public/js/cljs-runtime"
   },
   },
   "dependencies": {
   "dependencies": {
     "browserfs": "^1.4.3",
     "browserfs": "^1.4.3",

+ 0 - 0
web/public/static/css/highlight.css → web/public/css/highlight.css


+ 0 - 0
web/public/static/css/org.css → web/public/css/org.css


+ 3 - 3
web/public/static/css/style.css → web/public/css/style.css

@@ -209,15 +209,15 @@ dt { font-weight: bold; }
 
 
 /* scroll-bg */
 /* scroll-bg */
 .scroll-background {
 .scroll-background {
-    height: 400%; width: 400%; top: -25%; left: -100%; background-size: 800px auto; background-image: url('/static/img/hero-pattern-lg.png');
+    height: 400%; width: 400%; top: -25%; left: -100%; background-size: 800px auto; background-image: url('/img/hero-pattern-lg.png');
 }
 }
 
 
 .angled-background {
 .angled-background {
-    background-image: url('/static/img/angled-background.svg'); background-size: 100% auto; background-position: -5px -5px;
+    background-image: url('/img/angled-background.svg'); background-size: 100% auto; background-position: -5px -5px;
 }
 }
 
 
 .scroll-background-2 {
 .scroll-background-2 {
-    height: 800%; width: 400%; top: -100%; left: -100%; background-size: 400px auto; background-image: url('/static/img/hero-pattern-lg.png');
+    height: 800%; width: 400%; top: -100%; left: -100%; background-size: 400px auto; background-image: url('/img/hero-pattern-lg.png');
 }
 }
 
 
 @-webkit-keyframes scrollSmall {0%{transform:rotate(-13deg) translateY(0)}to{transform:rotate(-13deg) translateY(-639px)}}
 @-webkit-keyframes scrollSmall {0%{transform:rotate(-13deg) translateY(0)}to{transform:rotate(-13deg) translateY(-639px)}}

+ 0 - 0
web/public/static/css/tailwind.min.css → web/public/css/tailwind.min.css


+ 0 - 0
web/public/static/img/angled-background.svg → web/public/img/angled-background.svg


+ 0 - 0
web/public/static/img/hero-pattern-lg.png → web/public/img/hero-pattern-lg.png


+ 0 - 0
web/public/static/img/logo.png → web/public/img/logo.png


+ 8 - 8
web/public/index.html

@@ -3,13 +3,13 @@
   <head>
   <head>
     <meta charset="utf-8">
     <meta charset="utf-8">
     <meta content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
     <meta content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
-    <!-- <link href="/static/css/tailwind.min.css" rel="stylesheet" type="text/css"> -->
+    <!-- <link href="/css/tailwind.min.css" rel="stylesheet" type="text/css"> -->
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css">
-    <link href="/static/css/org.css" rel="stylesheet">
-    <link href="/static/css/style.css" rel="stylesheet" type="text/css">
+    <link href="/css/org.css" rel="stylesheet">
+    <link href="/css/style.css" rel="stylesheet" type="text/css">
     <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap" rel="stylesheet">
     <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap" rel="stylesheet">
-    <link href="/static/css/highlight.css" rel="stylesheet">
-    <title>Gitnotes</title>
+    <link href="/css/highlight.css" rel="stylesheet">
+    <title>Logseq</title>
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>
@@ -17,11 +17,11 @@
     </script>
     </script>
     <script src="https://unpkg.com/[email protected]/dist/bundle.umd.min.js"></script>
     <script src="https://unpkg.com/[email protected]/dist/bundle.umd.min.js"></script>
     <script>
     <script>
-      window.fs = new LightningFS('gitnotes');
+      window.fs = new LightningFS('logseq');
       git.plugins.set('fs', window.fs);
       git.plugins.set('fs', window.fs);
       window.pfs = window.fs.promises;
       window.pfs = window.fs.promises;
     </script>
     </script>
-    <script src="/static/js/highlight.pack.js"></script>
-    <script src="/static/js/main.js"></script>
+    <script src="/js/highlight.pack.js"></script>
+    <script src="/js/main.js"></script>
   </body>
   </body>
 </html>
 </html>

+ 0 - 0
web/public/static/js/highlight.pack.js → web/public/js/highlight.pack.js


File diff suppressed because it is too large
+ 0 - 0
web/public/static/js/manifest.edn


+ 4 - 4
web/shadow-cljs.edn

@@ -20,16 +20,16 @@
   {:target :browser
   {:target :browser
    :modules {:main {:init-fn frontend.core/init}}
    :modules {:main {:init-fn frontend.core/init}}
 
 
-   :output-dir "public/static/js"
-   :asset-path "/static/js"
+   :output-dir "public/js"
+   :asset-path "/js"
 
 
    :compiler-options {:infer-externs :auto
    :compiler-options {:infer-externs :auto
                       :externs ["datascript/externs.js"]}
                       :externs ["datascript/externs.js"]}
    ;; TODO: purge-css not working properly
    ;; TODO: purge-css not working properly
    ;; :build-hooks [(shadow.hooks/purge-css
    ;; :build-hooks [(shadow.hooks/purge-css
    ;;                {:css-source "node_modules/tailwindcss/dist/tailwind.min.css"
    ;;                {:css-source "node_modules/tailwindcss/dist/tailwind.min.css"
-   ;;                 :js-globs   ["public/static/js/*.js"]
-   ;;                 :public-dir    "public/static/css"})]
+   ;;                 :js-globs   ["public/js/*.js"]
+   ;;                 :public-dir    "public/css"})]
    :devtools
    :devtools
    ;; before live-reloading any code call this function
    ;; before live-reloading any code call this function
    {:before-load frontend.core/stop
    {:before-load frontend.core/stop

+ 4 - 4
web/src/frontend/components/home.cljs

@@ -22,8 +22,8 @@
       [:div.flex.items-center.justify-between
       [:div.flex.items-center.justify-between
        [:div
        [:div
         [:img.h-6.lg:h-8.xl:h-9
         [:img.h-6.lg:h-8.xl:h-9
-         {:alt "Gitnotes",
-          :src "/static/img/tailwindui-logo-on-dark.svg"}]]
+         {:alt "Logseq",
+          :src "/img/logo.png"}]]
        [:div
        [:div
         [:a.text-sm.font-semibold.text-white.focus:outline-none.focus:underline
         [:a.text-sm.font-semibold.text-white.focus:outline-none.focus:underline
          {:href (str config/api "login/github")}
          {:href (str config/api "login/github")}
@@ -55,7 +55,7 @@
         {:href "https://twitter.com/adamwathan"}]
         {:href "https://twitter.com/adamwathan"}]
        [:div.flex-shrink-0
        [:div.flex-shrink-0
         [:img.h-12.w-12.rounded-full.border-2.border-white
         [:img.h-12.w-12.rounded-full.border-2.border-white
-         {:alt "", :src "/static/img/adam.jpg"}]]
+         {:alt "", :src "/img/adam.jpg"}]]
        [:div.ml-3
        [:div.ml-3
         [:p.font-semibold.text-white.leading-tight "Adam Wathan"]
         [:p.font-semibold.text-white.leading-tight "Adam Wathan"]
         [:p.text-sm.text-gray-500.leading-tight
         [:p.text-sm.text-gray-500.leading-tight
@@ -64,7 +64,7 @@
         {:href "https://twitter.com/steveschoger"}]
         {:href "https://twitter.com/steveschoger"}]
        [:div.flex-shrink-0
        [:div.flex-shrink-0
         [:img.h-12.w-12.rounded-full.border-2.border-white
         [:img.h-12.w-12.rounded-full.border-2.border-white
-         {:alt "", :src "/static/img/steve.jpg"}]]
+         {:alt "", :src "/img/steve.jpg"}]]
        [:div.ml-3
        [:div.ml-3
         [:p.font-semibold.text-white.leading-tight "Steve Schoger"]
         [:p.font-semibold.text-white.leading-tight "Steve Schoger"]
         [:p.text-sm.text-gray-500.leading-tight
         [:p.text-sm.text-gray-500.leading-tight

+ 4 - 4
web/src/frontend/components/sidebar.cljs

@@ -90,8 +90,8 @@
               :stroke-linecap "round"}]]]])
               :stroke-linecap "round"}]]]])
        [:div.flex-shrink-0.flex.items-center.h-16.px-4.bg-gray-900
        [:div.flex-shrink-0.flex.items-center.h-16.px-4.bg-gray-900
         [:img.h-8.w-auto
         [:img.h-8.w-auto
-         {:alt "Gitnotes",
-          :src "/static/img/logo.png"}]]
+         {:alt "Logseq",
+          :src "/img/logo.png"}]]
        [:div.flex-1.h-0.overflow-y-auto
        [:div.flex-1.h-0.overflow-y-auto
         (sidebar-nav)]
         (sidebar-nav)]
        ]]
        ]]
@@ -99,8 +99,8 @@
       [:div.flex.flex-col.w-64
       [:div.flex.flex-col.w-64
        [:div.flex.items-center.h-16.flex-shrink-0.px-4.bg-gray-900
        [:div.flex.items-center.h-16.flex-shrink-0.px-4.bg-gray-900
         [:img.h-8.w-auto
         [:img.h-8.w-auto
-         {:alt "Gitnotes",
-          :src "/static/img/logo.png"}]]
+         {:alt "Logseq",
+          :src "/img/logo.png"}]]
        [:div.h-0.flex-1.flex.flex-col.overflow-y-auto
        [:div.h-0.flex-1.flex.flex-col.overflow-y-auto
         (sidebar-nav)]]]
         (sidebar-nav)]]]
      [:div.flex.flex-col.w-0.flex-1.overflow-hidden
      [:div.flex.flex-col.w-0.flex-1.overflow-hidden

+ 3 - 3
web/src/frontend/config.cljs

@@ -6,9 +6,9 @@
 (def website
 (def website
   (if dev?
   (if dev?
     "http://localhost:8080"
     "http://localhost:8080"
-    "https://gitnotes.now.sh"))
+    "https://logseq.now.sh"))
 
 
 (def api
 (def api
   (if dev?
   (if dev?
-    "http://localhost:3000/api/"
-    (str website "/api/")))
+    "http://localhost:3000/api/v1/"
+    (str website "/api/v1/")))

+ 1 - 1
web/src/frontend/db.cljs

@@ -10,7 +10,7 @@
 
 
 ;; TODO: don't persistent :github/token
 ;; TODO: don't persistent :github/token
 
 
-(def datascript-db "gitnotes/DB")
+(def datascript-db "logseq/DB")
 (def schema
 (def schema
   {:db/ident        {:db/unique :db.unique/identity}
   {:db/ident        {:db/unique :db.unique/identity}
    :github/token    {}
    :github/token    {}

+ 1 - 1
web/src/frontend/handler.cljs

@@ -160,7 +160,7 @@
 
 
 (defn new-notification
 (defn new-notification
   [text]
   [text]
-  (js/Notification. "Gitnotes" #js {:body text
+  (js/Notification. "Logseq" #js {:body text
                                     ;; :icon logo
                                     ;; :icon logo
                                     }))
                                     }))
 
 

Some files were not shown because too many files changed in this diff