Browse Source

First commit

master
Gisle Aune 4 years ago
commit
190e1eb01d
  1. 14
      .idea/dictionaries/gisle.xml
  2. 6
      .idea/misc.xml
  3. 8
      .idea/modules.xml
  4. 8
      .idea/stufflog.iml
  5. 29
      .idea/watcherTasks.xml
  6. 768
      .idea/workspace.xml
  7. 22
      Dockerfile
  8. 137
      api/activity.go
  9. 178
      api/period.go
  10. 75
      api/user.go
  11. 28
      config/config.go
  12. 6
      config/database.go
  13. 6
      config/server.go
  14. 5
      config/users.go
  15. 26
      database/database.go
  16. 229
      database/drivers/bolt/activity.go
  17. 123
      database/drivers/bolt/activity_test.go
  18. 76
      database/drivers/bolt/db.go
  19. 24
      database/drivers/bolt/db_test.go
  20. 339
      database/drivers/bolt/idx.go
  21. 215
      database/drivers/bolt/idx_test.go
  22. 288
      database/drivers/bolt/period.go
  23. 91
      database/drivers/bolt/user.go
  24. 80
      database/drivers/bolt/user_test.go
  25. 197
      database/drivers/bolt/usersession.go
  26. 15
      database/repositories/activity.go
  27. 16
      database/repositories/period.go
  28. 14
      database/repositories/user.go
  29. 16
      database/repositories/usersessions.go
  30. 17
      go.mod
  31. 84
      go.sum
  32. 40
      internal/generate/id.go
  33. 87
      main.go
  34. 119
      models/activity.go
  35. 356
      models/period.go
  36. 44
      models/user.go
  37. 221
      services/auth.go
  38. 98
      services/scoring.go
  39. 82
      slerrors/slerrors.go
  40. BIN
      stufflog
  41. 4
      svelte-ui/.gitignore
  42. 64
      svelte-ui/README.md
  43. 6465
      svelte-ui/package-lock.json
  44. 29
      svelte-ui/package.json
  45. BIN
      svelte-ui/public/favicon.png
  46. 63
      svelte-ui/public/global.css
  47. 17
      svelte-ui/public/index.html
  48. 64
      svelte-ui/src/App.svelte
  49. 22
      svelte-ui/src/LoginCheck.svelte
  50. 139
      svelte-ui/src/api/stufflog.js
  51. 45
      svelte-ui/src/components/ActivityIcon.svelte
  52. 42
      svelte-ui/src/components/ActivityIconSelect.svelte
  53. 40
      svelte-ui/src/components/AddBoi.svelte
  54. 41
      svelte-ui/src/components/Boi.svelte
  55. 51
      svelte-ui/src/components/Col.svelte
  56. 16
      svelte-ui/src/components/Link.svelte
  57. 20
      svelte-ui/src/components/Menu.svelte
  58. 28
      svelte-ui/src/components/MenuItem.svelte
  59. 168
      svelte-ui/src/components/ModalFrame.svelte
  60. 117
      svelte-ui/src/components/PointsBar.svelte
  61. 28
      svelte-ui/src/components/Property.svelte
  62. 8
      svelte-ui/src/components/Row.svelte
  63. 43
      svelte-ui/src/components/SubGoalInput.svelte
  64. 10
      svelte-ui/src/hooks/Modal.svelte
  65. 10
      svelte-ui/src/main.js
  66. 116
      svelte-ui/src/modals/AddPeriodGoalModal.svelte
  67. 111
      svelte-ui/src/modals/AddPeriodLogModal.svelte
  68. 55
      svelte-ui/src/modals/AddSubActivityModal.svelte
  69. 35
      svelte-ui/src/modals/CreateActivityModal.svelte
  70. 53
      svelte-ui/src/modals/CreatePeriodModal.svelte
  71. 32
      svelte-ui/src/modals/DeleteActivityModal.svelte
  72. 32
      svelte-ui/src/modals/DeletePeriodModal.svelte
  73. 37
      svelte-ui/src/modals/EditActivityModal.svelte
  74. 51
      svelte-ui/src/modals/EditPeriodModal.svelte
  75. 57
      svelte-ui/src/modals/EditSubActivityModal.svelte
  76. 62
      svelte-ui/src/modals/InfoPeriodLogModal.svelte
  77. 40
      svelte-ui/src/modals/LoginModal.svelte
  78. 34
      svelte-ui/src/modals/RemovePeriodGoalModal.svelte
  79. 38
      svelte-ui/src/modals/RemovePeriodLogModal.svelte
  80. 34
      svelte-ui/src/modals/RemoveSubActivityModal.svelte
  81. 61
      svelte-ui/src/models/activity.d.ts
  82. 25
      svelte-ui/src/models/activity.js
  83. 128
      svelte-ui/src/models/period.d.ts
  84. 77
      svelte-ui/src/models/period.js
  85. 91
      svelte-ui/src/routes/ActivitiesPage.svelte
  86. 168
      svelte-ui/src/routes/LogPage.svelte
  87. 49
      svelte-ui/src/stores/auth.js
  88. 19
      svelte-ui/src/stores/modal.js
  89. 203
      svelte-ui/src/stores/stufflog.js
  90. 16
      svelte-ui/src/utils/dateStr.js
  91. 66
      svelte-ui/webpack.config.js

14
.idea/dictionaries/gisle.xml

@ -0,0 +1,14 @@
<component name="ProjectDictionaryState">
<dictionary name="gisle">
<words>
<w>gctx</w>
<w>msgpack</w>
<w>roleplay</w>
<w>serr</w>
<w>sessid</w>
<w>slerr</w>
<w>slerror</w>
<w>stufflog</w>
</words>
</dictionary>
</component>

