add working signup + login
This commit is contained in:
parent
bc85b7c340
commit
d8cb8c8fa8
27 changed files with 600 additions and 39 deletions
|
@ -3,8 +3,12 @@ package database
|
|||
import "github.com/oklog/ulid/v2"
|
||||
|
||||
type Config struct {
|
||||
Name string
|
||||
AdminID *ulid.ULID
|
||||
Name string
|
||||
AdminID *ulid.ULID
|
||||
InternalApplication *ulid.ULID
|
||||
|
||||
// ID is always 1
|
||||
ID int
|
||||
}
|
||||
|
||||
var DefaultConfig = Config{
|
||||
|
|
|
@ -38,7 +38,31 @@ create table posts (
|
|||
visibility post_visibility not null
|
||||
);
|
||||
|
||||
create table applications (
|
||||
id text primary key,
|
||||
name text not null
|
||||
);
|
||||
|
||||
create table tokens (
|
||||
id text primary key,
|
||||
app_id text not null references applications (id) on delete cascade,
|
||||
user_id text not null references accounts (id) on delete cascade,
|
||||
scopes text[] not null default array[]::text[],
|
||||
expires timestamptz not null
|
||||
);
|
||||
|
||||
create table config (
|
||||
id int primary key not null default 1,
|
||||
name text not null, -- instance name
|
||||
admin_id text references blogs (id) on delete set null, -- admin contact
|
||||
internal_application text references applications (id) on delete set null,
|
||||
constraint singleton check (id = 1) -- only one config table entry
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
drop table config;
|
||||
drop table tokens;
|
||||
drop table applications;
|
||||
drop table accounts;
|
||||
drop table blogs;
|
||||
drop table posts;
|
||||
|
|
|
@ -37,8 +37,13 @@ func (s *AccountStore) ByID(ctx context.Context, id ulid.ULID) (a database.Accou
|
|||
}
|
||||
|
||||
// ByUsername gets an account by its username.
|
||||
func (s *AccountStore) ByUsername(ctx context.Context, username string, host *string) (a database.Account, err error) {
|
||||
q := sqlf.Sprintf("SELECT * FROM accounts WHERE username = %s AND host = %v", username, host)
|
||||
func (s *AccountStore) ByUsername(ctx context.Context, username, domain string) (a database.Account, err error) {
|
||||
q := sqlf.Sprintf("SELECT * FROM accounts WHERE username = %s", username)
|
||||
if domain == "" {
|
||||
q = sqlf.Sprintf("%v AND domain IS NULL", q)
|
||||
} else {
|
||||
q = sqlf.Sprintf("%v AND domain = %s", q, domain)
|
||||
}
|
||||
|
||||
a, err = Get[database.Account](ctx, s.q, q)
|
||||
if err != nil {
|
||||
|
@ -64,7 +69,7 @@ func (s *AccountStore) CreateLocal(
|
|||
}
|
||||
|
||||
q := sqlf.Sprintf(
|
||||
"INSERT INTO accounts (id, username, host, email, password) VALUES (%s, %v, NULL, %v, %v) RETURNING *",
|
||||
"INSERT INTO accounts (id, username, domain, email, password) VALUES (%s, %v, NULL, %v, %v) RETURNING *",
|
||||
makeULID(), username, email, hash)
|
||||
|
||||
a, err = Get[database.Account](ctx, s.q, q)
|
||||
|
|
51
internal/database/sql/config.go
Normal file
51
internal/database/sql/config.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"git.sleepycat.moe/sam/mercury/internal/database"
|
||||
"github.com/keegancsmith/sqlf"
|
||||
)
|
||||
|
||||
// ConfigStore is the interface to configs in the database.
|
||||
type ConfigStore struct {
|
||||
q Querier
|
||||
}
|
||||
|
||||
// NewConfigStore creates a new ConfigStore instance.
|
||||
func NewConfigStore(q Querier) *ConfigStore {
|
||||
return &ConfigStore{q: q}
|
||||
}
|
||||
|
||||
func (s *ConfigStore) initConfig(ctx context.Context) error {
|
||||
q := sqlf.Sprintf(`INSERT INTO config
|
||||
(id, name)
|
||||
VALUES (1, %s)
|
||||
ON CONFLICT (id) DO NOTHING`, database.DefaultConfig.Name)
|
||||
|
||||
err := Exec(ctx, s.q, q)
|
||||
return errors.Wrap(err, "executing query")
|
||||
}
|
||||
|
||||
func (s *ConfigStore) Get(ctx context.Context) (database.Config, error) {
|
||||
q := sqlf.Sprintf("SELECT * FROM config WHERE id = 1")
|
||||
|
||||
return Get[database.Config](ctx, s.q, q)
|
||||
}
|
||||
|
||||
func (s *ConfigStore) Set(ctx context.Context, cur, new database.Config) (database.Config, error) {
|
||||
q := sqlf.Sprintf("UPDATE config SET")
|
||||
if cur.Name != new.Name {
|
||||
q = sqlf.Sprintf("%v name = %v,", q, new.Name)
|
||||
}
|
||||
if cur.AdminID != new.AdminID {
|
||||
q = sqlf.Sprintf("%v admin_id = %v,", q, new.AdminID)
|
||||
}
|
||||
if cur.InternalApplication != new.InternalApplication {
|
||||
q = sqlf.Sprintf("%v internal_application = %v,", q, new.InternalApplication)
|
||||
}
|
||||
q = sqlf.Sprintf("%v id = %v WHERE id = %v RETURNING *", q, cur.ID, cur.ID)
|
||||
|
||||
return Get[database.Config](ctx, s.q, q)
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"git.sleepycat.moe/sam/mercury/internal/database"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
@ -24,9 +25,55 @@ func NewBase(ctx context.Context, connString string) (*Base, error) {
|
|||
base := &Base{
|
||||
pool: pool,
|
||||
}
|
||||
|
||||
// create configuration
|
||||
if err := base.initSingletons(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "initializing configuration")
|
||||
}
|
||||
|
||||
return base, nil
|
||||
}
|
||||
|
||||
func (base *Base) initSingletons(ctx context.Context) error {
|
||||
err := NewConfigStore(base.pool).initConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "initializing configuration")
|
||||
}
|
||||
|
||||
cfg, err := NewConfigStore(base.pool).Get(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting configuration")
|
||||
}
|
||||
|
||||
if cfg.InternalApplication == nil {
|
||||
tx, err := base.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
app, err := NewTokenStore(tx).CreateApplication(ctx, database.InternalApplicationName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating internal application")
|
||||
}
|
||||
|
||||
newCfg := cfg
|
||||
newCfg.InternalApplication = &app.ID
|
||||
|
||||
cfg, err = NewConfigStore(tx).Set(ctx, cfg, newCfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "updating configuration")
|
||||
}
|
||||
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "committing transaction")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Acquire acquires a connection from the database pool.
|
||||
// It is the caller's responsibility to call the Release method.
|
||||
func (base *Base) Acquire(ctx context.Context) (ReleaseableQuerier, error) {
|
||||
|
|
|
@ -48,3 +48,8 @@ func Get[T any](ctx context.Context, querier Querier, query *sqlf.Query) (T, err
|
|||
}
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
func Exec(ctx context.Context, querier Querier, query *sqlf.Query) error {
|
||||
_, err := querier.Exec(ctx, query.Query(sqlf.PostgresBindVar), query.Args()...)
|
||||
return err
|
||||
}
|
||||
|
|
52
internal/database/sql/token.go
Normal file
52
internal/database/sql/token.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"git.sleepycat.moe/sam/mercury/internal/database"
|
||||
"github.com/keegancsmith/sqlf"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
// TokenStore is the interface to tokens in the database.
|
||||
type TokenStore struct {
|
||||
q Querier
|
||||
}
|
||||
|
||||
// NewTokenStore creates a new TokenStore instance.
|
||||
func NewTokenStore(q Querier) *TokenStore {
|
||||
return &TokenStore{q: q}
|
||||
}
|
||||
|
||||
func (s *TokenStore) Get(ctx context.Context, id ulid.ULID) (database.Token, error) {
|
||||
q := sqlf.Sprintf("SELECT * FROM tokens WHERE id = %s", id)
|
||||
|
||||
t, err := Get[database.Token](ctx, s.q, q)
|
||||
return t, errors.Wrap(err, "executing query")
|
||||
}
|
||||
|
||||
func (s *TokenStore) GetApplication(ctx context.Context, id ulid.ULID) (database.Application, error) {
|
||||
q := sqlf.Sprintf("SELECT * FROM applications WHERE id = %s", id)
|
||||
|
||||
app, err := Get[database.Application](ctx, s.q, q)
|
||||
return app, errors.Wrap(err, "executing query")
|
||||
}
|
||||
|
||||
func (s *TokenStore) Create(ctx context.Context, userID, appID ulid.ULID, scopes []string, expires time.Time) (database.Token, error) {
|
||||
q := sqlf.Sprintf(`INSERT INTO tokens
|
||||
(id, user_id, app_id, scopes, expires)
|
||||
values (%s, %s, %s, %v, %v)
|
||||
RETURNING *`, makeULID(), userID, appID, scopes, expires)
|
||||
|
||||
t, err := Get[database.Token](ctx, s.q, q)
|
||||
return t, errors.Wrap(err, "executing query")
|
||||
}
|
||||
|
||||
func (s *TokenStore) CreateApplication(ctx context.Context, name string) (database.Application, error) {
|
||||
q := sqlf.Sprintf("INSERT INTO applications (id, name) VALUES (%s, %s) RETURNING *", makeULID(), name)
|
||||
|
||||
app, err := Get[database.Application](ctx, s.q, q)
|
||||
return app, errors.Wrap(err, "executing query")
|
||||
}
|
45
internal/database/token.go
Normal file
45
internal/database/token.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
const InternalApplicationName = "mercury_internal"
|
||||
const TokenCookieName = "mercury_token"
|
||||
|
||||
type Application struct {
|
||||
ID ulid.ULID
|
||||
Name string
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
ID ulid.ULID
|
||||
AppID ulid.ULID
|
||||
UserID ulid.ULID
|
||||
Scopes []string
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
func (t Token) ToClaims() TokenClaims {
|
||||
createdAt := ulid.Time(t.ID.Time())
|
||||
|
||||
return TokenClaims{
|
||||
TokenID: t.ID,
|
||||
UserID: t.UserID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "mercury",
|
||||
ExpiresAt: jwt.NewNumericDate(t.Expires),
|
||||
IssuedAt: jwt.NewNumericDate(createdAt),
|
||||
NotBefore: jwt.NewNumericDate(createdAt),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type TokenClaims struct {
|
||||
TokenID ulid.ULID `json:"jti"`
|
||||
UserID ulid.ULID `json:"sub"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue