+8
appview/db/db.go
+8
appview/db/db.go
+23
-7
appview/db/repos.go
+23
-7
appview/db/repos.go
···
18
Created time.Time
19
AtUri string
20
Description string
21
22
// optionally, populate this when querying for reverse mappings
23
RepoStats *RepoStats
···
138
139
func GetRepo(e Execer, did, name string) (*Repo, error) {
140
var repo Repo
141
-
var nullableDescription sql.NullString
142
143
-
row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name)
144
145
var createdAt string
146
-
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
147
return nil, err
148
}
149
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
150
repo.Created = createdAtTime
151
152
-
if nullableDescription.Valid {
153
-
repo.Description = nullableDescription.String
154
-
} else {
155
-
repo.Description = ""
156
}
157
158
return &repo, nil
···
302
func UpdateDescription(e Execer, repoAt, newDescription string) error {
303
_, err := e.Exec(
304
`update repos set description = ? where at_uri = ?`, newDescription, repoAt)
305
return err
306
}
307
···
18
Created time.Time
19
AtUri string
20
Description string
21
+
Spindle string
22
23
// optionally, populate this when querying for reverse mappings
24
RepoStats *RepoStats
···
139
140
func GetRepo(e Execer, did, name string) (*Repo, error) {
141
var repo Repo
142
+
var description, spindle sql.NullString
143
144
+
row := e.QueryRow(`
145
+
select did, name, knot, created, at_uri, description, spindle
146
+
from repos
147
+
where did = ? and name = ?
148
+
`,
149
+
did,
150
+
name,
151
+
)
152
153
var createdAt string
154
+
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil {
155
return nil, err
156
}
157
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
158
repo.Created = createdAtTime
159
160
+
if description.Valid {
161
+
repo.Description = description.String
162
+
}
163
+
164
+
if spindle.Valid {
165
+
repo.Spindle = spindle.String
166
}
167
168
return &repo, nil
···
312
func UpdateDescription(e Execer, repoAt, newDescription string) error {
313
_, err := e.Exec(
314
`update repos set description = ? where at_uri = ?`, newDescription, repoAt)
315
+
return err
316
+
}
317
+
318
+
func UpdateSpindle(e Execer, repoAt, spindle string) error {
319
+
_, err := e.Exec(
320
+
`update repos set spindle = ? where at_uri = ?`, spindle, repoAt)
321
return err
322
}
323
+1
appview/middleware/middleware.go
+1
appview/middleware/middleware.go
···
225
ctx := context.WithValue(req.Context(), "knot", repo.Knot)
226
ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
227
ctx = context.WithValue(ctx, "repoDescription", repo.Description)
228
ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
229
next.ServeHTTP(w, req.WithContext(ctx))
230
})
···
225
ctx := context.WithValue(req.Context(), "knot", repo.Knot)
226
ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
227
ctx = context.WithValue(ctx, "repoDescription", repo.Description)
228
+
ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle)
229
ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
230
next.ServeHTTP(w, req.WithContext(ctx))
231
})
+7
-5
appview/pages/pages.go
+7
-5
appview/pages/pages.go
···
616
}
617
618
type RepoSettingsParams struct {
619
+
LoggedInUser *oauth.User
620
+
RepoInfo repoinfo.RepoInfo
621
+
Collaborators []Collaborator
622
+
Active string
623
+
Branches []types.Branch
624
+
Spindles []string
625
+
CurrentSpindle string
626
// TODO: use repoinfo.roles
627
IsCollaboratorInviteAllowed bool
628
}
+37
-2
appview/pages/templates/repo/settings.html
+37
-2
appview/pages/templates/repo/settings.html
···
81
</div>
82
</form>
83
84
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
85
<form
86
hx-confirm="Are you sure you want to delete this repository?"
···
89
hx-indicator="#delete-repo-spinner"
90
>
91
<label for="branch">delete repository</label>
92
-
<button class="btn my-2 flex gap-2 items-center" type="text">
93
<span>delete</span>
94
<span id="delete-repo-spinner" class="group">
95
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
96
</span>
97
</button>
98
<span>
···
81
</div>
82
</form>
83
84
+
{{ if .RepoInfo.Roles.IsOwner }}
85
+
<form
86
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/spindle"
87
+
class="mt-6 group"
88
+
>
89
+
<label for="spindle">spindle</label>
90
+
<div class="flex gap-2 items-center">
91
+
<select id="spindle" name="spindle" required class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
92
+
<option
93
+
value=""
94
+
disabled
95
+
selected
96
+
>
97
+
Choose a spindle
98
+
</option>
99
+
{{ range .Spindles }}
100
+
<option
101
+
value="{{ . }}"
102
+
class="py-1"
103
+
{{ if .eq . $.Repo.Spindle }}
104
+
selected
105
+
{{ end }}
106
+
>
107
+
{{ . }}
108
+
</option>
109
+
{{ end }}
110
+
</select>
111
+
<button class="btn my-2 flex gap-2 items-center" type="submit">
112
+
<span>save</span>
113
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
114
+
</button>
115
+
</div>
116
+
</form>
117
+
{{ end }}
118
+
119
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
120
<form
121
hx-confirm="Are you sure you want to delete this repository?"
···
124
hx-indicator="#delete-repo-spinner"
125
>
126
<label for="branch">delete repository</label>
127
+
<button class="btn my-2 flex items-center" type="text">
128
<span>delete</span>
129
<span id="delete-repo-spinner" class="group">
130
+
{{ i "loader-circle" "pl-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
131
</span>
132
</button>
133
<span>
+99
-10
appview/repo/repo.go
+99
-10
appview/repo/repo.go
···
367
})
368
return
369
case http.MethodPut:
370
-
user := rp.oauth.GetUser(r)
371
newDescription := r.FormValue("description")
372
client, err := rp.oauth.AuthorizedClient(r)
373
if err != nil {
···
753
return
754
}
755
756
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
757
f, err := rp.repoResolver.Resolve(r)
758
if err != nil {
···
794
}
795
796
if ksResp.StatusCode != http.StatusNoContent {
797
-
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
798
return
799
}
800
801
tx, err := rp.db.BeginTx(r.Context(), nil)
802
if err != nil {
803
log.Println("failed to start tx")
804
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
805
return
806
}
807
defer func() {
···
814
815
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
816
if err != nil {
817
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
818
return
819
}
820
821
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
822
if err != nil {
823
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
824
return
825
}
826
···
838
return
839
}
840
841
-
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
842
843
}
844
···
897
tx, err := rp.db.BeginTx(r.Context(), nil)
898
if err != nil {
899
log.Println("failed to start tx")
900
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
901
return
902
}
903
defer func() {
···
988
return
989
}
990
991
-
w.Write([]byte(fmt.Sprint("default branch set to: ", branch)))
992
}
993
994
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
···
1027
return
1028
}
1029
1030
rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1031
LoggedInUser: user,
1032
RepoInfo: f.RepoInfo(user),
1033
Collaborators: repoCollaborators,
1034
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1035
Branches: result.Branches,
1036
})
1037
}
1038
}
···
1049
case http.MethodPost:
1050
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
1051
if err != nil {
1052
-
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", f.Knot))
1053
return
1054
}
1055
···
1135
}
1136
secret, err := db.GetRegistrationKey(rp.db, knot)
1137
if err != nil {
1138
-
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", knot))
1139
return
1140
}
1141
···
367
})
368
return
369
case http.MethodPut:
370
newDescription := r.FormValue("description")
371
client, err := rp.oauth.AuthorizedClient(r)
372
if err != nil {
···
752
return
753
}
754
755
+
// modify the spindle configured for this repo
756
+
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
757
+
f, err := rp.repoResolver.Resolve(r)
758
+
if err != nil {
759
+
log.Println("failed to get repo and knot", err)
760
+
w.WriteHeader(http.StatusBadRequest)
761
+
return
762
+
}
763
+
764
+
repoAt := f.RepoAt
765
+
rkey := repoAt.RecordKey().String()
766
+
if rkey == "" {
767
+
log.Println("invalid aturi for repo", err)
768
+
w.WriteHeader(http.StatusInternalServerError)
769
+
return
770
+
}
771
+
772
+
user := rp.oauth.GetUser(r)
773
+
774
+
newSpindle := r.FormValue("spindle")
775
+
client, err := rp.oauth.AuthorizedClient(r)
776
+
if err != nil {
777
+
log.Println("failed to get client")
778
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
779
+
return
780
+
}
781
+
782
+
// ensure that this is a valid spindle for this user
783
+
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
784
+
if err != nil {
785
+
log.Println("failed to get valid spindles")
786
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
787
+
return
788
+
}
789
+
790
+
if !slices.Contains(validSpindles, newSpindle) {
791
+
log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles)
792
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
793
+
return
794
+
}
795
+
796
+
// optimistic update
797
+
err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
798
+
if err != nil {
799
+
log.Println("failed to perform update-spindle query", err)
800
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
801
+
return
802
+
}
803
+
804
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
805
+
if err != nil {
806
+
// failed to get record
807
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.")
808
+
return
809
+
}
810
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
811
+
Collection: tangled.RepoNSID,
812
+
Repo: user.Did,
813
+
Rkey: rkey,
814
+
SwapRecord: ex.Cid,
815
+
Record: &lexutil.LexiconTypeDecoder{
816
+
Val: &tangled.Repo{
817
+
Knot: f.Knot,
818
+
Name: f.RepoName,
819
+
Owner: user.Did,
820
+
CreatedAt: f.CreatedAt,
821
+
Description: &f.Description,
822
+
Spindle: &newSpindle,
823
+
},
824
+
},
825
+
})
826
+
827
+
if err != nil {
828
+
log.Println("failed to perform update-spindle query", err)
829
+
// failed to get record
830
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.")
831
+
return
832
+
}
833
+
834
+
w.Write(fmt.Append(nil, "spindle set to: ", newSpindle))
835
+
}
836
+
837
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
838
f, err := rp.repoResolver.Resolve(r)
839
if err != nil {
···
875
}
876
877
if ksResp.StatusCode != http.StatusNoContent {
878
+
w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))
879
return
880
}
881
882
tx, err := rp.db.BeginTx(r.Context(), nil)
883
if err != nil {
884
log.Println("failed to start tx")
885
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
886
return
887
}
888
defer func() {
···
895
896
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
897
if err != nil {
898
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
899
return
900
}
901
902
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
903
if err != nil {
904
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
905
return
906
}
907
···
919
return
920
}
921
922
+
w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))
923
924
}
925
···
978
tx, err := rp.db.BeginTx(r.Context(), nil)
979
if err != nil {
980
log.Println("failed to start tx")
981
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
982
return
983
}
984
defer func() {
···
1069
return
1070
}
1071
1072
+
w.Write(fmt.Append(nil, "default branch set to: ", branch))
1073
}
1074
1075
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
···
1108
return
1109
}
1110
1111
+
spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1112
+
if err != nil {
1113
+
log.Println("failed to fetch spindles", err)
1114
+
return
1115
+
}
1116
+
1117
rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1118
LoggedInUser: user,
1119
RepoInfo: f.RepoInfo(user),
1120
Collaborators: repoCollaborators,
1121
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1122
Branches: result.Branches,
1123
+
Spindles: spindles,
1124
+
CurrentSpindle: f.Spindle,
1125
})
1126
}
1127
}
···
1138
case http.MethodPost:
1139
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
1140
if err != nil {
1141
+
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot))
1142
return
1143
}
1144
···
1224
}
1225
secret, err := db.GetRegistrationKey(rp.db, knot)
1226
if err != nil {
1227
+
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
1228
return
1229
}
1230
+1
appview/repo/router.go
+1
appview/repo/router.go
···
70
})
71
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
72
r.Get("/", rp.RepoSettings)
73
r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator)
74
r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo)
75
r.Put("/branches/default", rp.SetDefaultBranch)
···
70
})
71
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
72
r.Get("/", rp.RepoSettings)
73
+
r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle)
74
r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator)
75
r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo)
76
r.Put("/branches/default", rp.SetDefaultBranch)
+3
appview/reporesolver/resolver.go
+3
appview/reporesolver/resolver.go
···
31
RepoName string
32
RepoAt syntax.ATURI
33
Description string
34
CreatedAt string
35
Ref string
36
CurrentDir string
···
95
// pass through values from the middleware
96
description, ok := r.Context().Value("repoDescription").(string)
97
addedAt, ok := r.Context().Value("repoAddedAt").(string)
98
99
return &ResolvedRepo{
100
Knot: knot,
···
105
CreatedAt: addedAt,
106
Ref: ref,
107
CurrentDir: currentDir,
108
109
rr: rr,
110
}, nil
···
31
RepoName string
32
RepoAt syntax.ATURI
33
Description string
34
+
Spindle string
35
CreatedAt string
36
Ref string
37
CurrentDir string
···
96
// pass through values from the middleware
97
description, ok := r.Context().Value("repoDescription").(string)
98
addedAt, ok := r.Context().Value("repoAddedAt").(string)
99
+
spindle, ok := r.Context().Value("repoSpindle").(string)
100
101
return &ResolvedRepo{
102
Knot: knot,
···
107
CreatedAt: addedAt,
108
Ref: ref,
109
CurrentDir: currentDir,
110
+
Spindle: spindle,
111
112
rr: rr,
113
}, nil