6
.idea/misc.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

8
.idea/modules.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/stufflog.iml" filepath="$PROJECT_DIR$/.idea/stufflog.iml" />
</modules>
</component>
</project>

8
.idea/stufflog.iml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

29
.idea/watcherTasks.xml

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="fmt $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="go" />
<option name="immediateSync" value="false" />
<option name="name" value="go fmt" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$GoExecPath$" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$ProjectFileDir$" />
<envs>
<env name="GOROOT" value="$GOROOT$" />
<env name="GOPATH" value="$GOPATH$" />
<env name="PATH" value="$GoBinDirs$" />
</envs>
</TaskOptions>
</component>
</project>

768
.idea/workspace.xml

@ -0,0 +1,768 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BookmarkManager">
<bookmark url="file://$PROJECT_DIR$/slerrors/slerrors.go" line="38" />
</component>
<component name="ChangeListManager">
<list default="true" id="ee4ecc12-436b-456a-8502-29eb7cf43ef4" name="Default Changelist" comment="" />
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="CoverageViewManager">
<option name="myElementSize" value="1111" />
</component>
<component name="FileEditorManager">
<leaf SIDE_TABS_SIZE_LIMIT_KEY="450">
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/period.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="8910">
<caret line="172" column="18" selection-start-line="172" selection-start-column="18" selection-end-line="172" selection-end-column="18" />
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/models/user.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="486">
<caret line="14" column="1" selection-start-line="14" selection-start-column="1" selection-end-line="14" selection-end-column="1" />
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/main.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1386">
<caret line="81" lean-forward="true" selection-start-line="81" selection-end-line="81" />
<folding>
<element signature="e#14#242#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file:///usr/lib/go/src/net/http/fs.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="609">
<caret line="94" column="5" selection-start-line="94" selection-start-column="5" selection-end-line="94" selection-end-column="5" />
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/models/activity.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2700">
<caret line="53" column="32" selection-start-line="53" selection-start-column="32" selection-end-line="53" selection-end-column="32" />
<folding>
<element signature="e#16#111#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/Dockerfile">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1134">
<caret line="21" column="76" selection-start-line="21" selection-start-column="76" selection-end-line="21" selection-end-column="76" />
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/database/repositories/period.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="540">
<caret line="13" column="2" selection-start-line="13" selection-start-column="2" selection-end-line="13" selection-end-column="2" />
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/database/repositories/activity.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="378">
<caret line="10" column="2" selection-start-line="10" selection-start-column="2" selection-end-line="10" selection-end-column="2" />
<folding>
<element signature="e#22#79#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/models/period.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="16308">
<caret line="306" column="41" selection-start-line="306" selection-start-column="41" selection-end-line="306" selection-end-column="41" />
</state>
</provider>
</entry>
</file>
<file pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/services/scoring.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3456">
<caret line="70" column="2" selection-start-line="70" selection-start-column="2" selection-end-line="70" selection-end-column="2" />
</state>
</provider>
</entry>
</file>
</leaf>
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Go File" />
</list>
</option>
</component>
<component name="FindInProjectRecents">
<findStrings>
<find>User</find>
<find>bnAct</find>
<find>WithContext</find>
<find>Activity</find>
<find>an Period</find>
<find>bn</find>
<find>key required</find>
<find>duplicate value</find>
<find>PeriodLog</find>
</findStrings>
</component>
<component name="GOROOT" path="/usr/lib/go" />
<component name="GoLibraries">
<option name="indexEntireGoPath" value="false" />
</component>
<component name="IdeDocumentHistory">
<option name="CHANGED_PATHS">
<list>
<option value="$PROJECT_DIR$/models/Interval.go" />
<option value="$PROJECT_DIR$/database/repositories.go" />
<option value="$PROJECT_DIR$/config/database.go" />
<option value="$PROJECT_DIR$/database/bolt/user.go" />
<option value="$PROJECT_DIR$/database/bolt/db.go" />
<option value="$PROJECT_DIR$/database/repos/user.go" />
<option value="$PROJECT_DIR$/database/repositories/errors.go" />
<option value="$PROJECT_DIR$/go.mod" />
<option value="$PROJECT_DIR$/database/generate/id.go" />
<option value="$PROJECT_DIR$/database/repositories/usersessions.go" />
<option value="$PROJECT_DIR$/database/repositories/goal.go" />
<option value="$PROJECT_DIR$/slerrors.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/user.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/usersession.go" />
<option value="$PROJECT_DIR$/database/repositories/user.go" />
<option value="$PROJECT_DIR$/database/repositories/activity.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/db.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/idx_test.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/db_test.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/user_test.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/activity_test.go" />
<option value="$PROJECT_DIR$/models/user.go" />
<option value="$PROJECT_DIR$/config/users.go" />
<option value="$PROJECT_DIR$/config/server.go" />
<option value="$PROJECT_DIR$/config/config.go" />
<option value="$PROJECT_DIR$/database/database.go" />
<option value="$PROJECT_DIR$/services/period.go" />
<option value="$PROJECT_DIR$/services/scorer.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/idx.go" />
<option value="$PROJECT_DIR$/api/period.go" />
<option value="$PROJECT_DIR$/services/auth.go" />
<option value="$PROJECT_DIR$/database/repositories/period.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/activity.go" />
<option value="$PROJECT_DIR$/slerrors/slerrors.go" />
<option value="$PROJECT_DIR$/api/activity.go" />
<option value="$PROJECT_DIR$/api/user.go" />
<option value="$PROJECT_DIR$/models/activity.go" />
<option value="$PROJECT_DIR$/services/scoring.go" />
<option value="$PROJECT_DIR$/database/drivers/bolt/period.go" />
<option value="$PROJECT_DIR$/models/period.go" />
<option value="$PROJECT_DIR$/Dockerfile" />
<option value="$PROJECT_DIR$/main.go" />
</list>
</option>
</component>
<component name="MacroExpansionManager">
<option name="directoryName" value="ISqLKAuO" />
</component>
<component name="ProjectFrameBounds">
<option name="y" value="41" />
<option name="width" value="3840" />
<option name="height" value="2119" />
</component>
<component name="ProjectView">
<navigator proportions="" version="1">
<foldersAlwaysOnTop value="true" />
</navigator>
<panes>
<pane id="ProjectPane">
<subPane>
<expand>
<path>
<item name="stufflog" type="b2602c69:ProjectViewProjectNode" />
<item name="stufflog" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="stufflog" type="b2602c69:ProjectViewProjectNode" />
<item name="stufflog" type="462c0819:PsiDirectoryNode" />
<item name="api" type="462c0819:PsiDirectoryNode" />
</path>
</expand>
<select />
</subPane>
</pane>
<pane id="Scope" />
</panes>
</component>
<component name="PropertiesComponent">
<property name="DefaultGoTemplateProperty" value="Go File" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="go.gopath.indexing.explicitly.defined" value="true" />
<property name="go.import.settings.migrated" value="true" />
<property name="go.sdk.automatically.set" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/../rpdata-api" />
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
<property name="nodejs_npm_path_reset_for_default_project" value="true" />
<property name="nodejs_package_manager_path" value="npm" />
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/api" />
<recent name="$PROJECT_DIR$/database/drivers/bolt" />
<recent name="$PROJECT_DIR$/database/repositories" />
<recent name="$PROJECT_DIR$/database" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/internal" />
<recent name="$PROJECT_DIR$/database/drivers" />
<recent name="$PROJECT_DIR$/database/repos" />
</key>
</component>
<component name="RunDashboard">
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
</component>
<component name="RunManager" selected="Go Test.go test github.com/gisle/stufflog/database/drivers/bolt">
<configuration name="TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" type="GoTestRunConfiguration" factoryName="Go Test" temporary="true" nameIsGenerated="true">
<module name="stufflog" />
<working_directory value="$PROJECT_DIR$/database/drivers/bolt" />
<framework value="gotest" />
<kind value="PACKAGE" />
<package value="github.com/gisle/stufflog/database/drivers/bolt" />
<directory value="$PROJECT_DIR$/" />
<filePath value="$PROJECT_DIR$/" />
<pattern value="^TestActivityRepository$" />
<method v="2" />
</configuration>
<configuration name="TestIndex in github.com/gisle/stufflog/database/drivers/bolt" type="GoTestRunConfiguration" factoryName="Go Test" temporary="true" nameIsGenerated="true">
<module name="stufflog" />
<working_directory value="$PROJECT_DIR$/database/drivers/bolt" />
<framework value="gotest" />
<kind value="PACKAGE" />
<package value="github.com/gisle/stufflog/database/drivers/bolt" />
<directory value="$PROJECT_DIR$/" />
<filePath value="$PROJECT_DIR$/" />
<pattern value="^TestIndex$" />
<method v="2" />
</configuration>
<configuration name="TestIndex/Check_Initial in github.com/gisle/stufflog/database/drivers/bolt" type="GoTestRunConfiguration" factoryName="Go Test" temporary="true" nameIsGenerated="true">
<module name="stufflog" />
<working_directory value="$PROJECT_DIR$/database/drivers/bolt" />
<framework value="gotest" />
<kind value="PACKAGE" />
<package value="github.com/gisle/stufflog/database/drivers/bolt" />
<directory value="$PROJECT_DIR$/" />
<filePath value="$PROJECT_DIR$/" />
<pattern value="^TestIndex$/^Check_Initial$" />
<method v="2" />
</configuration>
<configuration name="TestUserRepository in github.com/gisle/stufflog/database/drivers/bolt" type="GoTestRunConfiguration" factoryName="Go Test" temporary="true" nameIsGenerated="true">
<module name="stufflog" />
<working_directory value="$PROJECT_DIR$/database/drivers/bolt" />
<framework value="gotest" />
<kind value="PACKAGE" />
<package value="github.com/gisle/stufflog/database/drivers/bolt" />
<directory value="$PROJECT_DIR$/" />
<filePath value="$PROJECT_DIR$/" />
<pattern value="^TestUserRepository$" />
<method v="2" />
</configuration>
<configuration name="go test github.com/gisle/stufflog/database/drivers/bolt" type="GoTestRunConfiguration" factoryName="Go Test" temporary="true" nameIsGenerated="true">
<module name="stufflog" />
<working_directory value="$PROJECT_DIR$/database/drivers/bolt" />
<framework value="gotest" />
<kind value="PACKAGE" />
<package value="github.com/gisle/stufflog/database/drivers/bolt" />
<directory value="$PROJECT_DIR$/" />
<filePath value="$PROJECT_DIR$/" />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Go Test.go test github.com/gisle/stufflog/database/drivers/bolt" />
<item itemvalue="Go Test.TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" />
<item itemvalue="Go Test.TestUserRepository in github.com/gisle/stufflog/database/drivers/bolt" />
<item itemvalue="Go Test.TestIndex in github.com/gisle/stufflog/database/drivers/bolt" />
<item itemvalue="Go Test.TestIndex/Check_Initial in github.com/gisle/stufflog/database/drivers/bolt" />
</list>
</recent_temporary>
</component>
<component name="TestHistory">
<history-entry file="TestActivityRepository_in_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 07m 39s.xml">
<configuration name="TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="TestActivityRepository_in_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 10m 45s.xml">
<configuration name="TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="TestActivityRepository_in_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 11m 21s.xml">
<configuration name="TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="go_test_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 11m 33s.xml">
<configuration name="go test github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="TestActivityRepository_in_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 15m 06s.xml">
<configuration name="TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="TestActivityRepository_in_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 16m 12s.xml">
<configuration name="TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="TestActivityRepository_in_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 18m 32s.xml">
<configuration name="TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="go_test_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 19m 26s.xml">
<configuration name="go test github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="TestActivityRepository_in_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 21m 32s.xml">
<configuration name="TestActivityRepository in github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
<history-entry file="go_test_github_com_gisle_stufflog_database_drivers_bolt - 2019.09.28 at 17h 21m 40s.xml">
<configuration name="go test github.com/gisle/stufflog/database/drivers/bolt" configurationId="GoTestRunConfiguration" />
</history-entry>
</component>
<component name="ToolWindowManager">
<frame x="0" y="41" width="3840" height="2119" extended-state="0" />
<editor active="true" />
<layout>
<window_info active="true" content_ui="combo" id="Project" order="0" sideWeight="0.49605885" visible="true" weight="0.13777421" />
<window_info id="Structure" order="1" sideWeight="0.5039411" side_tool="true" visible="true" weight="0.13777421" />
<window_info id="Favorites" order="2" side_tool="true" />
<window_info anchor="bottom" id="Message" order="0" />
<window_info anchor="bottom" id="Find" order="1" weight="0.32947975" />
<window_info anchor="bottom" id="Run" order="2" weight="0.32947975" />
<window_info anchor="bottom" id="Debug" order="3" weight="0.3998949" />
<window_info anchor="bottom" id="Cvs" order="4" weight="0.25" />
<window_info anchor="bottom" id="Inspection" order="5" weight="0.4" />
<window_info anchor="bottom" id="TODO" order="6" />
<window_info anchor="bottom" id="Docker" order="7" show_stripe_button="false" />
<window_info anchor="bottom" id="Version Control" order="8" />
<window_info anchor="bottom" id="GraphQL" order="9" />
<window_info anchor="bottom" id="Database Changes" order="10" />
<window_info anchor="bottom" id="Terminal" order="11" />
<window_info anchor="bottom" id="Event Log" order="12" side_tool="true" />
<window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" />
<window_info anchor="right" id="Ant Build" order="1" weight="0.25" />
<window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" />
<window_info anchor="right" id="Cargo" order="3" />
<window_info anchor="right" id="Database" order="4" />
<window_info anchor="right" id="Coverage" order="5" side_tool="true" weight="0.32985553" />
</layout>
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="1" />
</component>
<component name="VgoProject">
<integration-enabled>true</integration-enabled>
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/stufflog$TestIndex_Insert_Initial_in_github_com_gisle_stufflog_database_drivers_bolt.out" NAME="TestIndex/Insert_Initial in github.com/gisle/stufflog/database/drivers/bolt Coverage Results" MODIFIED="1569258865601" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
<SUITE FILE_PATH="coverage/stufflog$TestIndex_in_github_com_gisle_stufflog_database_drivers_bolt.out" NAME="TestIndex in github.com/gisle/stufflog/database/drivers/bolt Coverage Results" MODIFIED="1569258979795" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
<SUITE FILE_PATH="coverage/stufflog$go_test_github_com_gisle_stufflog_database_drivers_bolt.out" NAME="go test github.com/gisle/stufflog/database/drivers/bolt Coverage Results" MODIFIED="1569684100617" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
</component>
<component name="editorHistoryManager">
<entry file="file:///usr/lib/go/src/strings/builder.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="648">
<caret line="22" column="37" lean-forward="true" selection-start-line="22" selection-start-column="37" selection-end-line="22" selection-end-column="37" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/go.sum">
<provider selected="true" editor-type-id="text-editor" />
</entry>
<entry file="file://$PROJECT_DIR$/go.mod">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="216">
<caret line="4" column="9" selection-start-line="4" selection-start-column="9" selection-end-line="4" selection-end-column="9" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/generate/logid.go" />
<entry file="file://$PROJECT_DIR$/slerrors.go" />
<entry file="file:///usr/lib/go/src/strconv/atoi.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="562">
<caret line="23" column="13" selection-start-line="23" selection-start-column="5" selection-end-line="23" selection-end-column="13" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/internal/generate/id.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="378">
<caret line="13" column="7" selection-start-line="13" selection-start-column="5" selection-end-line="13" selection-end-column="7" />
<folding>
<element signature="e#18#103#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/repositories/usersessions.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="594">
<caret line="14" column="7" selection-start-line="14" selection-start-column="7" selection-end-line="14" selection-end-column="7" />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/go/pkg/mod/go.etcd.io/bbolt@v1.3.3/errors.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3456">
<caret line="64" column="2" selection-start-line="64" selection-start-column="2" selection-end-line="64" selection-end-column="2" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/repositories/errors.go" />
<entry file="file://$USER_HOME$/go/pkg/mod/go.etcd.io/bbolt@v1.3.3/cursor.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="588">
<caret line="116" column="17" selection-start-line="116" selection-start-column="17" selection-end-line="116" selection-end-column="17" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/bytes/bytes.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="609">
<caret line="25" column="5" selection-start-line="25" selection-start-column="5" selection-end-line="25" selection-end-column="5" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/time/format.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="588">
<caret line="81" column="17" lean-forward="true" selection-start-line="81" selection-start-column="17" selection-end-line="81" selection-end-column="27" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/builtin/builtin.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1473">
<caret line="149" column="20" selection-start-line="149" selection-start-column="15" selection-end-line="149" selection-end-column="20" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/usersession.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="630">
<caret line="116" lean-forward="true" selection-start-line="116" selection-end-line="128" selection-end-column="4" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/db_test.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="702">
<caret line="13" column="29" selection-start-line="13" selection-start-column="29" selection-end-line="13" selection-end-column="29" />
<folding>
<element signature="e#14#50#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/user.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="480">
<caret line="20" column="20" selection-start-line="20" selection-start-column="20" selection-end-line="20" selection-end-column="20" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/idx_test.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="432">
<caret line="11" selection-start-line="11" selection-end-line="13" />
<folding>
<element signature="e#14#108#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/user_test.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="540">
<caret line="40" selection-start-line="40" selection-end-line="44" selection-end-column="4" />
<folding>
<element signature="e#14#46#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/activity_test.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="594">
<caret line="11" column="18" selection-start-line="11" selection-start-column="18" selection-end-line="11" selection-end-column="18" />
<folding>
<element signature="e#14#120#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/net/http/cookie.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="609">
<caret line="30" column="2" selection-start-line="30" selection-start-column="2" selection-end-line="30" selection-end-column="2" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/net/http/request.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="879">
<caret line="357" column="8" lean-forward="true" selection-start-line="357" selection-start-column="8" selection-end-line="357" selection-end-column="8" />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="588">
<caret line="120" column="18" selection-start-line="120" selection-start-column="18" selection-end-line="120" selection-end-column="18" />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/routergroup.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="609">
<caret line="57" column="26" selection-start-line="57" selection-start-column="26" selection-end-line="57" selection-end-column="26" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/config/server.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="324">
<caret line="6" lean-forward="true" selection-start-line="6" selection-end-line="6" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/config/database.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="216">
<caret line="4" column="27" selection-start-line="4" selection-start-column="27" selection-end-line="4" selection-end-column="27" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/config/config.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="576">
<caret line="12" column="33" selection-start-line="12" selection-start-column="33" selection-end-line="12" selection-end-column="33" />
<folding>
<element signature="e#16#52#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/database.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="486">
<caret line="14" column="31" selection-start-line="14" selection-start-column="31" selection-end-line="14" selection-end-column="31" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/config/users.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="108">
<caret line="2" column="5" selection-start-line="2" selection-start-column="5" selection-end-line="2" selection-end-column="5" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/strings/compare.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="588">
<caret line="12" column="5" selection-start-line="12" selection-start-column="5" selection-end-line="12" selection-end-column="5" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/sort/sort.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="987">
<caret line="99" column="22" lean-forward="true" selection-start-line="99" selection-start-column="22" selection-end-line="99" selection-end-column="22" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/repositories/user.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="540">
<caret line="10" selection-start-line="10" selection-end-line="10" />
<folding>
<element signature="e#22#79#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/time/time.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="717">
<caret line="1269" lean-forward="true" selection-start-line="1269" selection-end-line="1269" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/idx.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="8964">
<caret line="181" column="11" selection-start-line="181" selection-start-column="11" selection-end-line="181" selection-end-column="11" />
<folding>
<element signature="e#14#106#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/services/auth.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="306">
<caret line="169" column="48" selection-start-line="169" selection-start-column="48" selection-end-line="169" selection-end-column="48" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/db.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1836">
<caret line="50" column="3" selection-start-line="50" selection-start-column="3" selection-end-line="50" selection-end-column="3" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/activity.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="4050">
<caret line="82" column="7" selection-start-line="82" selection-start-column="7" selection-end-line="82" selection-end-column="7" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/api/activity.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2538">
<caret line="54" column="28" selection-start-line="54" selection-start-column="28" selection-end-line="54" selection-end-column="28" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/slerrors/slerrors.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3402">
<caret line="69" column="1" selection-start-line="69" selection-start-column="1" selection-end-line="69" selection-end-column="1" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/api/user.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="648">
<caret line="17" selection-start-line="17" selection-end-line="17" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/api/period.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="7074">
<caret line="138" column="36" selection-start-line="138" selection-start-column="36" selection-end-line="138" selection-end-column="36" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/drivers/bolt/period.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="8910">
<caret line="172" column="18" selection-start-line="172" selection-start-column="18" selection-end-line="172" selection-end-column="18" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/models/user.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="486">
<caret line="14" column="1" selection-start-line="14" selection-start-column="1" selection-end-line="14" selection-end-column="1" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/models/activity.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2700">
<caret line="53" column="32" selection-start-line="53" selection-start-column="32" selection-end-line="53" selection-end-column="32" />
<folding>
<element signature="e#16#111#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/repositories/period.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="540">
<caret line="13" column="2" selection-start-line="13" selection-start-column="2" selection-end-line="13" selection-end-column="2" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/database/repositories/activity.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="378">
<caret line="10" column="2" selection-start-line="10" selection-start-column="2" selection-end-line="10" selection-end-column="2" />
<folding>
<element signature="e#22#79#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/models/period.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="16308">
<caret line="306" column="41" selection-start-line="306" selection-start-column="41" selection-end-line="306" selection-end-column="41" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/services/scoring.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3456">
<caret line="70" column="2" selection-start-line="70" selection-start-column="2" selection-end-line="70" selection-end-column="2" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1134">
<caret line="21" column="76" selection-start-line="21" selection-start-column="76" selection-end-line="21" selection-end-column="76" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/go/src/net/http/fs.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="609">
<caret line="94" column="5" selection-start-line="94" selection-start-column="5" selection-end-line="94" selection-end-column="5" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/main.go">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1386">
<caret line="81" lean-forward="true" selection-start-line="81" selection-end-line="81" />
<folding>
<element signature="e#14#242#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</component>
</project>

