Skip to content

Commit 7bde599

Browse files
committed
Add user creation and password update
1 parent 5f2489d commit 7bde599

20 files changed

+587
-413
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ENV GBM_CSS_COLUMNS=""
1111
ENV GBM_NAMESPACE=""
1212
ENV FAVICON_CACHE_DIR=/data/favicons
1313
ENV FAVICON_CACHE_SIZE=20971520
14-
ENV JSON_DB_PATH=/db/gobookmarks
14+
ENV LOCAL_GIT_PATH=/db/gobookmarks
1515
ENV GOBM_ENV_FILE=/etc/gobookmarks/gobookmarks.env
1616
ENV GOBM_CONFIG_FILE=/etc/gobookmarks/config.json
1717
EXPOSE 8080

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Configuration values can be supplied as environment variables, via a JSON config
8181
| `FAVICON_CACHE_SIZE` | Maximum size in bytes of the favicon cache before old icons are removed. Defaults to `20971520`. |
8282
| `GITHUB_SERVER` | Base URL for GitHub (set for GitHub Enterprise). |
8383
| `GITLAB_SERVER` | Base URL for GitLab (self-hosted). |
84-
| `JSON_DB_PATH` | Directory used for the local JSON provider. |
84+
| `LOCAL_GIT_PATH` | Directory used for the local git provider. |
8585
| `GOBM_ENV_FILE` | Path to a file of `KEY=VALUE` pairs loaded before the environment. Defaults to `/etc/gobookmarks/gobookmarks.env`. |
8686
| `GOBM_CONFIG_FILE` | Path to the JSON config file. If unset the program uses `$XDG_CONFIG_HOME/gobookmarks/config.json` or `$HOME/.config/gobookmarks/config.json` for normal users and `/etc/gobookmarks/config.json` when run as root. |
8787

@@ -126,10 +126,12 @@ and both service files run the daemon as `gobookmarks`.
126126
### Docker
127127

128128
The Docker image continues to work as before. Mount `/data` if you need
129-
persistent storage and mount `/db` for the JSON provider. Pass the same environment variables as listed above. The JSON provider stores data under
130-
`$JSON_DB_PATH/<sha256(username)>/` with a `.password` file containing the bcrypt hash,
131-
`bookmarks.txt`, `branches.txt`, `tags.txt` and a tar archive per branch.
132-
Login with this provider via `/login/json` using the username and password that match the stored bcrypt hash.
129+
persistent storage and mount `/db` for the git provider. Pass the same environment variables as listed above. The git provider stores data under
130+
`$LOCAL_GIT_PATH/<sha256(username)>/` as a git repository with a `.password` file containing the bcrypt hash.
131+
Create an account via `/signup/git`. This stores the password hash under
132+
`$LOCAL_GIT_PATH/<sha256(username)>/.password` and creates the repository. Log in
133+
later via `/login/git` using the same credentials. Passwords can be updated with
134+
the provider's SetPassword method.
133135
Favicons are cached on disk under `/data/favicons` by default. Set
134136
`FAVICON_CACHE_DIR` to an empty string to disable disk caching.
135137
You can also mount a config file and env file:

authHandlers.go

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,13 @@ package gobookmarks
22

33
import (
44
"context"
5-
"crypto/sha256"
6-
"encoding/hex"
75
"errors"
86
"fmt"
97
"github.com/gorilla/mux"
108
"github.com/gorilla/securecookie"
119
"github.com/gorilla/sessions"
12-
"golang.org/x/crypto/bcrypt"
1310
"log"
1411
"net/http"
15-
"os"
16-
"path/filepath"
1712
)
1813

