Lindenii Project Forge
Login

server

Lindenii Forge’s main backend daemon
Commit info
ID
9cf817e614a906c54990c74c28d4a6dcf9465731
Author
Runxi Yu <me@runxiyu.org>
Author date
Wed, 19 Mar 2025 12:19:57 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Wed, 19 Mar 2025 12:19:57 +0800
Actions
Remove underscores from Go code, pt 6
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

// globalData is passed as "global" when rendering HTML templates.
var globalData = map[string]any{
	"server_public_key_string":      &server_public_key_string,
	"server_public_key_fingerprint": &server_public_key_fingerprint,
	"server_public_key_string":      &serverPubkeyString,
	"server_public_key_fingerprint": &serverPubkeyFP,
	"forge_version":                 VERSION,
	// Some other ones are populated after config parsing
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"net/http"

	"github.com/jackc/pgx/v5"
)

type id_title_status_t struct {
type idTitleStatus struct {
	ID     int
	Title  string
	Status string
}

func httpHandleRepoContribIndex(w http.ResponseWriter, r *http.Request, params map[string]any) {
	var rows pgx.Rows
	var result []id_title_status_t
	var result []idTitleStatus
	var err error

	if rows, err = database.Query(r.Context(),
		"SELECT id, COALESCE(title, 'Untitled'), status FROM merge_requests WHERE repo_id = $1",
		params["repo_id"],
	); err != nil {
		http.Error(w, "Error querying merge requests: "+err.Error(), http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var id int
		var title, status string
		if err = rows.Scan(&id, &title, &status); err != nil {
			http.Error(w, "Error scanning merge request: "+err.Error(), http.StatusInternalServerError)
			return
		}
		result = append(result, id_title_status_t{id, title, status})
		result = append(result, idTitleStatus{id, title, status})
	}
	if err = rows.Err(); err != nil {
		http.Error(w, "Error ranging over merge requests: "+err.Error(), http.StatusInternalServerError)
		return
	}
	params["merge_requests"] = result

	renderTemplate(w, "repo_contrib_index", params)
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"net/http"
	"strings"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/storer"
)

func httpHandleRepoIndex(w http.ResponseWriter, r *http.Request, params map[string]any) {
	var repo *git.Repository
	var repo_name string
	var group_path []string
	var repoName string
	var groupPath []string
	var refHash plumbing.Hash
	var err error
	var recent_commits []*object.Commit
	var commit_object *object.Commit
	var recentCommits []*object.Commit
	var commitObj *object.Commit
	var tree *object.Tree
	var notes []string
	var branches []string
	var branches_ storer.ReferenceIter
	var branchesIter storer.ReferenceIter

	repo, repo_name, group_path = params["repo"].(*git.Repository), params["repo_name"].(string), params["group_path"].([]string)
	repo, repoName, groupPath = params["repo"].(*git.Repository), params["repo_name"].(string), params["group_path"].([]string)

	if strings.Contains(repo_name, "\n") || slice_contains_newline(group_path) {
	if strings.Contains(repoName, "\n") || sliceContainsNewlines(groupPath) {
		notes = append(notes, "Path contains newlines; HTTP Git access impossible")
	}

	refHash, err = getRefHash(repo, params["ref_type"].(string), params["ref_name"].(string))
	if err != nil {
		goto no_ref
	}

	branches_, err = repo.Branches()
	if err != nil {
	}
	err = branches_.ForEach(func(branch *plumbing.Reference) error {
		branches = append(branches, branch.Name().Short())
		return nil
	})
	if err != nil {
	branchesIter, err = repo.Branches()
	if err == nil {
		branchesIter.ForEach(func(branch *plumbing.Reference) error {
			branches = append(branches, branch.Name().Short())
			return nil
		})
	}
	params["branches"] = branches

	if recent_commits, err = getRecentCommits(repo, refHash, 3); err != nil {
	if recentCommits, err = getRecentCommits(repo, refHash, 3); err != nil {
		goto no_ref
	}
	params["commits"] = recent_commits
	params["commits"] = recentCommits

	if commit_object, err = repo.CommitObject(refHash); err != nil {
	if commitObj, err = repo.CommitObject(refHash); err != nil {
		goto no_ref
	}

	if tree, err = commit_object.Tree(); err != nil {
	if tree, err = commitObj.Tree(); err != nil {
		goto no_ref
	}

	params["files"] = makeDisplayTree(tree)
	params["readme_filename"], params["readme"] = renderReadmeAtTree(tree)

no_ref:

	params["http_clone_url"] = genHTTPRemoteURL(group_path, repo_name)
	params["ssh_clone_url"] = genSSHRemoteURL(group_path, repo_name)
	params["http_clone_url"] = genHTTPRemoteURL(groupPath, repoName)
	params["ssh_clone_url"] = genSSHRemoteURL(groupPath, repoName)
	params["notes"] = notes

	renderTemplate(w, "repo_index", params)
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"fmt"
	"net/http"
	"path"
	"strings"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
)

func httpHandleRepoRaw(w http.ResponseWriter, r *http.Request, params map[string]any) {
	var raw_path_spec, path_spec string
	var rawPathSpec, pathSpec string
	var repo *git.Repository
	var refHash plumbing.Hash
	var commit_object *object.Commit
	var commitObj *object.Commit
	var tree *object.Tree
	var err error

	raw_path_spec = params["rest"].(string)
	repo, path_spec = params["repo"].(*git.Repository), strings.TrimSuffix(raw_path_spec, "/")
	params["path_spec"] = path_spec
	rawPathSpec = params["rest"].(string)
	repo, pathSpec = params["repo"].(*git.Repository), strings.TrimSuffix(rawPathSpec, "/")
	params["path_spec"] = pathSpec

	if refHash, err = getRefHash(repo, params["ref_type"].(string), params["ref_name"].(string)); err != nil {
		http.Error(w, "Error getting ref hash: "+err.Error(), http.StatusInternalServerError)
		return
	}

	if commit_object, err = repo.CommitObject(refHash); err != nil {
	if commitObj, err = repo.CommitObject(refHash); err != nil {
		http.Error(w, "Error getting commit object: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if tree, err = commit_object.Tree(); err != nil {
	if tree, err = commitObj.Tree(); err != nil {
		http.Error(w, "Error getting file tree: "+err.Error(), http.StatusInternalServerError)
		return
	}

	var target *object.Tree
	if path_spec == "" {
	if pathSpec == "" {
		target = tree
	} else {
		if target, err = tree.Tree(path_spec); err != nil {
		if target, err = tree.Tree(pathSpec); err != nil {
			var file *object.File
			var file_contents string
			if file, err = tree.File(path_spec); err != nil {
			var fileContent string
			if file, err = tree.File(pathSpec); err != nil {
				http.Error(w, "Error retrieving path: "+err.Error(), http.StatusInternalServerError)
				return
			}
			if len(raw_path_spec) != 0 && raw_path_spec[len(raw_path_spec)-1] == '/' {
				http.Redirect(w, r, "../"+path_spec, http.StatusSeeOther)
			if len(rawPathSpec) != 0 && rawPathSpec[len(rawPathSpec)-1] == '/' {
				http.Redirect(w, r, "../"+pathSpec, http.StatusSeeOther)
				return
			}
			if file_contents, err = file.Contents(); err != nil {
			if fileContent, err = file.Contents(); err != nil {
				http.Error(w, "Error reading file: "+err.Error(), http.StatusInternalServerError)
				return
			}
			fmt.Fprint(w, file_contents)
			fmt.Fprint(w, fileContent)
			return
		}
	}

	if len(raw_path_spec) != 0 && raw_path_spec[len(raw_path_spec)-1] != '/' {
		http.Redirect(w, r, path.Base(path_spec)+"/", http.StatusSeeOther)
	if len(rawPathSpec) != 0 && rawPathSpec[len(rawPathSpec)-1] != '/' {
		http.Redirect(w, r, path.Base(pathSpec)+"/", http.StatusSeeOther)
		return
	}

	params["files"] = makeDisplayTree(target)

	renderTemplate(w, "repo_raw_dir", params)
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"io"
	"net/http"
	"os"
	"os/exec"

	"github.com/jackc/pgx/v5/pgtype"
)

func httpHandleUploadPack(w http.ResponseWriter, r *http.Request, params map[string]any) (err error) {
	var group_path []string
	var repo_name string
	var repo_path string
	var groupPath []string
	var repoName string
	var repoPath string
	var stdout io.ReadCloser
	var stdin io.WriteCloser
	var cmd *exec.Cmd

	group_path, repo_name = params["group_path"].([]string), params["repo_name"].(string)
	groupPath, repoName = params["group_path"].([]string), params["repo_name"].(string)

	if err := database.QueryRow(r.Context(), `
	WITH RECURSIVE group_path_cte AS (
		-- Start: match the first name in the path where parent_group IS NULL
		SELECT
			id,
			parent_group,
			name,
			1 AS depth
		FROM groups
		WHERE name = ($1::text[])[1]
			AND parent_group IS NULL
	
		UNION ALL
	
		-- Recurse: jion next segment of the path
		SELECT
			g.id,
			g.parent_group,
			g.name,
			group_path_cte.depth + 1
		FROM groups g
		JOIN group_path_cte ON g.parent_group = group_path_cte.id
		WHERE g.name = ($1::text[])[group_path_cte.depth + 1]
			AND group_path_cte.depth + 1 <= cardinality($1::text[])
	)
	SELECT r.filesystem_path
	FROM group_path_cte c
	JOIN repos r ON r.group_id = c.id
	WHERE c.depth = cardinality($1::text[])
		AND r.name = $2
	`,
		pgtype.FlatArray[string](group_path),
		repo_name,
	).Scan(&repo_path); err != nil {
		pgtype.FlatArray[string](groupPath),
		repoName,
	).Scan(&repoPath); err != nil {
		return err
	}

	w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
	w.Header().Set("Connection", "Keep-Alive")
	w.Header().Set("Transfer-Encoding", "chunked")
	w.WriteHeader(http.StatusOK)

	cmd = exec.Command("git", "upload-pack", "--stateless-rpc", repo_path)
	cmd = exec.Command("git", "upload-pack", "--stateless-rpc", repoPath)
	cmd.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+config.Hooks.Socket)
	if stdout, err = cmd.StdoutPipe(); err != nil {
		return err
	}
	cmd.Stderr = cmd.Stdout
	defer func() {
		_ = stdout.Close()
	}()

	if stdin, err = cmd.StdinPipe(); err != nil {
		return err
	}
	defer func() {
		_ = stdin.Close()
	}()

	if err = cmd.Start(); err != nil {
		return err
	}

	if _, err = io.Copy(stdin, r.Body); err != nil {
		return err
	}

	if err = stdin.Close(); err != nil {
		return err
	}

	if _, err = io.Copy(w, stdout); err != nil {
		return err
	}

	if err = cmd.Wait(); err != nil {
		return err
	}

	return nil
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"net/url"
	"strings"
)

// We don't use path.Join because it collapses multiple slashes into one.

func genSSHRemoteURL(group_path []string, repo_name string) string {
	return strings.TrimSuffix(config.SSH.Root, "/") + "/" + segmentsToURL(group_path) + "/:/repos/" + url.PathEscape(repo_name)
func genSSHRemoteURL(groupPath []string, repoName string) string {
	return strings.TrimSuffix(config.SSH.Root, "/") + "/" + segmentsToURL(groupPath) + "/:/repos/" + url.PathEscape(repoName)
}

func genHTTPRemoteURL(group_path []string, repo_name string) string {
	return strings.TrimSuffix(config.HTTP.Root, "/") + "/" + segmentsToURL(group_path) + "/:/repos/" + url.PathEscape(repo_name)
func genHTTPRemoteURL(groupPath []string, repoName string) string {
	return strings.TrimSuffix(config.HTTP.Root, "/") + "/" + segmentsToURL(groupPath) + "/:/repos/" + url.PathEscape(repoName)
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"fmt"
	"net"
	"os"
	"strings"

	glider_ssh "github.com/gliderlabs/ssh"
	gliderSSH "github.com/gliderlabs/ssh"
	"go.lindenii.runxiyu.org/lindenii-common/ansiec"
	"go.lindenii.runxiyu.org/lindenii-common/clog"
	go_ssh "golang.org/x/crypto/ssh"
	goSSH "golang.org/x/crypto/ssh"
)

var (
	server_public_key_string      string
	server_public_key_fingerprint string
	server_public_key             go_ssh.PublicKey
	serverPubkeyString string
	serverPubkeyFP     string
	serverPubkey       goSSH.PublicKey
)

func serveSSH(listener net.Listener) error {
	var host_key_bytes []byte
	var host_key go_ssh.Signer
	var host_key goSSH.Signer
	var err error
	var server *glider_ssh.Server
	var server *gliderSSH.Server

	if host_key_bytes, err = os.ReadFile(config.SSH.Key); err != nil {
		return err
	}

	if host_key, err = go_ssh.ParsePrivateKey(host_key_bytes); err != nil {
	if host_key, err = goSSH.ParsePrivateKey(host_key_bytes); err != nil {
		return err
	}

	server_public_key = host_key.PublicKey()
	server_public_key_string = string(go_ssh.MarshalAuthorizedKey(server_public_key))
	server_public_key_fingerprint = go_ssh.FingerprintSHA256(server_public_key)
	serverPubkey = host_key.PublicKey()
	serverPubkeyString = string(goSSH.MarshalAuthorizedKey(serverPubkey))
	serverPubkeyFP = goSSH.FingerprintSHA256(serverPubkey)

	server = &glider_ssh.Server{
		Handler: func(session glider_ssh.Session) {
	server = &gliderSSH.Server{
		Handler: func(session gliderSSH.Session) {
			client_public_key := session.PublicKey()
			var client_public_key_string string
			if client_public_key != nil {
				client_public_key_string = strings.TrimSuffix(string(go_ssh.MarshalAuthorizedKey(client_public_key)), "\n")
				client_public_key_string = strings.TrimSuffix(string(goSSH.MarshalAuthorizedKey(client_public_key)), "\n")
			}

			clog.Info("Incoming SSH: " + session.RemoteAddr().String() + " " + client_public_key_string + " " + session.RawCommand())
			fmt.Fprintln(session.Stderr(), ansiec.Blue+"Lindenii Forge "+VERSION+", source at "+strings.TrimSuffix(config.HTTP.Root, "/")+"/:/source/"+ansiec.Reset+"\r")

			cmd := session.Command()

			if len(cmd) < 2 {
				fmt.Fprintln(session.Stderr(), "Insufficient arguments\r")
				return
			}

			switch cmd[0] {
			case "git-upload-pack":
				if len(cmd) > 2 {
					fmt.Fprintln(session.Stderr(), "Too many arguments\r")
					return
				}
				err = sshHandleUploadPack(session, client_public_key_string, cmd[1])
			case "git-receive-pack":
				if len(cmd) > 2 {
					fmt.Fprintln(session.Stderr(), "Too many arguments\r")
					return
				}
				err = sshHandleRecvPack(session, client_public_key_string, cmd[1])
			default:
				fmt.Fprintln(session.Stderr(), "Unsupported command: "+cmd[0]+"\r")
				return
			}
			if err != nil {
				fmt.Fprintln(session.Stderr(), err.Error())
				return
			}
		},
		PublicKeyHandler:           func(ctx glider_ssh.Context, key glider_ssh.PublicKey) bool { return true },
		KeyboardInteractiveHandler: func(ctx glider_ssh.Context, challenge go_ssh.KeyboardInteractiveChallenge) bool { return true },
		PublicKeyHandler:           func(ctx gliderSSH.Context, key gliderSSH.PublicKey) bool { return true },
		KeyboardInteractiveHandler: func(ctx gliderSSH.Context, challenge goSSH.KeyboardInteractiveChallenge) bool { return true },
		// It is intentional that we do not check any credentials and accept all connections.
		// This allows all users to connect and clone repositories. However, the public key
		// is passed to handlers, so e.g. the push handler could check the key and reject the
		// push if it needs to.
	}

	server.AddHostKey(host_key)

	if err = server.Serve(listener); err != nil {
		clog.Fatal(1, "Serving SSH: "+err.Error())
	}

	return nil
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"strings"

	"go.lindenii.runxiyu.org/lindenii-common/ansiec"
)

var err_ssh_illegal_endpoint = errors.New("illegal endpoint during SSH access")
var errIllegalSSHRepoPath = errors.New("illegal SSH repo path")

func getRepoInfo2(ctx context.Context, ssh_path, ssh_pubkey string) (group_path []string, repo_name string, repo_id int, repo_path string, direct_access bool, contrib_requirements, user_type string, user_id int, err error) {
func getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (groupPath []string, repoName string, repoID int, repoPath string, directAccess bool, contribReq, userType string, userID int, err error) {
	var segments []string
	var separator_index int
	var module_type, module_name string
	var sepIndex int
	var moduleType, moduleName string

	segments = strings.Split(strings.TrimPrefix(ssh_path, "/"), "/")
	segments = strings.Split(strings.TrimPrefix(sshPath, "/"), "/")

	for i, segment := range segments {
		var err error
		segments[i], err = url.PathUnescape(segment)
		if err != nil {
			return []string{}, "", 0, "", false, "", "", 0, err
		}
	}

	if segments[0] == ":" {
		return []string{}, "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint
		return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath
	}

	separator_index = -1
	sepIndex = -1
	for i, part := range segments {
		if part == ":" {
			separator_index = i
			sepIndex = i
			break
		}
	}
	if segments[len(segments)-1] == "" {
		segments = segments[:len(segments)-1]
	}

	switch {
	case separator_index == -1:
		return []string{}, "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint
	case len(segments) <= separator_index+2:
		return []string{}, "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint
	case sepIndex == -1:
		return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath
	case len(segments) <= sepIndex+2:
		return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath
	}

	group_path = segments[:separator_index]
	module_type = segments[separator_index+1]
	module_name = segments[separator_index+2]
	repo_name = module_name
	switch module_type {
	groupPath = segments[:sepIndex]
	moduleType = segments[sepIndex+1]
	moduleName = segments[sepIndex+2]
	repoName = moduleName
	switch moduleType {
	case "repos":
		_1, _2, _3, _4, _5, _6, _7 := getRepoInfo(ctx, group_path, module_name, ssh_pubkey)
		return group_path, repo_name, _1, _2, _3, _4, _5, _6, _7
		_1, _2, _3, _4, _5, _6, _7 := getRepoInfo(ctx, groupPath, moduleName, sshPubkey)
		return groupPath, repoName, _1, _2, _3, _4, _5, _6, _7
	default:
		return []string{}, "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint
		return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath
	}
}

func writeRedError(w io.Writer, format string, args ...any) {
	fmt.Fprintln(w, ansiec.Red+fmt.Sprintf(format, args...)+ansiec.Reset)
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"errors"
	"net/http"
	"net/url"
	"strings"
)

var (
	errDupRefSpec = errors.New("duplicate ref spec")
	errNoRefSpec        = errors.New("no ref spec")
	errNoRefSpec  = errors.New("no ref spec")
)

func getParamRefTypeName(r *http.Request) (retRefType, retRefName string, err error) {
	qr := r.URL.RawQuery
	q, err := url.ParseQuery(qr)
	if err != nil {
		return
	}
	done := false
	for _, refType := range []string{"commit", "branch", "tag"} {
		refName, ok := q[refType]
		if ok {
			if done {
				err = errDupRefSpec
				return
			}
			done = true
			if len(refName) != 1 {
				err = errDupRefSpec
				return
			}
			retRefName = refName[0]
			retRefType = refType
		}
	}
	if !done {
		err = errNoRefSpec
	}
	return
}

func parseReqURI(requestURI string) (segments []string, params url.Values, err error) {
	path, paramsStr, _ := strings.Cut(requestURI, "?")

	segments = strings.Split(strings.TrimPrefix(path, "/"), "/")

	for i, segment := range segments {
		segments[i], err = url.PathUnescape(segment)
		if err != nil {
			return
		}
	}

	params, err = url.ParseQuery(paramsStr)
	return
}

func redirectDir(w http.ResponseWriter, r *http.Request) bool {
	requestURI := r.RequestURI

	pathEnd := strings.IndexAny(requestURI, "?#")
	var path, rest string
	if pathEnd == -1 {
		path = requestURI
	} else {
		path = requestURI[:pathEnd]
		rest = requestURI[pathEnd:]
	}

	if !strings.HasSuffix(path, "/") {
		http.Redirect(w, r, path+"/"+rest, http.StatusSeeOther)
		return true
	}
	return false
}

func redirectNoDir(w http.ResponseWriter, r *http.Request) bool {
	requestURI := r.RequestURI

	pathEnd := strings.IndexAny(requestURI, "?#")
	var path, rest string
	if pathEnd == -1 {
		path = requestURI
	} else {
		path = requestURI[:pathEnd]
		rest = requestURI[pathEnd:]
	}

	if strings.HasSuffix(path, "/") {
		http.Redirect(w, r, strings.TrimSuffix(path, "/")+rest, http.StatusSeeOther)
		return true
	}
	return false
}

func redirectUnconditionally(w http.ResponseWriter, r *http.Request) {
	requestURI := r.RequestURI

	pathEnd := strings.IndexAny(requestURI, "?#")
	var path, rest string
	if pathEnd == -1 {
		path = requestURI
	} else {
		path = requestURI[:pathEnd]
		rest = requestURI[pathEnd:]
	}

	http.Redirect(w, r, path+rest, http.StatusSeeOther)
}

func segmentsToURL(segments []string) string {
	for i, segment := range segments {
		segments[i] = url.PathEscape(segment)
	}
	return strings.Join(segments, "/")
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import (
	"context"

	"github.com/jackc/pgx/v5"
)

func addUserSSH(ctx context.Context, pubkey string) (user_id int, err error) {
func addUserSSH(ctx context.Context, pubkey string) (userID int, err error) {
	var tx pgx.Tx

	if tx, err = database.Begin(ctx); err != nil {
		return
	}
	defer func() {
		_ = tx.Rollback(ctx)
	}()

	if err = tx.QueryRow(ctx, `INSERT INTO users (type) VALUES ('pubkey_only') RETURNING id`).Scan(&user_id); err != nil {
	if err = tx.QueryRow(ctx, `INSERT INTO users (type) VALUES ('pubkey_only') RETURNING id`).Scan(&userID); err != nil {
		return
	}

	if _, err = tx.Exec(ctx, `INSERT INTO ssh_public_keys (key_string, user_id) VALUES ($1, $2)`, pubkey, user_id); err != nil {
	if _, err = tx.Exec(ctx, `INSERT INTO ssh_public_keys (key_string, user_id) VALUES ($1, $2)`, pubkey, userID); err != nil {
		return
	}

	err = tx.Commit(ctx)
	return
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>

package main

import "strings"

func slice_contains_newline(s []string) bool {
func sliceContainsNewlines(s []string) bool {
	for _, v := range s {
		if strings.Contains(v, "\n") {
			return true
		}
	}
	return false
}