22
Dockerfile

@ -0,0 +1,22 @@
# 1. Backend
FROM golang:1.13 AS build-backend
COPY . /project
WORKDIR /project
RUN go get
RUN CGO_ENABLED=0 go build -ldflags "-w -s" .
# 2. Frontend
FROM node:10-alpine AS build-frontend
COPY svelte-ui /project
WORKDIR project
RUN npm install
RUN npm run build
# 3. Release
FROM alpine:3.9.5 AS release
RUN apk add --no-cache ca-certificates
COPY --from=build-backend /project/stufflog /usr/local/bin/stufflog
COPY --from=build-frontend /project/public /usr/share/stufflog-ui
CMD ["/usr/local/bin/stufflog", "-conf", "/etc/stufflog/stufflog.yaml", "-ui", "/usr/share/stufflog-ui/"]

137
api/activity.go

@ -0,0 +1,137 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/gisle/stufflog/database"
"github.com/gisle/stufflog/models"
"github.com/gisle/stufflog/services"
"github.com/gisle/stufflog/slerrors"
"sort"
)
func Activity(g *gin.RouterGroup, db database.Database, auth *services.AuthService) {
type resObj struct {
Activity *models.Activity `json:"activity,omitempty"`
Activities []*models.Activity `json:"activities,omitempty"`
}
g.Use(auth.GinSessionMiddleware(true))
// GET / – List own activities.
g.GET("/", func(c *gin.Context) {
user := auth.UserFromContext(c)
activities, err := db.Activities().ListUser(c.Request.Context(), *user)
if err != nil {
slerrors.GinRespond(c, err)
return
}
sort.Sort(models.ActivitiesByName(activities))
c.JSON(200, resObj{Activities: activities})
})
// GET / — Find an activity
g.GET("/:id", func(c *gin.Context) {
user := auth.UserFromContext(c)
activity, err := db.Activities().FindID(c.Request.Context(), c.Param("id"))
if err != nil {
slerrors.GinRespond(c, err)
return
}
if activity.UserID != user.ID {
slerrors.GinRespond(c, slerrors.NotFound("Activity"))
return
}
c.JSON(200, resObj{Activity: activity})
})
// POST / – Create an activity
g.POST("/", func(c *gin.Context) {
user := auth.UserFromContext(c)
activity := models.Activity{}
err := c.BindJSON(&activity)
if err != nil {
slerrors.GinRespond(c, &slerrors.SLError{Code: 400, Text: err.Error()})
return
}
activity.UserID = user.ID
activity.GenerateIDs()
err = db.Activities().Insert(c.Request.Context(), activity)
if err != nil {
slerrors.GinRespond(c, err)
return
}
c.JSON(200, resObj{Activity: &activity})
})
// PATCH /:id – Update one activity
g.PATCH("/:id", func(c *gin.Context) {
user := auth.UserFromContext(c)
updates := make([]*models.ActivityUpdate, 0, 8)
err := c.BindJSON(&updates)
if err != nil {
slerrors.GinRespond(c, &slerrors.SLError{Code: 400, Text: err.Error()})
return
}
activity, err := db.Activities().FindID(c.Request.Context(), c.Param("id"))
if err != nil {
slerrors.GinRespond(c, err)
return
}
if activity.UserID != user.ID {
slerrors.GinRespond(c, slerrors.NotFound("Activity"))
return
}
activity, err = db.Activities().Update(c.Request.Context(), *activity, updates)
if err != nil {
slerrors.GinRespond(c, err)
return
}
c.JSON(200, resObj{Activity: activity})
})
// DELETE /:id – Delete an activity
g.DELETE("/:id", func(c *gin.Context) {
user := auth.UserFromContext(c)
activity, err := db.Activities().FindID(c.Request.Context(), c.Param("id"))
if err != nil {
slerrors.GinRespond(c, err)
return
}
if activity.UserID != user.ID {
slerrors.GinRespond(c, slerrors.NotFound("Activity"))
return
}
periods, err := db.Periods().ListActivity(c.Request.Context(), *activity)
if err != nil {
slerrors.GinRespond(c, slerrors.Wrap(err))
return
}
if len(periods) > 0 {
slerrors.GinRespond(c, slerrors.PreconditionFailed("You cannot delete an activity that's in use."))
return
}
err = db.Activities().Remove(c.Request.Context(), *activity)
if err != nil {
slerrors.GinRespond(c, err)
return
}
c.JSON(200, resObj{Activity: activity})
})
}

