aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-10-27 01:38:30 +0900
committernsfisis <nsfisis@gmail.com>2025-10-27 01:53:05 +0900
commitaf87dd1de0c1dd0e6284170aef807224d5c8ccb5 (patch)
tree50434d908ad3ee5a1ccd9414f8a13c0a9dee05f5
parent469e469dd3f77bc8cd22bbcd87017eb69569ac23 (diff)
downloadfeedaka-af87dd1de0c1dd0e6284170aef807224d5c8ccb5.tar.gz
feedaka-af87dd1de0c1dd0e6284170aef807224d5c8ccb5.tar.zst
feedaka-af87dd1de0c1dd0e6284170aef807224d5c8ccb5.zip
feat(backend): create-user command
-rw-r--r--backend/db/queries/users.sql14
-rw-r--r--backend/db/users.sql.go69
-rw-r--r--backend/go.mod2
-rw-r--r--backend/main.go57
4 files changed, 141 insertions, 1 deletions
diff --git a/backend/db/queries/users.sql b/backend/db/queries/users.sql
new file mode 100644
index 0000000..99515d5
--- /dev/null
+++ b/backend/db/queries/users.sql
@@ -0,0 +1,14 @@
+-- name: CreateUser :one
+INSERT INTO users (username, password_hash)
+VALUES (?, ?)
+RETURNING *;
+
+-- name: GetUserByUsername :one
+SELECT id, username, password_hash, created_at
+FROM users
+WHERE username = ?;
+
+-- name: GetUserByID :one
+SELECT id, username, password_hash, created_at
+FROM users
+WHERE id = ?;
diff --git a/backend/db/users.sql.go b/backend/db/users.sql.go
new file mode 100644
index 0000000..7255eba
--- /dev/null
+++ b/backend/db/users.sql.go
@@ -0,0 +1,69 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+// sqlc v1.29.0
+// source: users.sql
+
+package db
+
+import (
+ "context"
+)
+
+const createUser = `-- name: CreateUser :one
+INSERT INTO users (username, password_hash)
+VALUES (?, ?)
+RETURNING id, username, password_hash, created_at
+`
+
+type CreateUserParams struct {
+ Username string
+ PasswordHash string
+}
+
+func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
+ row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.PasswordHash)
+ var i User
+ err := row.Scan(
+ &i.ID,
+ &i.Username,
+ &i.PasswordHash,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getUserByID = `-- name: GetUserByID :one
+SELECT id, username, password_hash, created_at
+FROM users
+WHERE id = ?
+`
+
+func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
+ row := q.db.QueryRowContext(ctx, getUserByID, id)
+ var i User
+ err := row.Scan(
+ &i.ID,
+ &i.Username,
+ &i.PasswordHash,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getUserByUsername = `-- name: GetUserByUsername :one
+SELECT id, username, password_hash, created_at
+FROM users
+WHERE username = ?
+`
+
+func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
+ row := q.db.QueryRowContext(ctx, getUserByUsername, username)
+ var i User
+ err := row.Scan(
+ &i.ID,
+ &i.Username,
+ &i.PasswordHash,
+ &i.CreatedAt,
+ )
+ return i, err
+}
diff --git a/backend/go.mod b/backend/go.mod
index 78ca5d0..68a6343 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -11,6 +11,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.28
github.com/mmcdole/gofeed v1.3.0
github.com/vektah/gqlparser/v2 v2.5.30
+ golang.org/x/crypto v0.39.0
)
require (
@@ -69,7 +70,6 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
- golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
diff --git a/backend/main.go b/backend/main.go
index 3bbe2f7..117cfb9 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "bufio"
"context"
"database/sql"
"embed"
@@ -10,6 +11,7 @@ import (
"net/http"
"os"
"os/signal"
+ "strings"
"syscall"
"time"
@@ -23,6 +25,7 @@ import (
_ "github.com/mattn/go-sqlite3"
"github.com/mmcdole/gofeed"
"github.com/vektah/gqlparser/v2/ast"
+ "golang.org/x/crypto/bcrypt"
"undef.ninja/x/feedaka/db"
"undef.ninja/x/feedaka/graphql"
@@ -129,6 +132,53 @@ func fetchAllFeeds(ctx context.Context) error {
return result.ErrorOrNil()
}
+func runCreateUser(database *sql.DB) {
+ queries := db.New(database)
+ reader := bufio.NewReader(os.Stdin)
+
+ // Read username
+ fmt.Print("Enter username: ")
+ username, err := reader.ReadString('\n')
+ if err != nil {
+ log.Fatalf("Failed to read username: %v", err)
+ }
+ username = strings.TrimSpace(username)
+ if username == "" {
+ log.Fatal("Username cannot be empty")
+ }
+
+ // Read password
+ fmt.Print("Enter password: ")
+ password, err := reader.ReadString('\n')
+ if err != nil {
+ log.Fatalf("Failed to read password: %v", err)
+ }
+ password = strings.TrimSpace(password)
+
+ // Validate password length
+ if len(password) < 15 {
+ log.Fatalf("Password must be at least 15 characters long (got %d characters)", len(password))
+ }
+
+ // Hash password
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ if err != nil {
+ log.Fatalf("Failed to hash password: %v", err)
+ }
+
+ // Create user
+ ctx := context.Background()
+ user, err := queries.CreateUser(ctx, db.CreateUserParams{
+ Username: username,
+ PasswordHash: string(hashedPassword),
+ })
+ if err != nil {
+ log.Fatalf("Failed to create user: %v", err)
+ }
+
+ log.Printf("User created successfully: ID=%d, Username=%s", user.ID, user.Username)
+}
+
func scheduled(ctx context.Context, d time.Duration, fn func()) {
ticker := time.NewTicker(d)
go func() {
@@ -146,6 +196,7 @@ func scheduled(ctx context.Context, d time.Duration, fn func()) {
func main() {
// Parse command line flags
var migrate = flag.Bool("migrate", false, "Run database migrations")
+ var createUser = flag.Bool("create-user", false, "Create a new user")
flag.Parse()
port := os.Getenv("FEEDAKA_PORT")
@@ -171,6 +222,12 @@ func main() {
return
}
+ // Create user mode
+ if *createUser {
+ runCreateUser(database)
+ return
+ }
+
err = db.ValidateSchemaVersion(database)
if err != nil {
log.Fatal(err)