diff --git a/Taskfile.yaml b/Taskfile.yaml index 4431aca..e6d3f3c 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -1,6 +1,14 @@ version: "3" +vars: + test_db: "~/databases/test.db" + schema_db: "./sqlite/schema.sql" tasks: - sql: + sqlc: cmds: - - sqlc generate \ No newline at end of file + - sqlc generate + + rebuild_test_db: + cmds: + - rm {{.test_db}} + - sqlite3 {{.test_db}} < {{.schema_db}} diff --git a/api/nkode_api.go b/api/nkode_api.go index 28edba3..42d027e 100644 --- a/api/nkode_api.go +++ b/api/nkode_api.go @@ -5,6 +5,7 @@ import ( "git.infra.nkode.tech/dkelly/nkode-core/config" "git.infra.nkode.tech/dkelly/nkode-core/email" "git.infra.nkode.tech/dkelly/nkode-core/entities" + "git.infra.nkode.tech/dkelly/nkode-core/memcache" "git.infra.nkode.tech/dkelly/nkode-core/repository" "git.infra.nkode.tech/dkelly/nkode-core/security" "github.com/google/uuid" @@ -23,7 +24,7 @@ type NKodeAPI struct { repo repository.CustomerUserRepository signupSessionCache *cache.Cache emailQueue *email.Queue - forgotNkodeCache mem_cache.ForgotNKodeCache + forgotNkodeCache memcache.ForgotNKodeCache } func NewNKodeAPI(repo repository.CustomerUserRepository, queue *email.Queue) NKodeAPI { @@ -31,7 +32,7 @@ func NewNKodeAPI(repo repository.CustomerUserRepository, queue *email.Queue) NKo repo: repo, emailQueue: queue, signupSessionCache: cache.New(sessionExpiration, sessionCleanupInterval), - forgotNkodeCache: mem_cache.NewForgotNKodeCache(), + forgotNkodeCache: memcache.NewForgotNKodeCache(), } } diff --git a/mem-cache/forgot_nkode.go b/memcache/forgot_nkode.go similarity index 98% rename from mem-cache/forgot_nkode.go rename to memcache/forgot_nkode.go index fa27924..fa15878 100644 --- a/mem-cache/forgot_nkode.go +++ b/memcache/forgot_nkode.go @@ -1,4 +1,4 @@ -package mem_cache +package memcache import ( "git.infra.nkode.tech/dkelly/nkode-core/entities" diff --git a/sqlc/models.go b/sqlc/models.go index 43575da..fdea8e3 100644 --- a/sqlc/models.go +++ b/sqlc/models.go @@ -6,8 +6,43 @@ package sqlc import ( "database/sql" + "time" ) +type AuthorizationCode struct { + ID int64 + Code string + CodeChallenge string + CodeChallengeMethod string + UserID string + ClientID string + Scope sql.NullString + RedirectUri string + CreatedAt sql.NullTime + ExpiresAt time.Time + UsedAt sql.NullTime +} + +type Client struct { + ID string + Name string + Owner string + CreatedAt sql.NullTime +} + +type ClientApproval struct { + ID int64 + UserID string + ClientID string +} + +type ClientRedirect struct { + ID int64 + Uri string + ClientID string + CreatedAt sql.NullTime +} + type Customer struct { ID string MaxNkodeLen int64 @@ -27,6 +62,17 @@ type SvgIcon struct { Svg string } +type Token struct { + ID int64 + TokenType string + TokenValue string + UserID string + ClientID string + Scope sql.NullString + CreatedAt sql.NullTime + ExpiresAt time.Time +} + type User struct { ID string Email string diff --git a/sqlc/query.sql.go b/sqlc/query.sql.go index 38e63fe..b6aa826 100644 --- a/sqlc/query.sql.go +++ b/sqlc/query.sql.go @@ -8,6 +8,7 @@ package sqlc import ( "context" "database/sql" + "time" ) const addSvg = `-- name: AddSvg :exec @@ -33,6 +34,69 @@ func (q *Queries) AddUserPermission(ctx context.Context, arg AddUserPermissionPa return err } +const approveClient = `-- name: ApproveClient :exec +INSERT INTO client_approvals (user_id, client_id) +VALUES (?, ?) +` + +type ApproveClientParams struct { + UserID string + ClientID string +} + +func (q *Queries) ApproveClient(ctx context.Context, arg ApproveClientParams) error { + _, err := q.db.ExecContext(ctx, approveClient, arg.UserID, arg.ClientID) + return err +} + +const clientApproved = `-- name: ClientApproved :one +SELECT id, user_id, client_id +FROM client_approvals +WHERE user_id = ? AND client_id = ? +` + +type ClientApprovedParams struct { + UserID string + ClientID string +} + +func (q *Queries) ClientApproved(ctx context.Context, arg ClientApprovedParams) (ClientApproval, error) { + row := q.db.QueryRowContext(ctx, clientApproved, arg.UserID, arg.ClientID) + var i ClientApproval + err := row.Scan(&i.ID, &i.UserID, &i.ClientID) + return i, err +} + +const createAuthorizationCode = `-- name: CreateAuthorizationCode :exec +INSERT INTO authorization_codes (code, code_challenge, code_challenge_method, user_id, client_id, scope, redirect_uri, expires_at) +VALUES (?, ?, ?, ?, ?, ?, ?, ?) +` + +type CreateAuthorizationCodeParams struct { + Code string + CodeChallenge string + CodeChallengeMethod string + UserID string + ClientID string + Scope sql.NullString + RedirectUri string + ExpiresAt time.Time +} + +func (q *Queries) CreateAuthorizationCode(ctx context.Context, arg CreateAuthorizationCodeParams) error { + _, err := q.db.ExecContext(ctx, createAuthorizationCode, + arg.Code, + arg.CodeChallenge, + arg.CodeChallengeMethod, + arg.UserID, + arg.ClientID, + arg.Scope, + arg.RedirectUri, + arg.ExpiresAt, + ) + return err +} + const createCustomer = `-- name: CreateCustomer :exec INSERT INTO customer ( id @@ -81,6 +145,63 @@ func (q *Queries) CreateCustomer(ctx context.Context, arg CreateCustomerParams) return err } +const createOIDCClient = `-- name: CreateOIDCClient :exec +INSERT INTO clients (id, name, owner) +VALUES (?, ?, ?) +` + +type CreateOIDCClientParams struct { + ID string + Name string + Owner string +} + +func (q *Queries) CreateOIDCClient(ctx context.Context, arg CreateOIDCClientParams) error { + _, err := q.db.ExecContext(ctx, createOIDCClient, arg.ID, arg.Name, arg.Owner) + return err +} + +const createRedirectURI = `-- name: CreateRedirectURI :exec +INSERT INTO client_redirects (uri, client_id) +VALUES (?, ?) +` + +type CreateRedirectURIParams struct { + Uri string + ClientID string +} + +func (q *Queries) CreateRedirectURI(ctx context.Context, arg CreateRedirectURIParams) error { + _, err := q.db.ExecContext(ctx, createRedirectURI, arg.Uri, arg.ClientID) + return err +} + +const createToken = `-- name: CreateToken :exec +INSERT INTO tokens (token_type, token_value, user_id, client_id, scope, expires_at) +VALUES (?, ?, ?, ?, ?, ?) +` + +type CreateTokenParams struct { + TokenType string + TokenValue string + UserID string + ClientID string + Scope sql.NullString + ExpiresAt time.Time +} + +func (q *Queries) CreateToken(ctx context.Context, arg CreateTokenParams) error { + _, err := q.db.ExecContext(ctx, createToken, + arg.TokenType, + arg.TokenValue, + arg.UserID, + arg.ClientID, + arg.Scope, + arg.ExpiresAt, + ) + return err +} + const createUser = `-- name: CreateUser :exec INSERT INTO user ( id @@ -150,6 +271,110 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error { return err } +const deleteAuthCode = `-- name: DeleteAuthCode :exec +DELETE FROM authorization_codes +WHERE code = ? +` + +func (q *Queries) DeleteAuthCode(ctx context.Context, code string) error { + _, err := q.db.ExecContext(ctx, deleteAuthCode, code) + return err +} + +const deleteOldAuthCodes = `-- name: DeleteOldAuthCodes :exec +DELETE FROM authorization_codes +WHERE expires_at < CURRENT_TIMESTAMP +` + +func (q *Queries) DeleteOldAuthCodes(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, deleteOldAuthCodes) + return err +} + +const deleteOldTokens = `-- name: DeleteOldTokens :exec +DELETE FROM tokens +WHERE expires_at < CURRENT_TIMESTAMP +` + +func (q *Queries) DeleteOldTokens(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, deleteOldTokens) + return err +} + +const deleteRedirectURI = `-- name: DeleteRedirectURI :exec +DELETE FROM client_redirects +WHERE uri = ? AND client_id = ? +` + +type DeleteRedirectURIParams struct { + Uri string + ClientID string +} + +func (q *Queries) DeleteRedirectURI(ctx context.Context, arg DeleteRedirectURIParams) error { + _, err := q.db.ExecContext(ctx, deleteRedirectURI, arg.Uri, arg.ClientID) + return err +} + +const getAuthorizationCode = `-- name: GetAuthorizationCode :one +SELECT id, code, code_challenge, code_challenge_method, user_id, client_id, scope, redirect_uri, created_at, expires_at, used_at +FROM authorization_codes +WHERE code = ? +` + +func (q *Queries) GetAuthorizationCode(ctx context.Context, code string) (AuthorizationCode, error) { + row := q.db.QueryRowContext(ctx, getAuthorizationCode, code) + var i AuthorizationCode + err := row.Scan( + &i.ID, + &i.Code, + &i.CodeChallenge, + &i.CodeChallengeMethod, + &i.UserID, + &i.ClientID, + &i.Scope, + &i.RedirectUri, + &i.CreatedAt, + &i.ExpiresAt, + &i.UsedAt, + ) + return i, err +} + +const getClientRedirectURIs = `-- name: GetClientRedirectURIs :many +SELECT id, uri, client_id, created_at +FROM client_redirects +WHERE client_id = ? +` + +func (q *Queries) GetClientRedirectURIs(ctx context.Context, clientID string) ([]ClientRedirect, error) { + rows, err := q.db.QueryContext(ctx, getClientRedirectURIs, clientID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ClientRedirect + for rows.Next() { + var i ClientRedirect + if err := rows.Scan( + &i.ID, + &i.Uri, + &i.ClientID, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getCustomer = `-- name: GetCustomer :one SELECT max_nkode_len @@ -191,6 +416,24 @@ func (q *Queries) GetCustomer(ctx context.Context, id string) (GetCustomerRow, e return i, err } +const getOIDCClientByID = `-- name: GetOIDCClientByID :one +SELECT id, name, owner, created_at +FROM clients +WHERE id = ? +` + +func (q *Queries) GetOIDCClientByID(ctx context.Context, id string) (Client, error) { + row := q.db.QueryRowContext(ctx, getOIDCClientByID, id) + var i Client + err := row.Scan( + &i.ID, + &i.Name, + &i.Owner, + &i.CreatedAt, + ) + return i, err +} + const getSvgCount = `-- name: GetSvgCount :one SELECT COUNT(*) as count FROM svg_icon ` @@ -215,6 +458,28 @@ func (q *Queries) GetSvgId(ctx context.Context, id int64) (string, error) { return svg, err } +const getTokenByValue = `-- name: GetTokenByValue :one +SELECT id, token_type, token_value, user_id, client_id, scope, created_at, expires_at +FROM tokens +WHERE token_value = ? +` + +func (q *Queries) GetTokenByValue(ctx context.Context, tokenValue string) (Token, error) { + row := q.db.QueryRowContext(ctx, getTokenByValue, tokenValue) + var i Token + err := row.Scan( + &i.ID, + &i.TokenType, + &i.TokenValue, + &i.UserID, + &i.ClientID, + &i.Scope, + &i.CreatedAt, + &i.ExpiresAt, + ) + return i, err +} + const getUser = `-- name: GetUser :one SELECT id @@ -282,6 +547,42 @@ func (q *Queries) GetUser(ctx context.Context, arg GetUserParams) (GetUserRow, e return i, err } +const getUserClients = `-- name: GetUserClients :many + +SELECT id, name, owner, created_at +FROM clients +WHERE owner = ? +` + +// -------- go-oidc +func (q *Queries) GetUserClients(ctx context.Context, owner string) ([]Client, error) { + rows, err := q.db.QueryContext(ctx, getUserClients, owner) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Client + for rows.Next() { + var i Client + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Owner, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getUserPermissions = `-- name: GetUserPermissions :many SELECT permission FROM user_permission WHERE user_id = ? ` diff --git a/sqlite/query.sql b/sqlite/query.sql index 5429241..936ea66 100644 --- a/sqlite/query.sql +++ b/sqlite/query.sql @@ -143,3 +143,72 @@ SELECT permission FROM user_permission WHERE user_id = ?; -- name: AddUserPermission :exec INSERT INTO user_permission (user_id, permission) VALUES (?, ?); + + +---------- go-oidc + +-- name: GetUserClients :many +SELECT * +FROM clients +WHERE owner = ?; + +-- name: GetOIDCClientByID :one +SELECT * +FROM clients +WHERE id = ?; + +-- name: CreateOIDCClient :exec +INSERT INTO clients (id, name, owner) +VALUES (?, ?, ?); + +-- name: CreateRedirectURI :exec +INSERT INTO client_redirects (uri, client_id) +VALUES (?, ?); + +-- name: DeleteRedirectURI :exec +DELETE FROM client_redirects +WHERE uri = ? AND client_id = ?; + +-- name: GetClientRedirectURIs :many +SELECT * +FROM client_redirects +WHERE client_id = ?; + +-- name: GetAuthorizationCode :one +SELECT * +FROM authorization_codes +WHERE code = ?; + +-- name: CreateAuthorizationCode :exec +INSERT INTO authorization_codes (code, code_challenge, code_challenge_method, user_id, client_id, scope, redirect_uri, expires_at) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); + +-- name: DeleteOldAuthCodes :exec +DELETE FROM authorization_codes +WHERE expires_at < CURRENT_TIMESTAMP; + +-- name: DeleteOldTokens :exec +DELETE FROM tokens +WHERE expires_at < CURRENT_TIMESTAMP; + +-- name: GetTokenByValue :one +SELECT * +FROM tokens +WHERE token_value = ?; + +-- name: CreateToken :exec +INSERT INTO tokens (token_type, token_value, user_id, client_id, scope, expires_at) +VALUES (?, ?, ?, ?, ?, ?); + +-- name: ApproveClient :exec +INSERT INTO client_approvals (user_id, client_id) +VALUES (?, ?); + +-- name: ClientApproved :one +SELECT * +FROM client_approvals +WHERE user_id = ? AND client_id = ?; + +-- name: DeleteAuthCode :exec +DELETE FROM authorization_codes +WHERE code = ?; diff --git a/sqlite/schema.sql b/sqlite/schema.sql index 8c7165d..6cc6a92 100644 --- a/sqlite/schema.sql +++ b/sqlite/schema.sql @@ -63,4 +63,63 @@ CREATE TABLE IF NOT EXISTS user_permission ( ,permission TEXT NOT NULL ,FOREIGN KEY (user_id) REFERENCES user(id) ,UNIQUE(user_id, permission) -); \ No newline at end of file +); + + +---- go-oidc + +CREATE TABLE IF NOT EXISTS clients ( + id TEXT PRIMARY KEY + ,name TEXT NOT NULL + ,owner TEXT NOT NULL + ,created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ,FOREIGN KEY (owner) REFERENCES user (id) + ,UNIQUE(name, owner) + ); + +CREATE TABLE IF NOT EXISTS client_redirects ( + id INTEGER PRIMARY KEY AUTOINCREMENT + ,uri TEXT NOT NULL + ,client_id TEXT NOT NULL + ,created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ,FOREIGN KEY (client_id) REFERENCES clients (id) + ,UNIQUE(uri, client_id) + ); + +CREATE TABLE IF NOT EXISTS authorization_codes ( + id INTEGER PRIMARY KEY AUTOINCREMENT + ,code TEXT NOT NULL UNIQUE + ,code_challenge TEXT NOT NULL UNIQUE + ,code_challenge_method TEXT NOT NULL CHECK (code_challenge_method IN ('S256', 'plain')) + ,user_id TEXT NOT NULL + ,client_id TEXT NOT NULL + ,scope TEXT + ,redirect_uri TEXT NOT NULL + ,created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ,expires_at DATETIME NOT NULL + ,used_at DATETIME + ,FOREIGN KEY (user_id) REFERENCES user (id) + ,FOREIGN KEY (client_id) REFERENCES client (id) + ); + +CREATE TABLE IF NOT EXISTS tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT + ,token_type TEXT NOT NULL CHECK (token_type IN ('access', 'refresh')) + ,token_value TEXT NOT NULL UNIQUE + ,user_id TEXT NOT NULL + ,client_id TEXT NOT NULL + ,scope TEXT + ,created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ,expires_at DATETIME NOT NULL + ,FOREIGN KEY (user_id) REFERENCES user (id) + ,FOREIGN KEY (client_id) REFERENCES clients (id) + ); + +CREATE TABLE IF NOT EXISTS client_approvals ( + id INTEGER PRIMARY KEY AUTOINCREMENT + ,user_id TEXT NOT NULL + ,client_id TEXT NOT NULL + ,UNIQUE(user_id, client_id) + ,FOREIGN KEY (user_id) REFERENCES users (id) + ,FOREIGN KEY (client_id) REFERENCES clients (id) +);