178
api/period.go

@ -0,0 +1,178 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/gisle/stufflog/database"
"github.com/gisle/stufflog/models"
"github.com/gisle/stufflog/services"
"github.com/gisle/stufflog/slerrors"
"sort"
)
func Period(g *gin.RouterGroup, db database.Database, scoring *services.ScoringService, auth *services.AuthService) {
type resObj struct {
Period *models.Period `json:"period,omitempty"`
Periods []*models.Period `json:"periods,omitempty"`
Activities []*models.Activity `json:"activities,omitempty"`
}
g.Use(auth.GinSessionMiddleware(true))
// GET / – List own periods.
g.GET("/", func(c *gin.Context) {
user := auth.UserFromContext(c)
periods, err := db.Periods().ListUser(c.Request.Context(), *user)
if err != nil {
slerrors.GinRespond(c, err)
return
}
sort.Sort(models.PeriodsByFrom(periods))
c.JSON(200, resObj{Periods: periods})
})
// GET / — Find a period
g.GET("/:id", func(c *gin.Context) {
user := auth.UserFromContext(c)
period, err := db.Periods().FindID(c.Request.Context(), c.Param("id"))
if err != nil {
slerrors.GinRespond(c, err)
return
}
if period.UserID != user.ID {
slerrors.GinRespond(c, slerrors.NotFound("Period"))
return
}
activities := make([]*models.Activity, 0, 8)
activityAdded := make(map[string]bool)
for _, goal := range period.Goals {
if !activityAdded[goal.ActivityID] {
activityAdded[goal.ActivityID] = true
activity, err := db.Activities().FindID(c.Request.Context(), goal.ActivityID)
if err != nil {
slerrors.GinRespond(c, err)
return
}
activities = append(activities, activity)
}
}
c.JSON(200, resObj{Period: period, Activities: activities})
})
// POST / – Create a period
g.POST("/", func(c *gin.Context) {
user := auth.UserFromContext(c)
period := models.Period{}
err := c.BindJSON(&period)
if err != nil {
slerrors.GinRespond(c, &slerrors.SLError{Code: 400, Text: err.Error()})
return
}
period.GenerateIDs()
period.RemoveDuplicateTags()
period.UserID = user.ID
period.Logs = make([]models.PeriodLog, 0)
period.ShouldReScore = false
if period.Tags == nil {
period.Tags = make([]string, 0)
}
if period.Goals == nil {
period.Goals = make([]models.PeriodGoal, 0)
}
for i := range period.Goals {
if period.Goals[i].SubGoals == nil {
period.Goals[i].SubGoals = make([]models.PeriodSubGoal, 0)
}
}
err = db.Periods().Insert(c.Request.Context(), period)
if err != nil {
slerrors.GinRespond(c, err)
return
}
c.JSON(200, resObj{Period: &period})
})
// PATCH /:id – Update one Period
g.PATCH("/:id", func(c *gin.Context) {
user := auth.UserFromContext(c)
updates := make([]*models.PeriodUpdate, 0, 8)
err := c.BindJSON(&updates)
if err != nil {
slerrors.GinRespond(c, &slerrors.SLError{Code: 400, Text: err.Error()})
return
}
period, err := db.Periods().FindID(c.Request.Context(), c.Param("id"))
if err != nil {
slerrors.GinRespond(c, err)
return
}
if period.UserID != user.ID {
slerrors.GinRespond(c, slerrors.NotFound("Period"))
return
}
for _, update := range updates {
if update.AddLog != nil {
score, err := scoring.ScoreOne(c.Request.Context(), *period, *update.AddLog)
if err != nil {
slerrors.GinRespond(c, err)
return
}
update.AddLog.Score = score
}
}
period, err = db.Periods().Update(c.Request.Context(), *period, updates)
if err != nil {
slerrors.GinRespond(c, err)
return
}
if period.ShouldReScore {
period, err = scoring.ScoreAll(c.Request.Context(), *period)
if err != nil {
slerrors.GinRespond(c, err)
return
}
}
c.JSON(200, resObj{Period: period})
})
// DELETE /:id – Delete a period
g.DELETE("/:id", func(c *gin.Context) {
user := auth.UserFromContext(c)
period, err := db.Periods().FindID(c.Request.Context(), c.Param("id"))
if err != nil {
slerrors.GinRespond(c, err)
return
}
if period.UserID != user.ID {
slerrors.GinRespond(c, slerrors.NotFound("Period"))
return
}
err = db.Periods().Remove(c.Request.Context(), *period)
if err != nil {
slerrors.GinRespond(c, err)
return
}
c.JSON(200, resObj{Period: period})
})
}

75
api/user.go

@ -0,0 +1,75 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/gisle/stufflog/models"
"github.com/gisle/stufflog/services"
"github.com/gisle/stufflog/slerrors"
)
func User(g *gin.RouterGroup, auth *services.AuthService) {
type loginRegisterRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type resObj struct {
User *models.User `json:"user,omitempty"`
}
g.Use(auth.GinSessionMiddleware(false))
// GET / – Check session
g.GET("/", func(c *gin.Context) {
c.JSON(200, resObj{
User: auth.UserFromContext(c.Request.Context()),
})
})
g.POST("/register", func(c *gin.Context) {
data := loginRegisterRequest{}
err := c.BindJSON(&data)
if err != nil {
slerrors.GinRespond(c, &slerrors.SLError{Code: 400, Text: err.Error()})
return
}
user, err := auth.Register(c.Request.Context(), data.Username, data.Password)
if err != nil {
slerrors.GinRespond(c, err)
return
}
c.JSON(200, resObj{User: user})
})
g.POST("/login", func(c *gin.Context) {
data := loginRegisterRequest{}
err := c.BindJSON(&data)
if err != nil {
slerrors.GinRespond(c, &slerrors.SLError{Code: 400, Text: err.Error()})
return
}
user, session, err := auth.Login(c.Request.Context(), data.Username, data.Password)
if err != nil {
slerrors.GinRespond(c, err)
return
}
auth.GinSaveSession(c, *session)
c.JSON(200, resObj{User: user})
})
g.POST("/logout", func(c *gin.Context) {
err := auth.Logout(c.Request.Context())
if err != nil {
slerrors.GinRespond(c, err)
return
}
auth.GinClearSession(c)
c.JSON(200, resObj{})
})
}

