+8
appview/db/db.go
+8
appview/db/db.go
···
455
455
})
456
456
db.Exec("pragma foreign_keys = on;")
457
457
458
+
// run migrations
459
+
runMigration(db, "add-spindle-to-repos", func(tx *sql.Tx) error {
460
+
tx.Exec(`
461
+
alter table repos add column spindle text;
462
+
`)
463
+
return nil
464
+
})
465
+
458
466
return &DB{db}, nil
459
467
}
460
468
+23
-7
appview/db/repos.go
+23
-7
appview/db/repos.go
···
18
18
Created time.Time
19
19
AtUri string
20
20
Description string
21
+
Spindle string
21
22
22
23
// optionally, populate this when querying for reverse mappings
23
24
RepoStats *RepoStats
···
138
139
139
140
func GetRepo(e Execer, did, name string) (*Repo, error) {
140
141
var repo Repo
141
-
var nullableDescription sql.NullString
142
+
var description, spindle sql.NullString
142
143
143
-
row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name)
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
+
)
144
152
145
153
var createdAt string
146
-
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
154
+
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil {
147
155
return nil, err
148
156
}
149
157
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
150
158
repo.Created = createdAtTime
151
159
152
-
if nullableDescription.Valid {
153
-
repo.Description = nullableDescription.String
154
-
} else {
155
-
repo.Description = ""
160
+
if description.Valid {
161
+
repo.Description = description.String
162
+
}
163
+
164
+
if spindle.Valid {
165
+
repo.Spindle = spindle.String
156
166
}
157
167
158
168
return &repo, nil
···
302
312
func UpdateDescription(e Execer, repoAt, newDescription string) error {
303
313
_, err := e.Exec(
304
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)
305
321
return err
306
322
}
307
323
+1
appview/middleware/middleware.go
+1
appview/middleware/middleware.go
···
225
225
ctx := context.WithValue(req.Context(), "knot", repo.Knot)
226
226
ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
227
227
ctx = context.WithValue(ctx, "repoDescription", repo.Description)
228
+
ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle)
228
229
ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
229
230
next.ServeHTTP(w, req.WithContext(ctx))
230
231
})
+7
-5
appview/pages/pages.go
+7
-5
appview/pages/pages.go
···
616
616
}
617
617
618
618
type RepoSettingsParams struct {
619
-
LoggedInUser *oauth.User
620
-
RepoInfo repoinfo.RepoInfo
621
-
Collaborators []Collaborator
622
-
Active string
623
-
Branches []types.Branch
619
+
LoggedInUser *oauth.User
620
+
RepoInfo repoinfo.RepoInfo
621
+
Collaborators []Collaborator
622
+
Active string
623
+
Branches []types.Branch
624
+
Spindles []string
625
+
CurrentSpindle string
624
626
// TODO: use repoinfo.roles
625
627
IsCollaboratorInviteAllowed bool
626
628
}
+37
-2
appview/pages/templates/repo/settings.html
+37
-2
appview/pages/templates/repo/settings.html
···
81
81
</div>
82
82
</form>
83
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
+
84
119
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
85
120
<form
86
121
hx-confirm="Are you sure you want to delete this repository?"
···
89
124
hx-indicator="#delete-repo-spinner"
90
125
>
91
126
<label for="branch">delete repository</label>
92
-
<button class="btn my-2 flex gap-2 items-center" type="text">
127
+
<button class="btn my-2 flex items-center" type="text">
93
128
<span>delete</span>
94
129
<span id="delete-repo-spinner" class="group">
95
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
130
+
{{ i "loader-circle" "pl-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
96
131
</span>
97
132
</button>
98
133
<span>
+99
-10
appview/repo/repo.go
+99
-10
appview/repo/repo.go
···
367
367
})
368
368
return
369
369
case http.MethodPut:
370
-
user := rp.oauth.GetUser(r)
371
370
newDescription := r.FormValue("description")
372
371
client, err := rp.oauth.AuthorizedClient(r)
373
372
if err != nil {
···
753
752
return
754
753
}
755
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
+
756
837
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
757
838
f, err := rp.repoResolver.Resolve(r)
758
839
if err != nil {
···
794
875
}
795
876
796
877
if ksResp.StatusCode != http.StatusNoContent {
797
-
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
878
+
w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))
798
879
return
799
880
}
800
881
801
882
tx, err := rp.db.BeginTx(r.Context(), nil)
802
883
if err != nil {
803
884
log.Println("failed to start tx")
804
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
885
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
805
886
return
806
887
}
807
888
defer func() {
···
814
895
815
896
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
816
897
if err != nil {
817
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
898
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
818
899
return
819
900
}
820
901
821
902
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
822
903
if err != nil {
823
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
904
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
824
905
return
825
906
}
826
907
···
838
919
return
839
920
}
840
921
841
-
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
922
+
w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))
842
923
843
924
}
844
925
···
897
978
tx, err := rp.db.BeginTx(r.Context(), nil)
898
979
if err != nil {
899
980
log.Println("failed to start tx")
900
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
981
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
901
982
return
902
983
}
903
984
defer func() {
···
988
1069
return
989
1070
}
990
1071
991
-
w.Write([]byte(fmt.Sprint("default branch set to: ", branch)))
1072
+
w.Write(fmt.Append(nil, "default branch set to: ", branch))
992
1073
}
993
1074
994
1075
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
···
1027
1108
return
1028
1109
}
1029
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
+
1030
1117
rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1031
1118
LoggedInUser: user,
1032
1119
RepoInfo: f.RepoInfo(user),
1033
1120
Collaborators: repoCollaborators,
1034
1121
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1035
1122
Branches: result.Branches,
1123
+
Spindles: spindles,
1124
+
CurrentSpindle: f.Spindle,
1036
1125
})
1037
1126
}
1038
1127
}
···
1049
1138
case http.MethodPost:
1050
1139
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
1051
1140
if err != nil {
1052
-
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", f.Knot))
1141
+
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot))
1053
1142
return
1054
1143
}
1055
1144
···
1135
1224
}
1136
1225
secret, err := db.GetRegistrationKey(rp.db, knot)
1137
1226
if err != nil {
1138
-
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", knot))
1227
+
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
1139
1228
return
1140
1229
}
1141
1230
+1
appview/repo/router.go
+1
appview/repo/router.go
···
70
70
})
71
71
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
72
72
r.Get("/", rp.RepoSettings)
73
+
r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle)
73
74
r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator)
74
75
r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo)
75
76
r.Put("/branches/default", rp.SetDefaultBranch)
+3
appview/reporesolver/resolver.go
+3
appview/reporesolver/resolver.go
···
31
31
RepoName string
32
32
RepoAt syntax.ATURI
33
33
Description string
34
+
Spindle string
34
35
CreatedAt string
35
36
Ref string
36
37
CurrentDir string
···
95
96
// pass through values from the middleware
96
97
description, ok := r.Context().Value("repoDescription").(string)
97
98
addedAt, ok := r.Context().Value("repoAddedAt").(string)
99
+
spindle, ok := r.Context().Value("repoSpindle").(string)
98
100
99
101
return &ResolvedRepo{
100
102
Knot: knot,
···
105
107
CreatedAt: addedAt,
106
108
Ref: ref,
107
109
CurrentDir: currentDir,
110
+
Spindle: spindle,
108
111
109
112
rr: rr,
110
113
}, nil