forked from tildetown/bbj2
		
	restructure stuff
This commit is contained in:
		
							parent
							
								
									62bff8ce22
								
							
						
					
					
						commit
						9d8bd4a2df
					
				
							
								
								
									
										45
									
								
								server/cmd/api/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								server/cmd/api/api.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | |||||||
|  | package api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"git.tilde.town/tildetown/bbj2/server/cmd/config" | ||||||
|  | 	"git.tilde.town/tildetown/bbj2/server/cmd/db" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type HTTPError struct { | ||||||
|  | 	Msg  string | ||||||
|  | 	Code int | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (e *HTTPError) Error() string { | ||||||
|  | 	return fmt.Sprintf("%d %s", e.Code, e.Msg) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type BBJResponse struct { | ||||||
|  | 	Error   bool               `json:"error"` | ||||||
|  | 	Data    interface{}        `json:"data"` | ||||||
|  | 	Usermap map[string]db.User `json:"usermap"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type API struct { | ||||||
|  | 	User *db.User | ||||||
|  | 	Opts config.Options | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *API) InstanceInfo() (*BBJResponse, error) { | ||||||
|  | 	type instanceInfo struct { | ||||||
|  | 		InstanceName string `json:"instance_name"` | ||||||
|  | 		AllowAnon    bool   `json:"allow_anon"` | ||||||
|  | 		Admins       []string | ||||||
|  | 	} | ||||||
|  | 	return &BBJResponse{ | ||||||
|  | 		Data: instanceInfo{ | ||||||
|  | 			InstanceName: a.Opts.Config.InstanceName, | ||||||
|  | 			AllowAnon:    a.Opts.Config.AllowAnon, | ||||||
|  | 			Admins:       a.Opts.Config.Admins, | ||||||
|  | 		}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ApiHandler func() (*BBJResponse, error) | ||||||
							
								
								
									
										22
									
								
								server/cmd/api/api_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								server/cmd/api/api_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | package api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"git.tilde.town/tildetown/bbj2/server/cmd/config" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestInstanceInfo(t *testing.T) { | ||||||
|  | 	ts := []struct { | ||||||
|  | 		name     string | ||||||
|  | 		opts     config.Options | ||||||
|  | 		wantResp *BBJResponse | ||||||
|  | 		wantErr  HTTPError | ||||||
|  | 	}{} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range ts { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// TODO | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -1,7 +1,9 @@ | |||||||
| package main | package config | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"database/sql" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
| 
 | 
 | ||||||
| 	yaml "gopkg.in/yaml.v3" | 	yaml "gopkg.in/yaml.v3" | ||||||
| @ -14,7 +16,32 @@ const ( | |||||||
| 	defaultDBPath       = "db.sqlite3" | 	defaultDBPath       = "db.sqlite3" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func parseConfig(configPath string) (*Config, error) { | type IOStreams struct { | ||||||
|  | 	Err io.Writer | ||||||
|  | 	Out io.Writer | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Config struct { | ||||||
|  | 	Admins       []string | ||||||
|  | 	Port         int | ||||||
|  | 	Host         string | ||||||
|  | 	InstanceName string `yaml:"instance_name"` | ||||||
|  | 	AllowAnon    bool   `yaml:"allow_anon"` | ||||||
|  | 	Debug        bool | ||||||
|  | 	DBPath       string `yaml:"db_path"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Options struct { | ||||||
|  | 	ConfigPath string | ||||||
|  | 	IO         IOStreams | ||||||
|  | 	Log        func(string) | ||||||
|  | 	Logf       func(string, ...interface{}) | ||||||
|  | 	Config     Config | ||||||
|  | 	DB         *sql.DB | ||||||
|  | 	Reset      bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ParseConfig(configPath string) (*Config, error) { | ||||||
| 	cfgBytes, err := os.ReadFile(configPath) | 	cfgBytes, err := os.ReadFile(configPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("failed to read config file: %w", err) | 		return nil, fmt.Errorf("failed to read config file: %w", err) | ||||||
							
								
								
									
										7
									
								
								server/cmd/db/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/cmd/db/db.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | package db | ||||||
|  | 
 | ||||||
|  | type User struct { | ||||||
|  | 	ID       string | ||||||
|  | 	Username string | ||||||
|  | 	Hash     string | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								server/cmd/example_config.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								server/cmd/example_config.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | allow_anon: true | ||||||
|  | instance_name: "t i l d e . t o w n" | ||||||
|  | admins: ["vilmibm", "natalia", "archangelic"] | ||||||
|  | port: 8099 | ||||||
| @ -7,13 +7,14 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/google/uuid" | 	"git.tilde.town/tildetown/bbj2/server/cmd/api" | ||||||
|  | 	"git.tilde.town/tildetown/bbj2/server/cmd/config" | ||||||
|  | 	"git.tilde.town/tildetown/bbj2/server/cmd/db" | ||||||
| 	_ "github.com/mattn/go-sqlite3" | 	_ "github.com/mattn/go-sqlite3" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -22,40 +23,15 @@ import ( | |||||||
| //go:embed schema.sql | //go:embed schema.sql | ||||||
| var schemaSQL string | var schemaSQL string | ||||||
| 
 | 
 | ||||||
| type Config struct { |  | ||||||
| 	Admins       []string |  | ||||||
| 	Port         int |  | ||||||
| 	Host         string |  | ||||||
| 	InstanceName string `yaml:"instance_name"` |  | ||||||
| 	AllowAnon    bool   `yaml:"allow_anon"` |  | ||||||
| 	Debug        bool |  | ||||||
| 	DBPath       string `yaml:"db_path"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type iostreams struct { |  | ||||||
| 	Err io.Writer |  | ||||||
| 	Out io.Writer |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type options struct { |  | ||||||
| 	ConfigPath string |  | ||||||
| 	IO         iostreams |  | ||||||
| 	Log        func(string) |  | ||||||
| 	Logf       func(string, ...interface{}) |  | ||||||
| 	Config     Config |  | ||||||
| 	DB         *sql.DB |  | ||||||
| 	Reset      bool |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func main() { | func main() { | ||||||
| 	var configFlag = flag.String("config", "config.yml", "A path to a config file.") | 	var configFlag = flag.String("config", "config.yml", "A path to a config file.") | ||||||
| 	var resetFlag = flag.Bool("reset", false, "reset the database. WARNING this wipes everything.") | 	var resetFlag = flag.Bool("reset", false, "reset the database. WARNING this wipes everything.") | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 	io := iostreams{ | 	io := config.IOStreams{ | ||||||
| 		Err: os.Stderr, | 		Err: os.Stderr, | ||||||
| 		Out: os.Stdout, | 		Out: os.Stdout, | ||||||
| 	} | 	} | ||||||
| 	opts := &options{ | 	opts := &config.Options{ | ||||||
| 		ConfigPath: *configFlag, | 		ConfigPath: *configFlag, | ||||||
| 		Reset:      *resetFlag, | 		Reset:      *resetFlag, | ||||||
| 		IO:         io, | 		IO:         io, | ||||||
| @ -77,14 +53,14 @@ func main() { | |||||||
| 
 | 
 | ||||||
| type Teardown func() | type Teardown func() | ||||||
| 
 | 
 | ||||||
| func setupDB(opts *options) (Teardown, error) { | func setupDB(opts *config.Options) (Teardown, error) { | ||||||
| 	db, err := sql.Open("sqlite3", opts.Config.DBPath) | 	db, err := sql.Open("sqlite3", opts.Config.DBPath) | ||||||
| 	opts.DB = db | 	opts.DB = db | ||||||
| 	return func() { db.Close() }, err | 	return func() { db.Close() }, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func _main(opts *options) error { | func _main(opts *config.Options) error { | ||||||
| 	cfg, err := parseConfig(opts.ConfigPath) | 	cfg, err := config.ParseConfig(opts.ConfigPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, "could not read config file '%s'", opts.ConfigPath) | 		fmt.Fprintf(os.Stderr, "could not read config file '%s'", opts.ConfigPath) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -114,7 +90,7 @@ func _main(opts *options) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func ensureSchema(opts options) error { | func ensureSchema(opts config.Options) error { | ||||||
| 	db := opts.DB | 	db := opts.DB | ||||||
| 
 | 
 | ||||||
| 	if opts.Reset { | 	if opts.Reset { | ||||||
| @ -149,7 +125,7 @@ func ensureSchema(opts options) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func handler(opts options, f http.HandlerFunc) http.HandlerFunc { | func handler(opts config.Options, f http.HandlerFunc) http.HandlerFunc { | ||||||
| 	// TODO make this more real | 	// TODO make this more real | ||||||
| 	return func(w http.ResponseWriter, req *http.Request) { | 	return func(w http.ResponseWriter, req *http.Request) { | ||||||
| 		opts.Log(req.URL.Path) | 		opts.Log(req.URL.Path) | ||||||
| @ -162,46 +138,27 @@ func handler(opts options, f http.HandlerFunc) http.HandlerFunc { | |||||||
| // encryption, it doesn't really help anything. I'd rather have plaintext + | // encryption, it doesn't really help anything. I'd rather have plaintext + | ||||||
| // transport encryption and then, on the server side, proper salted hashing. | // transport encryption and then, on the server side, proper salted hashing. | ||||||
| 
 | 
 | ||||||
| type User struct { | func writeResponse(w http.ResponseWriter, resp api.BBJResponse) { | ||||||
| 	ID       string |  | ||||||
| 	Username string |  | ||||||
| 	Hash     string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type BBJResponse struct { |  | ||||||
| 	Error   bool            `json:"error"` |  | ||||||
| 	Data    interface{}     `json:"data"` |  | ||||||
| 	Usermap map[string]User `json:"usermap"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func writeResponse(w http.ResponseWriter, resp BBJResponse) { |  | ||||||
| 	w.WriteHeader(http.StatusOK) | 	w.WriteHeader(http.StatusOK) | ||||||
| 	w.Header().Set("Content-Type", "application/json") | 	w.Header().Set("Content-Type", "application/json") | ||||||
| 	json.NewEncoder(w).Encode(resp) | 	json.NewEncoder(w).Encode(resp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NB breaking: i'm not just returning 200 always but using http status codes | // NB breaking: i'm not just returning 200 always but using http status codes | ||||||
| func writeErrorResponse(w http.ResponseWriter, code int, resp BBJResponse) { | func writeErrorResponse(w http.ResponseWriter, code int, resp api.BBJResponse) { | ||||||
| 	w.WriteHeader(code) | 	w.WriteHeader(code) | ||||||
| 	w.Header().Set("Content-Type", "application/json") | 	w.Header().Set("Content-Type", "application/json") | ||||||
| 	json.NewEncoder(w).Encode(resp) | 	json.NewEncoder(w).Encode(resp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func getUserFromReq(opts options, req *http.Request) (u User, err error) { | func getUserFromReq(opts config.Options, req *http.Request) (u *db.User, err error) { | ||||||
|  | 	u = &db.User{} | ||||||
| 	u.Username = req.Header.Get("User") | 	u.Username = req.Header.Get("User") | ||||||
| 	u.Hash = req.Header.Get("Auth") | 	u.Hash = req.Header.Get("Auth") | ||||||
| 	if u.Username == "" { | 	if u.Username == "" || u.Username == "anon" { | ||||||
| 		err = errors.New("no User header set") |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if u.Username == "anon" { |  | ||||||
| 		if !opts.Config.AllowAnon { |  | ||||||
| 			err = errors.New("anonymous access disabled") |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	db := opts.DB | 	db := opts.DB | ||||||
| 	stmt, err := db.Prepare("select auth_hash, id from users where user_name = ?") | 	stmt, err := db.Prepare("select auth_hash, id from users where user_name = ?") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -228,7 +185,7 @@ func getUserFromReq(opts options, req *http.Request) (u User, err error) { | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func checkAuth(opts options, username, hash string) error { | func checkAuth(opts config.Options, username, hash string) error { | ||||||
| 	db := opts.DB | 	db := opts.DB | ||||||
| 	stmt, err := db.Prepare("select auth_hash from users where user_name = ?") | 	stmt, err := db.Prepare("select auth_hash from users where user_name = ?") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -253,325 +210,350 @@ func checkAuth(opts options, username, hash string) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func setupAPI(opts options) { | func setupAPI(opts config.Options) { | ||||||
| 	serverErr := func(w http.ResponseWriter, err error) { | 	newAPI := func(opts config.Options, w http.ResponseWriter, req *http.Request) *api.API { | ||||||
| 		opts.Logf(err.Error()) |  | ||||||
| 		writeErrorResponse(w, 500, BBJResponse{ |  | ||||||
| 			Error: true, |  | ||||||
| 			Data:  "server error", |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	badMethod := func(w http.ResponseWriter) { |  | ||||||
| 		writeErrorResponse(w, 400, BBJResponse{ |  | ||||||
| 			Error: true, |  | ||||||
| 			Data:  "bad method", |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	invalidArgs := func(w http.ResponseWriter) { |  | ||||||
| 		writeErrorResponse(w, 400, BBJResponse{ |  | ||||||
| 			Error: true, |  | ||||||
| 			Data:  "invalid args", |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/instance_info", handler(opts, func(w http.ResponseWriter, req *http.Request) { |  | ||||||
| 		type instanceInfo struct { |  | ||||||
| 			InstanceName string `json:"instance_name"` |  | ||||||
| 			AllowAnon    bool   `json:"allow_anon"` |  | ||||||
| 			Admins       []string |  | ||||||
| 		} |  | ||||||
| 		writeResponse(w, BBJResponse{ |  | ||||||
| 			Data: instanceInfo{ |  | ||||||
| 				InstanceName: opts.Config.InstanceName, |  | ||||||
| 				AllowAnon:    opts.Config.AllowAnon, |  | ||||||
| 				Admins:       opts.Config.Admins, |  | ||||||
| 			}, |  | ||||||
| 		}) |  | ||||||
| 	})) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/user_register", handler(opts, func(w http.ResponseWriter, req *http.Request) { |  | ||||||
| 		if req.Method != "POST" { |  | ||||||
| 			badMethod(w) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		type AuthArgs struct { |  | ||||||
| 			Username string `json:"user_name"` |  | ||||||
| 			Hash     string `json:"auth_hash"` |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var args AuthArgs |  | ||||||
| 		if err := json.NewDecoder(req.Body).Decode(&args); err != nil { |  | ||||||
| 			invalidArgs(w) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if args.Hash == "" || args.Username == "" { |  | ||||||
| 			invalidArgs(w) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		opts.Logf("querying for %s", args.Username) |  | ||||||
| 
 |  | ||||||
| 		if err := checkAuth(opts, args.Username, args.Hash); err == nil { |  | ||||||
| 			opts.Logf("found %s", args.Username) |  | ||||||
| 			// code 4 apparently |  | ||||||
| 			writeErrorResponse(w, 403, BBJResponse{ |  | ||||||
| 				Error: true, |  | ||||||
| 				Data:  "user already exists", |  | ||||||
| 			}) |  | ||||||
| 		} else if err.Error() != "no such user" { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		db := opts.DB |  | ||||||
| 		stmt, err := db.Prepare(`INSERT INTO users VALUES (?, ?, ?, "", "", 0, 0, ?)`) |  | ||||||
| 		id, err := uuid.NewRandom() |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		_, err = stmt.Exec(id, args.Username, args.Hash, time.Now()) |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		writeResponse(w, BBJResponse{ |  | ||||||
| 			Data: true, // TODO probably something else |  | ||||||
| 			// TODO prob usermap |  | ||||||
| 		}) |  | ||||||
| 	})) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/check_auth", handler(opts, func(w http.ResponseWriter, req *http.Request) { |  | ||||||
| 		if req.Method != "POST" { |  | ||||||
| 			badMethod(w) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		type AuthArgs struct { |  | ||||||
| 			Username string `json:"target_user"` |  | ||||||
| 			AuthHash string `json:"target_hash"` |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var args AuthArgs |  | ||||||
| 		if err := json.NewDecoder(req.Body).Decode(&args); err != nil { |  | ||||||
| 			invalidArgs(w) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		opts.Logf("got %s %s", args.Username, args.AuthHash) |  | ||||||
| 
 |  | ||||||
| 		db := opts.DB |  | ||||||
| 
 |  | ||||||
| 		stmt, err := db.Prepare("select auth_hash from users where user_name = ?") |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		defer stmt.Close() |  | ||||||
| 
 |  | ||||||
| 		var authHash string |  | ||||||
| 		err = stmt.QueryRow(args.Username).Scan(&authHash) |  | ||||||
| 		if err != nil { |  | ||||||
| 			if strings.Contains(err.Error(), "no rows in result") { |  | ||||||
| 				opts.Logf("user not found") |  | ||||||
| 				writeErrorResponse(w, 404, BBJResponse{ |  | ||||||
| 					Error: true, |  | ||||||
| 					Data:  "user not found", |  | ||||||
| 				}) |  | ||||||
| 			} else { |  | ||||||
| 				serverErr(w, err) |  | ||||||
| 			} |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// TODO unique constraint on user_name |  | ||||||
| 
 |  | ||||||
| 		if authHash != args.AuthHash { |  | ||||||
| 			http.Error(w, "incorrect password", 403) |  | ||||||
| 			writeErrorResponse(w, 403, BBJResponse{ |  | ||||||
| 				Error: true, |  | ||||||
| 				Data:  "incorrect password", |  | ||||||
| 			}) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// TODO include usermap? |  | ||||||
| 		writeResponse(w, BBJResponse{ |  | ||||||
| 			Data: true, |  | ||||||
| 		}) |  | ||||||
| 	})) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/thread_index", handler(opts, func(w http.ResponseWriter, req *http.Request) { |  | ||||||
| 		db := opts.DB |  | ||||||
| 		rows, err := db.Query("SELECT * FROM threads JOIN messages ON threads.thread_id = messages.thread_id") |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		defer rows.Close() |  | ||||||
| 		for rows.Next() { |  | ||||||
| 			var id string |  | ||||||
| 			err = rows.Scan(&id) |  | ||||||
| 			if err != nil { |  | ||||||
| 				serverErr(w, err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			opts.Log(id) |  | ||||||
| 		} |  | ||||||
| 		writeResponse(w, BBJResponse{Data: "TODO"}) |  | ||||||
| 		// TODO |  | ||||||
| 	})) |  | ||||||
| 
 |  | ||||||
| 	http.HandleFunc("/thread_create", handler(opts, func(w http.ResponseWriter, req *http.Request) { |  | ||||||
| 		if req.Method != "POST" { |  | ||||||
| 			badMethod(w) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// TODO make this getUserInfoFromReq or similar so we can use the user ID later |  | ||||||
| 		user, err := getUserFromReq(opts, req) | 		user, err := getUserFromReq(opts, req) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			writeErrorResponse(w, 403, BBJResponse{ | 			writeErrorResponse(w, 403, api.BBJResponse{ | ||||||
| 				Error: true, | 				Error: true, | ||||||
| 				Data:  err.Error(), | 				Data:  err.Error(), | ||||||
| 			}) | 			}) | ||||||
| 			return | 			return nil | ||||||
| 		} | 		} | ||||||
| 
 | 		return &api.API{ | ||||||
| 		type threadCreateArgs struct { | 			Opts: opts, | ||||||
| 			Title   string | 			User: user, | ||||||
| 			Body    string |  | ||||||
| 			SendRaw bool `json:"send_raw"` |  | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 		var args threadCreateArgs | 	invokeAPI := func(w http.ResponseWriter, apiFn api.ApiHandler) { | ||||||
| 		if err := json.NewDecoder(req.Body).Decode(&args); err != nil { | 		resp, err := apiFn() | ||||||
| 			invalidArgs(w) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if args.Title == "" || args.Body == "" { |  | ||||||
| 			invalidArgs(w) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		db := opts.DB |  | ||||||
| 		tx, err := db.Begin() |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			serverErr(w, err) | 			he := &api.HTTPError{} | ||||||
| 			return | 			_ = errors.As(err, &he) | ||||||
| 		} | 			resp := api.BBJResponse{ | ||||||
| 
 | 				Error: true, | ||||||
| 		stmt, err := tx.Prepare("insert into threads VALUES ( ?, ?, ?, ?, ?, 0, 0, ? )") | 				Data:  he.Msg, | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		defer stmt.Close() |  | ||||||
| 
 |  | ||||||
| 		threadID, err := uuid.NewRandom() |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		now := time.Now() |  | ||||||
| 		if _, err = stmt.Exec( |  | ||||||
| 			threadID, |  | ||||||
| 			user.ID, |  | ||||||
| 			args.Title, |  | ||||||
| 			now, |  | ||||||
| 			now, |  | ||||||
| 			user.Username, |  | ||||||
| 		); err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		stmt, err = tx.Prepare("insert into messages values ( ?, 1, ?, ?, 0, ?, ? )") |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		defer stmt.Close() |  | ||||||
| 
 |  | ||||||
| 		if _, err = stmt.Exec( |  | ||||||
| 			threadID, |  | ||||||
| 			user.ID, |  | ||||||
| 			now, |  | ||||||
| 			args.Body, |  | ||||||
| 			args.SendRaw, |  | ||||||
| 		); err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if err = tx.Commit(); err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		stmt, err = db.Prepare("select * from threads where thread_id = ? limit 1") |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		defer stmt.Close() |  | ||||||
| 
 |  | ||||||
| 		t := &Thread{} |  | ||||||
| 
 |  | ||||||
| 		// TODO fill in rest of thread |  | ||||||
| 		if err = stmt.QueryRow(threadID).Scan( |  | ||||||
| 			t.ID, |  | ||||||
| 			t.Author, |  | ||||||
| 			t.Title, |  | ||||||
| 			t.LastMod, |  | ||||||
| 			t.Created, |  | ||||||
| 			t.ReplyCount, |  | ||||||
| 			t.Pinned, |  | ||||||
| 			t.LastAuthor, |  | ||||||
| 		); err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		stmt, err = db.Prepare("select * from messages where thread_id = ?") |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		defer stmt.Close() |  | ||||||
| 		rows, err := stmt.Query(threadID) |  | ||||||
| 		if err != nil { |  | ||||||
| 			serverErr(w, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		t.Messages = []Message{} |  | ||||||
| 
 |  | ||||||
| 		for rows.Next() { |  | ||||||
| 			m := &Message{} |  | ||||||
| 			if err := rows.Scan( |  | ||||||
| 				m.ThreadID, |  | ||||||
| 				m.PostID, |  | ||||||
| 				m.Author, |  | ||||||
| 				m.Created, |  | ||||||
| 				m.Edited, |  | ||||||
| 				m.Body, |  | ||||||
| 				m.SendRaw, |  | ||||||
| 			); err != nil { |  | ||||||
| 				serverErr(w, err) |  | ||||||
| 				return |  | ||||||
| 			} | 			} | ||||||
| 			t.Messages = append(t.Messages, *m) | 			w.WriteHeader(he.Code) | ||||||
|  | 			w.Header().Set("Content-Type", "application/json") | ||||||
|  | 			json.NewEncoder(w).Encode(resp) | ||||||
|  | 			return | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		writeResponse(w, BBJResponse{Data: t}) | 		w.WriteHeader(http.StatusOK) | ||||||
|  | 		w.Header().Set("Content-Type", "application/json") | ||||||
|  | 		json.NewEncoder(w).Encode(resp) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	http.HandleFunc("/instance_info", handler(opts, func(w http.ResponseWriter, req *http.Request) { | ||||||
|  | 		api := newAPI(opts, w, req) | ||||||
|  | 		if api == nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		invokeAPI(w, api.InstanceInfo) | ||||||
| 	})) | 	})) | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  | 		http.HandleFunc("/instance_info", handler(opts, func(w http.ResponseWriter, req *http.Request) { | ||||||
|  | 			type instanceInfo struct { | ||||||
|  | 				InstanceName string `json:"instance_name"` | ||||||
|  | 				AllowAnon    bool   `json:"allow_anon"` | ||||||
|  | 				Admins       []string | ||||||
|  | 			} | ||||||
|  | 			writeResponse(w, BBJResponse{ | ||||||
|  | 				Data: instanceInfo{ | ||||||
|  | 					InstanceName: opts.Config.InstanceName, | ||||||
|  | 					AllowAnon:    opts.Config.AllowAnon, | ||||||
|  | 					Admins:       opts.Config.Admins, | ||||||
|  | 				}, | ||||||
|  | 			}) | ||||||
|  | 		})) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 			http.HandleFunc("/user_register", handler(opts, func(w http.ResponseWriter, req *http.Request) { | ||||||
|  | 				if req.Method != "POST" { | ||||||
|  | 					badMethod(w) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				type AuthArgs struct { | ||||||
|  | 					Username string `json:"user_name"` | ||||||
|  | 					Hash     string `json:"auth_hash"` | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				var args AuthArgs | ||||||
|  | 				if err := json.NewDecoder(req.Body).Decode(&args); err != nil { | ||||||
|  | 					invalidArgs(w) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if args.Hash == "" || args.Username == "" { | ||||||
|  | 					invalidArgs(w) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				opts.Logf("querying for %s", args.Username) | ||||||
|  | 
 | ||||||
|  | 				if err := checkAuth(opts, args.Username, args.Hash); err == nil { | ||||||
|  | 					opts.Logf("found %s", args.Username) | ||||||
|  | 					// code 4 apparently | ||||||
|  | 					writeErrorResponse(w, 403, BBJResponse{ | ||||||
|  | 						Error: true, | ||||||
|  | 						Data:  "user already exists", | ||||||
|  | 					}) | ||||||
|  | 				} else if err.Error() != "no such user" { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				db := opts.DB | ||||||
|  | 				stmt, err := db.Prepare(`INSERT INTO users VALUES (?, ?, ?, "", "", 0, 0, ?)`) | ||||||
|  | 				id, err := uuid.NewRandom() | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				_, err = stmt.Exec(id, args.Username, args.Hash, time.Now()) | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				writeResponse(w, BBJResponse{ | ||||||
|  | 					Data: true, // TODO probably something else | ||||||
|  | 					// TODO prob usermap | ||||||
|  | 				}) | ||||||
|  | 			})) | ||||||
|  | 
 | ||||||
|  | 			http.HandleFunc("/check_auth", handler(opts, func(w http.ResponseWriter, req *http.Request) { | ||||||
|  | 				if req.Method != "POST" { | ||||||
|  | 					badMethod(w) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				type AuthArgs struct { | ||||||
|  | 					Username string `json:"target_user"` | ||||||
|  | 					AuthHash string `json:"target_hash"` | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				var args AuthArgs | ||||||
|  | 				if err := json.NewDecoder(req.Body).Decode(&args); err != nil { | ||||||
|  | 					invalidArgs(w) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				opts.Logf("got %s %s", args.Username, args.AuthHash) | ||||||
|  | 
 | ||||||
|  | 				db := opts.DB | ||||||
|  | 
 | ||||||
|  | 				stmt, err := db.Prepare("select auth_hash from users where user_name = ?") | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				defer stmt.Close() | ||||||
|  | 
 | ||||||
|  | 				var authHash string | ||||||
|  | 				err = stmt.QueryRow(args.Username).Scan(&authHash) | ||||||
|  | 				if err != nil { | ||||||
|  | 					if strings.Contains(err.Error(), "no rows in result") { | ||||||
|  | 						opts.Logf("user not found") | ||||||
|  | 						writeErrorResponse(w, 404, BBJResponse{ | ||||||
|  | 							Error: true, | ||||||
|  | 							Data:  "user not found", | ||||||
|  | 						}) | ||||||
|  | 					} else { | ||||||
|  | 						serverErr(w, err) | ||||||
|  | 					} | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// TODO unique constraint on user_name | ||||||
|  | 
 | ||||||
|  | 				if authHash != args.AuthHash { | ||||||
|  | 					http.Error(w, "incorrect password", 403) | ||||||
|  | 					writeErrorResponse(w, 403, BBJResponse{ | ||||||
|  | 						Error: true, | ||||||
|  | 						Data:  "incorrect password", | ||||||
|  | 					}) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// TODO include usermap? | ||||||
|  | 				writeResponse(w, BBJResponse{ | ||||||
|  | 					Data: true, | ||||||
|  | 				}) | ||||||
|  | 			})) | ||||||
|  | 
 | ||||||
|  | 			http.HandleFunc("/thread_index", handler(opts, func(w http.ResponseWriter, req *http.Request) { | ||||||
|  | 				db := opts.DB | ||||||
|  | 				rows, err := db.Query("SELECT * FROM threads JOIN messages ON threads.thread_id = messages.thread_id") | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				defer rows.Close() | ||||||
|  | 				for rows.Next() { | ||||||
|  | 					var id string | ||||||
|  | 					err = rows.Scan(&id) | ||||||
|  | 					if err != nil { | ||||||
|  | 						serverErr(w, err) | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 					opts.Log(id) | ||||||
|  | 				} | ||||||
|  | 				writeResponse(w, BBJResponse{Data: "TODO"}) | ||||||
|  | 				// TODO | ||||||
|  | 			})) | ||||||
|  | 
 | ||||||
|  | 			http.HandleFunc("/thread_create", handler(opts, func(w http.ResponseWriter, req *http.Request) { | ||||||
|  | 				if req.Method != "POST" { | ||||||
|  | 					badMethod(w) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// TODO make this getUserInfoFromReq or similar so we can use the user ID later | ||||||
|  | 				user, err := getUserFromReq(opts, req) | ||||||
|  | 				if err != nil { | ||||||
|  | 					writeErrorResponse(w, 403, BBJResponse{ | ||||||
|  | 						Error: true, | ||||||
|  | 						Data:  err.Error(), | ||||||
|  | 					}) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				type threadCreateArgs struct { | ||||||
|  | 					Title   string | ||||||
|  | 					Body    string | ||||||
|  | 					SendRaw bool `json:"send_raw"` | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				var args threadCreateArgs | ||||||
|  | 				if err := json.NewDecoder(req.Body).Decode(&args); err != nil { | ||||||
|  | 					invalidArgs(w) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if args.Title == "" || args.Body == "" { | ||||||
|  | 					invalidArgs(w) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				db := opts.DB | ||||||
|  | 				tx, err := db.Begin() | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				stmt, err := tx.Prepare("insert into threads VALUES ( ?, ?, ?, ?, ?, 0, 0, ? )") | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				defer stmt.Close() | ||||||
|  | 
 | ||||||
|  | 				threadID, err := uuid.NewRandom() | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				now := time.Now() | ||||||
|  | 				if _, err = stmt.Exec( | ||||||
|  | 					threadID, | ||||||
|  | 					user.ID, | ||||||
|  | 					args.Title, | ||||||
|  | 					now, | ||||||
|  | 					now, | ||||||
|  | 					user.Username, | ||||||
|  | 				); err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				stmt, err = tx.Prepare("insert into messages values ( ?, 1, ?, ?, 0, ?, ? )") | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				defer stmt.Close() | ||||||
|  | 
 | ||||||
|  | 				if _, err = stmt.Exec( | ||||||
|  | 					threadID, | ||||||
|  | 					user.ID, | ||||||
|  | 					now, | ||||||
|  | 					args.Body, | ||||||
|  | 					args.SendRaw, | ||||||
|  | 				); err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if err = tx.Commit(); err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				stmt, err = db.Prepare("select * from threads where thread_id = ? limit 1") | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				defer stmt.Close() | ||||||
|  | 
 | ||||||
|  | 				t := &Thread{} | ||||||
|  | 
 | ||||||
|  | 				// TODO fill in rest of thread | ||||||
|  | 				if err = stmt.QueryRow(threadID).Scan( | ||||||
|  | 					t.ID, | ||||||
|  | 					t.Author, | ||||||
|  | 					t.Title, | ||||||
|  | 					t.LastMod, | ||||||
|  | 					t.Created, | ||||||
|  | 					t.ReplyCount, | ||||||
|  | 					t.Pinned, | ||||||
|  | 					t.LastAuthor, | ||||||
|  | 				); err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				stmt, err = db.Prepare("select * from messages where thread_id = ?") | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				defer stmt.Close() | ||||||
|  | 				rows, err := stmt.Query(threadID) | ||||||
|  | 				if err != nil { | ||||||
|  | 					serverErr(w, err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				t.Messages = []Message{} | ||||||
|  | 
 | ||||||
|  | 				for rows.Next() { | ||||||
|  | 					m := &Message{} | ||||||
|  | 					if err := rows.Scan( | ||||||
|  | 						m.ThreadID, | ||||||
|  | 						m.PostID, | ||||||
|  | 						m.Author, | ||||||
|  | 						m.Created, | ||||||
|  | 						m.Edited, | ||||||
|  | 						m.Body, | ||||||
|  | 						m.SendRaw, | ||||||
|  | 					); err != nil { | ||||||
|  | 						serverErr(w, err) | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 					t.Messages = append(t.Messages, *m) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				writeResponse(w, BBJResponse{Data: t}) | ||||||
|  | 
 | ||||||
|  | 			})) | ||||||
|  | 	*/ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Thread struct { | type Thread struct { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user