28
config/config.go

@ -0,0 +1,28 @@
package config
import (
"gopkg.in/yaml.v2"
"os"
)
var root Config
type Config struct {
Database Database `yaml:"database"`
Users Users `yaml:"users"`
Server Server `yaml:"server"`
}
func Get() Config {
return root
}
func Load(path string) (Config, error) {
f, err := os.Open(path)
if err != nil {
return Config{}, err
}
err = yaml.NewDecoder(f).Decode(&root)
return root, err
}

6
config/database.go

@ -0,0 +1,6 @@
package config
type Database struct {
Driver string `yaml:"driver"`
Path string `yaml:"path"`
}

6
config/server.go

@ -0,0 +1,6 @@
package config
type Server struct {
Listen string
Debug bool
}

5
config/users.go

@ -0,0 +1,5 @@
package config
type Users struct {
AllowRegister bool `yaml:"allow_register"`
}

26
database/database.go

@ -0,0 +1,26 @@
package database
import (
"github.com/gisle/stufflog/config"
"github.com/gisle/stufflog/database/drivers/bolt"
"github.com/gisle/stufflog/database/repositories"
"github.com/gisle/stufflog/slerrors"
)
// Database is a collections of repositories.
type Database interface {
Users() repositories.UserRepository
UserSessions() repositories.UserSessionRepository
Activities() repositories.ActivityRepository
Periods() repositories.PeriodRepository
}
// Init gets you database based on the configuration provided.
func Init(cfg config.Database) (Database, error) {
switch cfg.Driver {
case "bolt", "boltdb":
return bolt.Init(cfg)
default:
return nil, &slerrors.SLError{Code: 500, Text: "Database driver " + cfg.Driver + " not recognized."}
}
}