1914
func UserLogoutAction(w http.ResponseWriter, r *http.Request) error {
@@ -125,19 +120,24 @@ func Oauth2CallbackPage(w http.ResponseWriter, r *http.Request) error {
125120
return nil
126121
}
127122

128-
func JSONLoginAction(w http.ResponseWriter, r *http.Request) error {
123+
func GitLoginAction(w http.ResponseWriter, r *http.Request) error {
129124
session, err := getSession(w, r)
130125
if err != nil {
131126
return fmt.Errorf("session error: %w", err)
132127
}
133128
user := r.FormValue("username")
134129
pass := r.FormValue("password")
135-
data, err := os.ReadFile(userPasswordPath(user))
136-
if err != nil || bcrypt.CompareHashAndPassword(data, []byte(pass)) != nil {
137-
http.Redirect(w, r, "/login/json?error=invalid", http.StatusSeeOther)
130+
p := GetProvider("git")
131+
ph, ok := p.(PasswordHandler)
132+
if !ok {
133+
return fmt.Errorf("password handler not available")
134+
}
135+
okPass, err := ph.CheckPassword(r.Context(), user, pass)
136+
if err != nil || !okPass {
137+
http.Redirect(w, r, "/login/git?error=invalid", http.StatusSeeOther)
138138
return nil
139139
}
140-
session.Values["Provider"] = "json"
140+
session.Values["Provider"] = "git"
141141
session.Values["GithubUser"] = &User{Login: user}
142142
session.Values["Token"] = nil
143143
session.Values["version"] = version
@@ -147,34 +147,27 @@ func JSONLoginAction(w http.ResponseWriter, r *http.Request) error {
147147
return nil
148148
}
149149

150-
func JSONSignupAction(w http.ResponseWriter, r *http.Request) error {
150+
func GitSignupAction(w http.ResponseWriter, r *http.Request) error {
151151
user := r.FormValue("username")
152152
pass := r.FormValue("password")
153-
p := userPasswordPath(user)
154-
if _, err := os.Stat(p); err == nil {
155-
http.Redirect(w, r, "/login/json?error=exists", http.StatusSeeOther)
156-
return nil
157-
} else if !os.IsNotExist(err) {
158-
return fmt.Errorf("stat: %w", err)
159-
}
160-
if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
161-
return fmt.Errorf("mkdir: %w", err)
162-
}
163-
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
164-
if err != nil {
165-
return fmt.Errorf("hash: %w", err)
153+
prov := GetProvider("git")
154+
ph, ok := prov.(PasswordHandler)
155+
if !ok {
156+
return fmt.Errorf("password handler not available")
157+
}
158+
if err := ph.CreateUser(r.Context(), user, pass); err != nil {
159+
if errors.Is(err, ErrUserExists) {
160+
http.Redirect(w, r, "/login/git?error=exists", http.StatusSeeOther)
161+
return nil
162+
}
163+
return err
166164
}
167-
if err := os.WriteFile(p, hash, 0600); err != nil {
168-
return fmt.Errorf("write: %w", err)
165+
if err := prov.CreateRepo(r.Context(), user, nil, RepoName); err != nil {
166+
return err
169167
}
170168
return nil
171169
}
172170

173-
func userPasswordPath(user string) string {
174-
uh := sha256.Sum256([]byte(user))
175-
return filepath.Join(JSONDBPath, hex.EncodeToString(uh[:]), ".password")
176-
}
177-
178171
func UserAdderMiddleware(next http.Handler) http.Handler {
179172
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
180173
// Get the session.

bookmarkActionHandlers.go

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ func BookmarksEditSaveAction(w http.ResponseWriter, r *http.Request) error {
1818
ref := r.PostFormValue("ref")
1919
sha := r.PostFormValue("sha")
2020
repoName := RepoName
21-
if r.PostFormValue("repoName") != "" {
22-
repoName = r.PostFormValue("repoName")
23-
}
2421

2522
login := ""
2623
if githubUser != nil {
@@ -30,7 +27,7 @@ func BookmarksEditSaveAction(w http.ResponseWriter, r *http.Request) error {
3027
_, curSha, err := GetBookmarks(r.Context(), login, ref, token)
3128
if err != nil {
3229
if errors.Is(err, ErrRepoNotFound) {
33-
return renderCreateRepoPrompt(w, r, repoName, text, branch, ref, sha, nil)
30+
return renderCreateRepoPrompt(w, r, text, branch, ref, sha, nil)
3431
}
3532
return fmt.Errorf("GetBookmarks: %w", err)
3633
}
@@ -44,9 +41,8 @@ func BookmarksEditSaveAction(w http.ResponseWriter, r *http.Request) error {
4441
return fmt.Errorf("create repo: %w", ErrNoProvider)
4542
}
4643
if err := p.CreateRepo(r.Context(), login, token, repoName); err != nil {
47-
return renderCreateRepoPrompt(w, r, repoName, text, branch, ref, sha, err)
44+
return renderCreateRepoPrompt(w, r, text, branch, ref, sha, err)
4845
}
49-
RepoName = repoName
5046
if err := CreateBookmarks(r.Context(), login, token, branch, text); err != nil {
5147
return fmt.Errorf("createBookmark error: %w", err)
5248
}
@@ -55,7 +51,7 @@ func BookmarksEditSaveAction(w http.ResponseWriter, r *http.Request) error {
5551

5652
if err := UpdateBookmarks(r.Context(), login, token, ref, branch, text, curSha); err != nil {
5753
if errors.Is(err, ErrRepoNotFound) {
58-
return renderCreateRepoPrompt(w, r, repoName, text, branch, ref, sha, nil)
54+
return renderCreateRepoPrompt(w, r, text, branch, ref, sha, nil)
5955
}
6056
return fmt.Errorf("updateBookmark error: %w", err)
6157
}
@@ -117,18 +113,16 @@ func CategoryEditSaveAction(w http.ResponseWriter, r *http.Request) error {
117113
return nil
118114
}
119115

120-
func renderCreateRepoPrompt(w http.ResponseWriter, r *http.Request, repoName, text, branch, ref, sha string, err error) error {
116+
func renderCreateRepoPrompt(w http.ResponseWriter, r *http.Request, text, branch, ref, sha string, err error) error {
121117
data := struct {
122118
*CoreData
123-
RepoName string
124-
Text string
125-
Branch string
126-
Ref string
127-
Sha string
128-
Error string
119+
Text string
120+
Branch string
121+
Ref string
122+
Sha string
123+
Error string
129124
}{
130125
CoreData: r.Context().Value(ContextValues("coreData")).(*CoreData),
131-
RepoName: repoName,
132126
Text: text,
133127
Branch: branch,
134128
Ref: ref,

cmd/gobookmarks/main.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func main() {
7171
}
7272
return 0
7373
}(),
74-
JSONDBPath: os.Getenv("JSON_DB_PATH"),
74+
LocalGitPath: os.Getenv("LOCAL_GIT_PATH"),
7575
}
7676

7777
configPath := DefaultConfigPath()
@@ -88,7 +88,7 @@ func main() {
8888
var glServerFlag stringFlag
8989
var faviconDirFlag stringFlag
9090
var faviconSizeFlag stringFlag
91-
var jsonPathFlag stringFlag
91+
var localGitPathFlag stringFlag
9292
var columnFlag boolFlag
9393
var versionFlag bool
9494
var dumpConfig bool
@@ -104,7 +104,7 @@ func main() {
104104
flag.Var(&faviconSizeFlag, "favicon-cache-size", "max size of favicon cache in bytes")
105105
flag.Var(&ghServerFlag, "github-server", "GitHub base URL")
106106
flag.Var(&glServerFlag, "gitlab-server", "GitLab base URL")
107-
flag.Var(&jsonPathFlag, "json-db-path", "JSON database path")
107+
flag.Var(&localGitPathFlag, "local-git-path", "directory for local git provider")
108108
flag.Var(&columnFlag, "css-columns", "use CSS columns")
109109
flag.BoolVar(&versionFlag, "version", false, "show version")
110110
flag.BoolVar(&dumpConfig, "dump-config", false, "print merged config and exit")
@@ -163,8 +163,8 @@ func main() {
163163
if glServerFlag.set {
164164
cfg.GitlabServer = glServerFlag.value
165165
}
166-
if jsonPathFlag.set {
167-
cfg.JSONDBPath = jsonPathFlag.value
166+
if localGitPathFlag.set {
167+
cfg.LocalGitPath = localGitPathFlag.value
168168
}
169169

170170
if dumpConfig {
@@ -191,8 +191,8 @@ func main() {
191191
} else {
192192
FaviconCacheSize = DefaultFaviconCacheSize
193193
}
194-
if cfg.JSONDBPath != "" {
195-
JSONDBPath = cfg.JSONDBPath
194+
if cfg.LocalGitPath != "" {
195+
LocalGitPath = cfg.LocalGitPath
196196
}
197197
githubID := cfg.GithubClientID
198198
githubSecret := cfg.GithubSecret
@@ -254,9 +254,9 @@ func main() {
254254
r.HandleFunc("/history/commits", runTemplate("historyCommits.gohtml")).Methods("GET").MatcherFunc(RequiresAnAccount())
255255

256256
r.HandleFunc("/login", runTemplate("loginPage.gohtml")).Methods("GET")
257-
r.HandleFunc("/login/json", runTemplate("jsonLoginPage.gohtml")).Methods("GET")
258-
r.HandleFunc("/login/json", runHandlerChain(JSONLoginAction, redirectToHandler("/"))).Methods("POST")
259-
r.HandleFunc("/signup/json", runHandlerChain(JSONSignupAction, redirectToHandler("/login/json"))).Methods("POST")
257+
r.HandleFunc("/login/git", runTemplate("gitLoginPage.gohtml")).Methods("GET")
258+
r.HandleFunc("/login/git", runHandlerChain(GitLoginAction, redirectToHandler("/"))).Methods("POST")
259+
r.HandleFunc("/signup/git", runHandlerChain(GitSignupAction, redirectToHandler("/login/git"))).Methods("POST")
260260
r.HandleFunc("/login/{provider}", runHandlerChain(LoginWithProvider)).Methods("GET")
261261
r.HandleFunc("/logout", runHandlerChain(UserLogoutAction, runTemplate("logoutPage.gohtml"))).Methods("GET")
262262
r.HandleFunc("/oauth2Callback", runHandlerChain(Oauth2CallbackPage, redirectToHandler("/"))).Methods("GET")

config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type Config struct {
2323
GitlabServer string `json:"gitlab_server"`
2424
FaviconCacheDir string `json:"favicon_cache_dir"`
2525
FaviconCacheSize int64 `json:"favicon_cache_size"`
26-
JSONDBPath string `json:"json_db_path"`
26+
LocalGitPath string `json:"local_git_path"`
2727
}
2828

2929
// LoadConfigFile loads configuration from the given path if it exists.
@@ -78,8 +78,8 @@ func MergeConfig(dst *Config, src Config) {
7878
if src.FaviconCacheSize != 0 {
7979
dst.FaviconCacheSize = src.FaviconCacheSize
8080
}
81-
if src.JSONDBPath != "" {
82-
dst.JSONDBPath = src.JSONDBPath
81+
if src.LocalGitPath != "" {
82+
dst.LocalGitPath = src.LocalGitPath
8383
}
8484
}
8585

data_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,12 @@ func TestExecuteTemplates(t *testing.T) {
151151
}{baseData.CoreData, "boom"}},
152152
{"createRepo", "createRepo.gohtml", struct {
153153
*CoreData
154-
RepoName string
155-
Text string
156-
Branch string
157-
Ref string
158-
Sha string
159-
Error string
160-
}{baseData.CoreData, "MyBookmarks", "text", "main", "ref", "sha", ""}},
154+
Text string
155+
Branch string
156+
Ref string
157+
Sha string
158+
Error string
159+
}{baseData.CoreData, "text", "main", "ref", "sha", ""}},
161160
}
162161

163162
for _, tt := range pages {

errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,9 @@ var ErrSignedOut = errors.New("signed out")
1515

1616
// ErrNoProvider indicates that no provider was selected for the request.
1717
var ErrNoProvider = errors.New("no provider selected")
18+
19+
// ErrUserExists indicates that a user already exists when attempting signup.
20+
var ErrUserExists = errors.New("user already exists")
21+
22+
// ErrUserNotFound indicates that a user does not exist when attempting to set a password.
23+
var ErrUserNotFound = errors.New("user not found")

go.mod

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,43 @@ toolchain go1.23.8
77
require (
88
github.com/PuerkitoBio/goquery v1.8.1
99
github.com/arran4/gorillamuxlogic v1.0.1
10-
github.com/google/go-cmp v0.5.9
10+
github.com/go-git/go-git/v5 v5.16.2
11+
github.com/google/go-cmp v0.7.0
1112
github.com/google/go-github/v55 v55.0.0
1213
github.com/gorilla/mux v1.8.0
1314
github.com/gorilla/securecookie v1.1.1
1415
github.com/gorilla/sessions v1.2.1
1516
github.com/xanzy/go-gitlab v0.115.0
17+
golang.org/x/crypto v0.37.0
1618
golang.org/x/image v0.28.0
1719
golang.org/x/oauth2 v0.12.0
1820
)
1921

2022
require (
21-
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
23+
dario.cat/mergo v1.0.0 // indirect
24+
github.com/Microsoft/go-winio v0.6.2 // indirect
25+
github.com/ProtonMail/go-crypto v1.1.6 // indirect
2226
github.com/andybalholm/cascadia v1.3.1 // indirect
23-
github.com/cloudflare/circl v1.3.7 // indirect
24-
github.com/golang/protobuf v1.5.3 // indirect
27+
github.com/cloudflare/circl v1.6.1 // indirect
28+
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
29+
github.com/emirpasic/gods v1.18.1 // indirect
30+
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
31+
github.com/go-git/go-billy/v5 v5.6.2 // indirect
32+
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
33+
github.com/golang/protobuf v1.5.4 // indirect
2534
github.com/google/go-querystring v1.1.0 // indirect
2635
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
2736
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
28-
golang.org/x/crypto v0.36.0 // indirect
29-
golang.org/x/net v0.38.0 // indirect
30-
golang.org/x/sys v0.31.0 // indirect
37+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
38+
github.com/kevinburke/ssh_config v1.2.0 // indirect
39+
github.com/pjbgf/sha1cd v0.3.2 // indirect
40+
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
41+
github.com/skeema/knownhosts v1.3.1 // indirect
42+
github.com/xanzy/ssh-agent v0.3.3 // indirect
43+
golang.org/x/net v0.39.0 // indirect
44+
golang.org/x/sys v0.32.0 // indirect
3145
golang.org/x/time v0.3.0 // indirect
3246
google.golang.org/appengine v1.6.7 // indirect
3347
google.golang.org/protobuf v1.33.0 // indirect
48+
gopkg.in/warnings.v0 v0.1.2 // indirect
3449
)

0 commit comments

Comments
 (0)