forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview: add spindle to repo settings form

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li a6afe6f7 31f06683

verified
Changed files
+179 -24
appview
db
middleware
pages
templates
repo
reporesolver
+8
appview/db/db.go
···
})
db.Exec("pragma foreign_keys = on;")
+
// run migrations
+
runMigration(db, "add-spindle-to-repos", func(tx *sql.Tx) error {
+
tx.Exec(`
+
alter table repos add column spindle text;
+
`)
+
return nil
+
})
+
return &DB{db}, nil
}
+23 -7
appview/db/repos.go
···
Created time.Time
AtUri string
Description string
+
Spindle string
// optionally, populate this when querying for reverse mappings
RepoStats *RepoStats
···
func GetRepo(e Execer, did, name string) (*Repo, error) {
var repo Repo
-
var nullableDescription sql.NullString
+
var description, spindle sql.NullString
-
row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name)
+
row := e.QueryRow(`
+
select did, name, knot, created, at_uri, description, spindle
+
from repos
+
where did = ? and name = ?
+
`,
+
did,
+
name,
+
)
var createdAt string
-
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
+
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil {
return nil, err
}
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
repo.Created = createdAtTime
-
if nullableDescription.Valid {
-
repo.Description = nullableDescription.String
-
} else {
-
repo.Description = ""
+
if description.Valid {
+
repo.Description = description.String
+
}
+
+
if spindle.Valid {
+
repo.Spindle = spindle.String
}
return &repo, nil
···
func UpdateDescription(e Execer, repoAt, newDescription string) error {
_, err := e.Exec(
`update repos set description = ? where at_uri = ?`, newDescription, repoAt)
+
return err
+
}
+
+
func UpdateSpindle(e Execer, repoAt, spindle string) error {
+
_, err := e.Exec(
+
`update repos set spindle = ? where at_uri = ?`, spindle, repoAt)
return err
}
+1
appview/middleware/middleware.go
···
ctx := context.WithValue(req.Context(), "knot", repo.Knot)
ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
ctx = context.WithValue(ctx, "repoDescription", repo.Description)
+
ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle)
ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
next.ServeHTTP(w, req.WithContext(ctx))
})
+7 -5
appview/pages/pages.go
···
}
type RepoSettingsParams struct {
-
LoggedInUser *oauth.User
-
RepoInfo repoinfo.RepoInfo
-
Collaborators []Collaborator
-
Active string
-
Branches []types.Branch
+
LoggedInUser *oauth.User
+
RepoInfo repoinfo.RepoInfo
+
Collaborators []Collaborator
+
Active string
+
Branches []types.Branch
+
Spindles []string
+
CurrentSpindle string
// TODO: use repoinfo.roles
IsCollaboratorInviteAllowed bool
}
+37 -2
appview/pages/templates/repo/settings.html
···
</div>
</form>
+
{{ if .RepoInfo.Roles.IsOwner }}
+
<form
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/spindle"
+
class="mt-6 group"
+
>
+
<label for="spindle">spindle</label>
+
<div class="flex gap-2 items-center">
+
<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">
+
<option
+
value=""
+
disabled
+
selected
+
>
+
Choose a spindle
+
</option>
+
{{ range .Spindles }}
+
<option
+
value="{{ . }}"
+
class="py-1"
+
{{ if .eq . $.Repo.Spindle }}
+
selected
+
{{ end }}
+
>
+
{{ . }}
+
</option>
+
{{ end }}
+
</select>
+
<button class="btn my-2 flex gap-2 items-center" type="submit">
+
<span>save</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
</div>
+
</form>
+
{{ end }}
+
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
<form
hx-confirm="Are you sure you want to delete this repository?"
···
hx-indicator="#delete-repo-spinner"
>
<label for="branch">delete repository</label>
-
<button class="btn my-2 flex gap-2 items-center" type="text">
+
<button class="btn my-2 flex items-center" type="text">
<span>delete</span>
<span id="delete-repo-spinner" class="group">
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
{{ i "loader-circle" "pl-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
</span>
</button>
<span>
+99 -10
appview/repo/repo.go
···
})
return
case http.MethodPut:
-
user := rp.oauth.GetUser(r)
newDescription := r.FormValue("description")
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
···
return
}
+
// modify the spindle configured for this repo
+
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
log.Println("failed to get repo and knot", err)
+
w.WriteHeader(http.StatusBadRequest)
+
return
+
}
+
+
repoAt := f.RepoAt
+
rkey := repoAt.RecordKey().String()
+
if rkey == "" {
+
log.Println("invalid aturi for repo", err)
+
w.WriteHeader(http.StatusInternalServerError)
+
return
+
}
+
+
user := rp.oauth.GetUser(r)
+
+
newSpindle := r.FormValue("spindle")
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
log.Println("failed to get client")
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
+
return
+
}
+
+
// ensure that this is a valid spindle for this user
+
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
+
if err != nil {
+
log.Println("failed to get valid spindles")
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
+
return
+
}
+
+
if !slices.Contains(validSpindles, newSpindle) {
+
log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles)
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
+
return
+
}
+
+
// optimistic update
+
err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
+
if err != nil {
+
log.Println("failed to perform update-spindle query", err)
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
+
return
+
}
+
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
+
if err != nil {
+
// failed to get record
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.")
+
return
+
}
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoNSID,
+
Repo: user.Did,
+
Rkey: rkey,
+
SwapRecord: ex.Cid,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.Repo{
+
Knot: f.Knot,
+
Name: f.RepoName,
+
Owner: user.Did,
+
CreatedAt: f.CreatedAt,
+
Description: &f.Description,
+
Spindle: &newSpindle,
+
},
+
},
+
})
+
+
if err != nil {
+
log.Println("failed to perform update-spindle query", err)
+
// failed to get record
+
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.")
+
return
+
}
+
+
w.Write(fmt.Append(nil, "spindle set to: ", newSpindle))
+
}
+
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
f, err := rp.repoResolver.Resolve(r)
if err != nil {
···
}
if ksResp.StatusCode != http.StatusNoContent {
-
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
+
w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))
return
}
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
return
}
defer func() {
···
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
if err != nil {
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
return
}
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
if err != nil {
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
return
}
···
return
}
-
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
+
w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))
}
···
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
-
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
return
}
defer func() {
···
return
}
-
w.Write([]byte(fmt.Sprint("default branch set to: ", branch)))
+
w.Write(fmt.Append(nil, "default branch set to: ", branch))
}
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
···
return
+
spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
+
if err != nil {
+
log.Println("failed to fetch spindles", err)
+
return
+
}
+
rp.pages.RepoSettings(w, pages.RepoSettingsParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Collaborators: repoCollaborators,
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
Branches: result.Branches,
+
Spindles: spindles,
+
CurrentSpindle: f.Spindle,
})
···
case http.MethodPost:
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
if err != nil {
-
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", f.Knot))
+
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot))
return
···
secret, err := db.GetRegistrationKey(rp.db, knot)
if err != nil {
-
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", knot))
+
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
return
+1
appview/repo/router.go
···
})
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
r.Get("/", rp.RepoSettings)
+
r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle)
r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator)
r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo)
r.Put("/branches/default", rp.SetDefaultBranch)
+3
appview/reporesolver/resolver.go
···
RepoName string
RepoAt syntax.ATURI
Description string
+
Spindle string
CreatedAt string
Ref string
CurrentDir string
···
// pass through values from the middleware
description, ok := r.Context().Value("repoDescription").(string)
addedAt, ok := r.Context().Value("repoAddedAt").(string)
+
spindle, ok := r.Context().Value("repoSpindle").(string)
return &ResolvedRepo{
Knot: knot,
···
CreatedAt: addedAt,
Ref: ref,
CurrentDir: currentDir,
+
Spindle: spindle,
rr: rr,
}, nil