229
database/drivers/bolt/activity.go

@ -0,0 +1,229 @@
package bolt
import (
"context"
"github.com/gisle/stufflog/database/repositories"
"github.com/gisle/stufflog/models"
"github.com/gisle/stufflog/slerrors"
"github.com/vmihailenco/msgpack/v4"
"go.etcd.io/bbolt"
)
var bnActivities = []byte("Activity")
type activityRepository struct {
db *bbolt.DB
userIdIdx *index
}
func (r *activityRepository) FindID(ctx context.Context, id string) (*models.Activity, error) {
activity := new(models.Activity)
err := r.db.View(func(tx *bbolt.Tx) error {
value := tx.Bucket(bnActivities).Get(unsafeStringToBytes(id))
if value == nil {
return slerrors.NotFound("Activity")
}
err := msgpack.Unmarshal(value, activity)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return activity, nil
}
func (r *activityRepository) List(ctx context.Context) ([]*models.Activity, error) {
activities := make([]*models.Activity, 0, 16)
err := r.db.View(func(tx *bbolt.Tx) error {
cursor := tx.Bucket(bnActivities).Cursor()
for key, value := cursor.First(); key != nil; key, value = cursor.Next() {
activity := new(models.Activity)
err := msgpack.Unmarshal(value, activity)
if err != nil {
return err
}
activities = append(activities, activity)
}
return nil
})
if err != nil {
return nil, err
}
return activities, nil
}
func (r *activityRepository) ListUser(ctx context.Context, user models.User) ([]*models.Activity, error) {
activities := make([]*models.Activity, 0, 16)
err := r.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bnActivities)
ids, err := r.userIdIdx.WithTx(tx).Get(user.ID)
if err != nil {
return err
}
for _, id := range ids {
value := bucket.Get(id)
if value == nil {
continue
}
activity := new(models.Activity)
err := msgpack.Unmarshal(value, activity)
if err != nil {
return err
}
activities = append(activities, activity)
}
return nil
})
if err != nil {
return nil, err
}
return activities, nil
}
func (r *activityRepository) Insert(ctx context.Context, activity models.Activity) error {
value, err := msgpack.Marshal(&activity)
if err != nil {
return err
}
return r.db.Update(func(tx *bbolt.Tx) error {
err := tx.Bucket(bnActivities).Put(unsafeStringToBytes(activity.ID), value)
if err != nil {
return err
}
err = r.userIdIdx.WithTx(tx).Set(unsafeStringToBytes(activity.ID), activity.UserID)
if err != nil {
return err
}
return nil
})
}
func (r *activityRepository) Update(ctx context.Context, activity models.Activity, updates []*models.ActivityUpdate) (*models.Activity, error) {
err := r.db.Update(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bnActivities)
// Re-Get to guarantee consistency.
value := bucket.Get(unsafeStringToBytes(activity.ID))
if value == nil {
return slerrors.NotFound("Activity")
}
err := msgpack.Unmarshal(value, &activity)
if err != nil {
return err
}
// Perform updates
didChange := false
for _, update := range updates {
changed, err := activity.ApplyUpdate(*update)
if err != nil {
return err
}
if changed {
didChange = true
}
}
// Put back into bucket
if didChange {
value, err := msgpack.Marshal(&activity)
if err != nil {
return err
}
err = tx.Bucket(bnActivities).Put(unsafeStringToBytes(activity.ID), value)
if err != nil {
return err
}
} else {
return errUnchanged
}
return nil
})
if err != nil && err != errUnchanged {
return nil, err
}
return &activity, nil
}
func (r *activityRepository) Remove(ctx context.Context, activity models.Activity) error {
return r.db.Update(func(tx *bbolt.Tx) error {
err := tx.Bucket(bnActivities).Delete(unsafeStringToBytes(activity.ID))
if err != nil {
return err
}
err = r.userIdIdx.WithTx(tx).Set(unsafeStringToBytes(activity.ID))
if err != nil {
return err
}
return nil