package db import "database/sql" import "errors" import "strings" import "time" import "codit/internal/models" import "codit/internal/util" func (s *Store) CreateUser(user models.User, passwordHash string) (models.User, error) { var id string var err error var now time.Time var nowUnix int64 if user.ID == "" { id, err = util.NewID() if err != nil { return user, err } user.ID = id } now = time.Now().UTC() nowUnix = now.Unix() user.CreatedAt = nowUnix user.UpdatedAt = nowUnix if user.AuthSource == "" { user.AuthSource = "db" } _, err = s.DB.Exec(` INSERT INTO users (public_id, username, display_name, email, password_hash, is_admin, disabled, auth_source, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, user.ID, user.Username, user.DisplayName, user.Email, passwordHash, user.IsAdmin, user.Disabled, user.AuthSource, now, now) return user, err } func (s *Store) UpdateUser(user models.User) error { var err error var now time.Time var nowUnix int64 now = time.Now().UTC() nowUnix = now.Unix() user.UpdatedAt = nowUnix _, err = s.DB.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, updated_at = ? WHERE public_id = ?`, user.DisplayName, user.Email, user.IsAdmin, user.Disabled, now, user.ID) return err } func (s *Store) UpdateUserWithPassword(user models.User, passwordHash string) error { var err error var now time.Time var nowUnix int64 var tx *sql.Tx now = time.Now().UTC() nowUnix = now.Unix() user.UpdatedAt = nowUnix tx, err = s.DB.Begin() if err != nil { return err } _, err = tx.Exec(`UPDATE users SET display_name = ?, email = ?, is_admin = ?, disabled = ?, password_hash = ?, updated_at = ? WHERE public_id = ?`, user.DisplayName, user.Email, user.IsAdmin, user.Disabled, passwordHash, now, user.ID) if err != nil { _ = tx.Rollback() return err } err = tx.Commit() if err != nil { return err } return nil } func (s *Store) SetUserPassword(userID, passwordHash string) error { var err error _, err = s.DB.Exec(`UPDATE users SET password_hash = ?, updated_at = ? WHERE public_id = ?`, passwordHash, time.Now().UTC(), userID) return err } func (s *Store) GetUserByID(id string) (models.User, error) { var user models.User var row *sql.Row var created time.Time var updated time.Time var err error row = s.DB.QueryRow(`SELECT public_id, username, display_name, email, is_admin, disabled, auth_source, created_at, updated_at FROM users WHERE public_id = ?`, id) err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &created, &updated) if err != nil { return user, err } user.CreatedAt = created.Unix() user.UpdatedAt = updated.Unix() return user, nil } func (s *Store) GetUserByUsername(username string) (models.User, string, error) { var user models.User var passwordHash sql.NullString var row *sql.Row var err error var created time.Time var updated time.Time row = s.DB.QueryRow(`SELECT public_id, username, display_name, email, is_admin, disabled, auth_source, password_hash, created_at, updated_at FROM users WHERE username = ?`, username) err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &passwordHash, &created, &updated) if err != nil { return user, passwordHash.String, err } user.CreatedAt = created.Unix() user.UpdatedAt = updated.Unix() return user, passwordHash.String, nil } func (s *Store) ListUsers() ([]models.User, error) { var rows *sql.Rows var err error var users []models.User var u models.User var created time.Time var updated time.Time rows, err = s.DB.Query(`SELECT public_id, username, display_name, email, is_admin, disabled, auth_source, created_at, updated_at FROM users ORDER BY username`) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Email, &u.IsAdmin, &u.Disabled, &u.AuthSource, &created, &updated) if err != nil { return nil, err } u.CreatedAt = created.Unix() u.UpdatedAt = updated.Unix() users = append(users, u) } return users, rows.Err() } func (s *Store) DeleteUser(id string) error { var err error _, err = s.DB.Exec(`DELETE FROM users WHERE public_id = ?`, id) return err } func (s *Store) GetAuthSettings() (models.AuthSettings, error) { var settings models.AuthSettings var rows *sql.Rows var err error var key string var value string rows, err = s.DB.Query(`SELECT key, value FROM app_settings WHERE key IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, "auth.mode", "auth.oidc.enabled", "auth.ldap.url", "auth.ldap.bind_dn", "auth.ldap.bind_password", "auth.ldap.user_base_dn", "auth.ldap.user_filter", "auth.ldap.tls_insecure_skip_verify", "auth.oidc.client_id", "auth.oidc.client_secret", "auth.oidc.authorize_url", "auth.oidc.token_url", "auth.oidc.userinfo_url", "auth.oidc.redirect_url", "auth.oidc.scopes", "auth.oidc.tls_insecure_skip_verify") if err != nil { return settings, err } defer rows.Close() for rows.Next() { err = rows.Scan(&key, &value) if err != nil { return settings, err } switch key { case "auth.mode": settings.AuthMode = value case "auth.oidc.enabled": settings.OIDCEnabled = value == "1" case "auth.ldap.url": settings.LDAPURL = value case "auth.ldap.bind_dn": settings.LDAPBindDN = value case "auth.ldap.bind_password": settings.LDAPBindPassword = value case "auth.ldap.user_base_dn": settings.LDAPUserBaseDN = value case "auth.ldap.user_filter": settings.LDAPUserFilter = value case "auth.ldap.tls_insecure_skip_verify": settings.LDAPTLSInsecureSkipVerify = value == "1" case "auth.oidc.client_id": settings.OIDCClientID = value case "auth.oidc.client_secret": settings.OIDCClientSecret = value case "auth.oidc.authorize_url": settings.OIDCAuthorizeURL = value case "auth.oidc.token_url": settings.OIDCTokenURL = value case "auth.oidc.userinfo_url": settings.OIDCUserInfoURL = value case "auth.oidc.redirect_url": settings.OIDCRedirectURL = value case "auth.oidc.scopes": settings.OIDCScopes = value case "auth.oidc.tls_insecure_skip_verify": settings.OIDCTLSInsecureSkipVerify = value == "1" } } err = rows.Err() if err != nil { return settings, err } return settings, nil } func (s *Store) SetAuthSettings(settings models.AuthSettings) error { var tx *sql.Tx var err error var now int64 var tlsInsecure string tx, err = s.DB.Begin() if err != nil { return err } now = time.Now().UTC().Unix() _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.mode", settings.AuthMode, now) if err != nil { _ = tx.Rollback() return err } if settings.OIDCEnabled { tlsInsecure = "1" } else { tlsInsecure = "0" } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.enabled", tlsInsecure, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.ldap.url", settings.LDAPURL, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.ldap.bind_dn", settings.LDAPBindDN, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.ldap.bind_password", settings.LDAPBindPassword, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.ldap.user_base_dn", settings.LDAPUserBaseDN, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.ldap.user_filter", settings.LDAPUserFilter, now) if err != nil { _ = tx.Rollback() return err } if settings.LDAPTLSInsecureSkipVerify { tlsInsecure = "1" } else { tlsInsecure = "0" } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.ldap.tls_insecure_skip_verify", tlsInsecure, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.client_id", settings.OIDCClientID, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.client_secret", settings.OIDCClientSecret, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.authorize_url", settings.OIDCAuthorizeURL, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.token_url", settings.OIDCTokenURL, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.userinfo_url", settings.OIDCUserInfoURL, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.redirect_url", settings.OIDCRedirectURL, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.scopes", settings.OIDCScopes, now) if err != nil { _ = tx.Rollback() return err } if settings.OIDCTLSInsecureSkipVerify { tlsInsecure = "1" } else { tlsInsecure = "0" } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "auth.oidc.tls_insecure_skip_verify", tlsInsecure, now) if err != nil { _ = tx.Rollback() return err } err = tx.Commit() if err != nil { return err } return nil } func (s *Store) GetTLSSettings() (models.TLSSettings, error) { var settings models.TLSSettings var rows *sql.Rows var err error var key string var value string rows, err = s.DB.Query(`SELECT key, value FROM app_settings WHERE key IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, "tls.http_addrs", "tls.https_addrs", "tls.server_cert_source", "tls.cert_file", "tls.key_file", "tls.pki_server_cert_id", "tls.client_auth", "tls.client_ca_file", "tls.pki_client_ca_id", "tls.min_version") if err != nil { return settings, err } defer rows.Close() for rows.Next() { err = rows.Scan(&key, &value) if err != nil { return settings, err } switch key { case "tls.http_addrs": settings.HTTPAddrs = splitCSVValue(value) case "tls.https_addrs": settings.HTTPSAddrs = splitCSVValue(value) case "tls.server_cert_source": settings.TLSServerCertSource = value case "tls.cert_file": settings.TLSCertFile = value case "tls.key_file": settings.TLSKeyFile = value case "tls.pki_server_cert_id": settings.TLSPKIServerCertID = value case "tls.client_auth": settings.TLSClientAuth = value case "tls.client_ca_file": settings.TLSClientCAFile = value case "tls.pki_client_ca_id": settings.TLSPKIClientCAID = value case "tls.min_version": settings.TLSMinVersion = value } } err = rows.Err() if err != nil { return settings, err } return settings, nil } func (s *Store) SetTLSSettings(settings models.TLSSettings) error { var tx *sql.Tx var err error var now int64 tx, err = s.DB.Begin() if err != nil { return err } now = time.Now().UTC().Unix() _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.http_addrs", strings.Join(settings.HTTPAddrs, ","), now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.https_addrs", strings.Join(settings.HTTPSAddrs, ","), now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.server_cert_source", settings.TLSServerCertSource, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.cert_file", settings.TLSCertFile, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.key_file", settings.TLSKeyFile, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.pki_server_cert_id", settings.TLSPKIServerCertID, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.client_auth", settings.TLSClientAuth, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.client_ca_file", settings.TLSClientCAFile, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.pki_client_ca_id", settings.TLSPKIClientCAID, now) if err != nil { _ = tx.Rollback() return err } _, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, "tls.min_version", settings.TLSMinVersion, now) if err != nil { _ = tx.Rollback() return err } err = tx.Commit() if err != nil { return err } return nil } func splitCSVValue(value string) []string { var parts []string var out []string var i int var p string parts = strings.Split(value, ",") for i = 0; i < len(parts); i++ { p = strings.TrimSpace(parts[i]) if p == "" { continue } out = append(out, p) } return out } func (s *Store) SetUserDisabled(id string, disabled bool) error { var err error var now time.Time now = time.Now().UTC() _, err = s.DB.Exec(`UPDATE users SET disabled = ?, updated_at = ? WHERE public_id = ?`, disabled, now, id) return err } func (s *Store) CreateAPIKey(userID string, name string, tokenHash string, prefix string, expiresAt int64) (models.APIKey, error) { var key models.APIKey var err error var now time.Time var nowUnix int64 var id string id, err = util.NewID() if err != nil { return key, err } now = time.Now().UTC() nowUnix = now.Unix() key = models.APIKey{ ID: id, UserID: userID, Name: name, Prefix: prefix, CreatedAt: nowUnix, LastUsedAt: 0, ExpiresAt: expiresAt, Disabled: false, } _, err = s.DB.Exec(`INSERT INTO api_keys (public_id, user_id, name, token_hash, token_prefix, created_at, last_used_at, expires_at, disabled) VALUES (?, (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?, ?, ?, ?)`, key.ID, key.UserID, key.Name, tokenHash, key.Prefix, key.CreatedAt, key.LastUsedAt, key.ExpiresAt, key.Disabled) return key, err } func (s *Store) ListAPIKeys(userID string) ([]models.APIKey, error) { var rows *sql.Rows var err error var keys []models.APIKey var key models.APIKey rows, err = s.DB.Query(`SELECT k.public_id, u.public_id, k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled FROM api_keys k JOIN users u ON u.id = k.user_id WHERE u.public_id = ? ORDER BY k.created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&key.ID, &key.UserID, &key.Name, &key.Prefix, &key.CreatedAt, &key.LastUsedAt, &key.ExpiresAt, &key.Disabled) if err != nil { return nil, err } keys = append(keys, key) } return keys, rows.Err() } func (s *Store) DeleteAPIKey(userID string, id string) error { var err error _, err = s.DB.Exec(`DELETE FROM api_keys WHERE public_id = ? AND user_id = (SELECT id FROM users WHERE public_id = ?)`, id, userID) return err } func (s *Store) SetAPIKeyDisabled(userID string, id string, disabled bool) error { var err error _, err = s.DB.Exec(`UPDATE api_keys SET disabled = ? WHERE public_id = ? AND user_id = (SELECT id FROM users WHERE public_id = ?)`, disabled, id, userID) return err } func (s *Store) DeleteAPIKeyByID(id string) error { var err error _, err = s.DB.Exec(`DELETE FROM api_keys WHERE public_id = ?`, id) return err } func (s *Store) SetAPIKeyDisabledByID(id string, disabled bool) error { var err error _, err = s.DB.Exec(`UPDATE api_keys SET disabled = ? WHERE public_id = ?`, disabled, id) return err } func (s *Store) ListAPIKeysAdmin(userID string, query string) ([]models.AdminAPIKey, error) { var rows *sql.Rows var err error var keys []models.AdminAPIKey var key models.AdminAPIKey var sqlQuery string var like string query = strings.TrimSpace(query) like = "%" + query + "%" if userID != "" && query != "" { sqlQuery = ` SELECT k.public_id, u.public_id, u.username, u.display_name, u.email, k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled FROM api_keys k JOIN users u ON u.id = k.user_id WHERE u.public_id = ? AND (k.name LIKE ? OR k.token_prefix LIKE ? OR u.username LIKE ? OR u.display_name LIKE ? OR u.email LIKE ?) ORDER BY k.created_at DESC ` rows, err = s.DB.Query(sqlQuery, userID, like, like, like, like, like) } else if userID != "" { sqlQuery = ` SELECT k.public_id, u.public_id, u.username, u.display_name, u.email, k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled FROM api_keys k JOIN users u ON u.id = k.user_id WHERE u.public_id = ? ORDER BY k.created_at DESC ` rows, err = s.DB.Query(sqlQuery, userID) } else if query != "" { sqlQuery = ` SELECT k.public_id, u.public_id, u.username, u.display_name, u.email, k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled FROM api_keys k JOIN users u ON u.id = k.user_id WHERE k.name LIKE ? OR k.token_prefix LIKE ? OR u.username LIKE ? OR u.display_name LIKE ? OR u.email LIKE ? ORDER BY k.created_at DESC ` rows, err = s.DB.Query(sqlQuery, like, like, like, like, like) } else { sqlQuery = ` SELECT k.public_id, u.public_id, u.username, u.display_name, u.email, k.name, k.token_prefix, k.created_at, k.last_used_at, k.expires_at, k.disabled FROM api_keys k JOIN users u ON u.id = k.user_id ORDER BY k.created_at DESC ` rows, err = s.DB.Query(sqlQuery) } if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan( &key.ID, &key.UserID, &key.Username, &key.DisplayName, &key.Email, &key.Name, &key.Prefix, &key.CreatedAt, &key.LastUsedAt, &key.ExpiresAt, &key.Disabled, ) if err != nil { return nil, err } keys = append(keys, key) } return keys, rows.Err() } func (s *Store) GetUserByAPIKeyHash(tokenHash string) (models.User, error) { var user models.User var row *sql.Row var created time.Time var updated time.Time var keyID int64 var now time.Time var nowUnix int64 var err error var currentUnix int64 now = time.Now().UTC() currentUnix = now.Unix() row = s.DB.QueryRow(` SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, u.created_at, u.updated_at, k.id FROM api_keys k JOIN users u ON u.id = k.user_id WHERE k.token_hash = ? AND u.disabled = 0 AND k.disabled = 0 AND (k.expires_at = 0 OR k.expires_at > ?) LIMIT 1 `, tokenHash, currentUnix) err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &created, &updated, &keyID) if err != nil { return user, err } user.CreatedAt = created.Unix() user.UpdatedAt = updated.Unix() now = time.Now().UTC() nowUnix = now.Unix() _, _ = s.DB.Exec(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`, nowUnix, keyID) return user, nil } func (s *Store) CreateSession(userID, token string, expiresAt time.Time) error { var err error _, err = s.DB.Exec(`INSERT INTO sessions (id, user_id, token, expires_at, created_at) VALUES (?, (SELECT id FROM users WHERE public_id = ?), ?, ?, ?)`, mustID(), userID, token, expiresAt, time.Now().UTC()) return err } func (s *Store) DeleteSession(token string) error { var err error _, err = s.DB.Exec(`DELETE FROM sessions WHERE token = ?`, token) return err } func (s *Store) GetSessionUser(token string) (models.User, time.Time, error) { var user models.User var expires time.Time var row *sql.Row var err error var created time.Time var updated time.Time row = s.DB.QueryRow(` SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, u.created_at, u.updated_at, s.expires_at FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND u.disabled = 0 `, token) err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &created, &updated, &expires) if err != nil { return user, time.Time{}, err } user.CreatedAt = created.Unix() user.UpdatedAt = updated.Unix() return user, expires, nil } func (s *Store) CreateProject(project models.Project) (models.Project, error) { var id string var err error var now time.Time var nowUnix int64 var tx *sql.Tx if project.ID == "" { id, err = util.NewID() if err != nil { return project, err } project.ID = id } now = time.Now().UTC() nowUnix = now.Unix() project.CreatedAt = nowUnix project.UpdatedAt = nowUnix if project.UpdatedBy == "" { project.UpdatedBy = project.CreatedBy } tx, err = s.DB.Begin() if err != nil { return project, err } if project.HomePage == "" { project.HomePage = "info" } _, err = tx.Exec(`INSERT INTO projects (public_id, slug, name, description, home_page, created_by, updated_by, created_at, updated_at, created_at_unix, updated_at_unix) VALUES (?, ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?)`, project.ID, project.Slug, project.Name, project.Description, project.HomePage, project.CreatedBy, project.UpdatedBy, now, now, project.CreatedAt, project.UpdatedAt, ) if err != nil { _ = tx.Rollback() return project, err } _, err = tx.Exec(`INSERT INTO project_members (project_id, user_id, role, created_at) VALUES ((SELECT id FROM projects WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?)`, project.ID, project.CreatedBy, "admin", now) if err != nil { _ = tx.Rollback() return project, err } err = tx.Commit() if err != nil { _ = tx.Rollback() return project, err } return project, nil } func (s *Store) UpdateProject(project models.Project) error { var err error if project.HomePage == "" { project.HomePage = "info" } _, err = s.DB.Exec(`UPDATE projects SET slug = ?, name = ?, description = ?, home_page = ?, updated_at = ?, updated_by = (SELECT id FROM users WHERE public_id = ?), updated_at_unix = ? WHERE public_id = ?`, project.Slug, project.Name, project.Description, project.HomePage, time.Unix(project.UpdatedAt, 0).UTC(), project.UpdatedBy, project.UpdatedAt, project.ID, ) return err } func (s *Store) GetProject(id string) (models.Project, error) { var project models.Project var row *sql.Row row = s.DB.QueryRow(` SELECT p.public_id, p.slug, p.name, p.description, p.home_page, c.public_id, u.public_id, COALESCE(c.username, ''), COALESCE(u.username, ''), p.created_at_unix, p.updated_at_unix FROM projects p LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users u ON u.id = p.updated_by WHERE p.public_id = ? `, id) return project, row.Scan( &project.ID, &project.Slug, &project.Name, &project.Description, &project.HomePage, &project.CreatedBy, &project.UpdatedBy, &project.CreatedByName, &project.UpdatedByName, &project.CreatedAt, &project.UpdatedAt, ) } func (s *Store) GetProjectBySlug(slug string) (models.Project, error) { var project models.Project var row *sql.Row var err error row = s.DB.QueryRow(` SELECT p.public_id, p.slug, p.name, p.description, p.home_page, c.public_id, u.public_id, COALESCE(c.username, ''), COALESCE(u.username, ''), p.created_at_unix, p.updated_at_unix FROM projects p LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users u ON u.id = p.updated_by WHERE p.slug = ? `, slug) err = row.Scan( &project.ID, &project.Slug, &project.Name, &project.Description, &project.HomePage, &project.CreatedBy, &project.UpdatedBy, &project.CreatedByName, &project.UpdatedByName, &project.CreatedAt, &project.UpdatedAt, ) if err != nil { return project, err } return project, nil } func (s *Store) ListProjects() ([]models.Project, error) { var rows *sql.Rows var err error var projects []models.Project var p models.Project rows, err = s.DB.Query(` SELECT p.public_id, p.slug, p.name, p.description, p.home_page, c.public_id, u.public_id, COALESCE(c.username, ''), COALESCE(u.username, ''), p.created_at_unix, p.updated_at_unix FROM projects p LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users u ON u.id = p.updated_by ORDER BY p.name `) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan( &p.ID, &p.Slug, &p.Name, &p.Description, &p.HomePage, &p.CreatedBy, &p.UpdatedBy, &p.CreatedByName, &p.UpdatedByName, &p.CreatedAt, &p.UpdatedAt, ) if err != nil { return nil, err } projects = append(projects, p) } return projects, rows.Err() } func (s *Store) ListProjectsForUser(userID string) ([]models.Project, error) { var rows *sql.Rows var err error var projects []models.Project var p models.Project rows, err = s.DB.Query(` SELECT p.public_id, p.slug, p.name, p.description, p.home_page, c.public_id, u.public_id, COALESCE(c.username, ''), COALESCE(u.username, ''), p.created_at_unix, p.updated_at_unix FROM projects p JOIN project_members m ON m.project_id = p.id LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users u ON u.id = p.updated_by WHERE m.user_id = (SELECT id FROM users WHERE public_id = ?) ORDER BY p.name `, userID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan( &p.ID, &p.Slug, &p.Name, &p.Description, &p.HomePage, &p.CreatedBy, &p.UpdatedBy, &p.CreatedByName, &p.UpdatedByName, &p.CreatedAt, &p.UpdatedAt, ) if err != nil { return nil, err } projects = append(projects, p) } return projects, rows.Err() } func (s *Store) ListProjectsFiltered(limit int, offset int, query string) ([]models.Project, error) { var rows *sql.Rows var err error var projects []models.Project var p models.Project if limit <= 0 { limit = 100 } if offset < 0 { offset = 0 } if query == "" { rows, err = s.DB.Query( `SELECT p.public_id, p.slug, p.name, p.description, p.home_page, c.public_id, u.public_id, COALESCE(c.username, ''), COALESCE(u.username, ''), p.created_at_unix, p.updated_at_unix FROM projects p LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users u ON u.id = p.updated_by ORDER BY p.name LIMIT ? OFFSET ?`, limit, offset, ) } else { rows, err = s.DB.Query( `SELECT p.public_id, p.slug, p.name, p.description, p.home_page, c.public_id, u.public_id, COALESCE(c.username, ''), COALESCE(u.username, ''), p.created_at_unix, p.updated_at_unix FROM projects p LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users u ON u.id = p.updated_by WHERE p.name LIKE ? OR p.slug LIKE ? ORDER BY p.name LIMIT ? OFFSET ?`, "%"+query+"%", "%"+query+"%", limit, offset, ) } if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan( &p.ID, &p.Slug, &p.Name, &p.Description, &p.HomePage, &p.CreatedBy, &p.UpdatedBy, &p.CreatedByName, &p.UpdatedByName, &p.CreatedAt, &p.UpdatedAt, ) if err != nil { return nil, err } projects = append(projects, p) } return projects, rows.Err() } func (s *Store) ListProjectsFilteredForUser(userID string, limit int, offset int, query string) ([]models.Project, error) { var rows *sql.Rows var err error var projects []models.Project var p models.Project if limit <= 0 { limit = 100 } if offset < 0 { offset = 0 } if query == "" { rows, err = s.DB.Query( `SELECT p.public_id, p.slug, p.name, p.description, p.home_page, c.public_id, u.public_id, COALESCE(c.username, ''), COALESCE(u.username, ''), p.created_at_unix, p.updated_at_unix FROM projects p JOIN project_members m ON m.project_id = p.id LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users u ON u.id = p.updated_by WHERE m.user_id = (SELECT id FROM users WHERE public_id = ?) ORDER BY p.name LIMIT ? OFFSET ?`, userID, limit, offset, ) } else { rows, err = s.DB.Query( `SELECT p.public_id, p.slug, p.name, p.description, p.home_page, c.public_id, u.public_id, COALESCE(c.username, ''), COALESCE(u.username, ''), p.created_at_unix, p.updated_at_unix FROM projects p JOIN project_members m ON m.project_id = p.id LEFT JOIN users c ON c.id = p.created_by LEFT JOIN users u ON u.id = p.updated_by WHERE m.user_id = (SELECT id FROM users WHERE public_id = ?) AND (p.name LIKE ? OR p.slug LIKE ?) ORDER BY p.name LIMIT ? OFFSET ?`, userID, "%"+query+"%", "%"+query+"%", limit, offset, ) } if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan( &p.ID, &p.Slug, &p.Name, &p.Description, &p.HomePage, &p.CreatedBy, &p.UpdatedBy, &p.CreatedByName, &p.UpdatedByName, &p.CreatedAt, &p.UpdatedAt, ) if err != nil { return nil, err } projects = append(projects, p) } return projects, rows.Err() } func (s *Store) DeleteProject(id string) error { var err error _, err = s.DB.Exec(`DELETE FROM projects WHERE public_id = ?`, id) return err } func (s *Store) AddProjectMember(projectID, userID, role string) (models.ProjectMember, error) { var member models.ProjectMember var err error var now time.Time now = time.Now().UTC() member = models.ProjectMember{ProjectID: projectID, UserID: userID, Role: role, CreatedAt: now.Unix()} _, err = s.DB.Exec(`INSERT INTO project_members (project_id, user_id, role, created_at) VALUES ((SELECT id FROM projects WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?)`, member.ProjectID, member.UserID, member.Role, now) return member, err } func (s *Store) UpdateProjectMemberRole(projectID, userID, role string) error { var err error _, err = s.DB.Exec(`UPDATE project_members SET role = ? WHERE project_id = (SELECT id FROM projects WHERE public_id = ?) AND user_id = (SELECT id FROM users WHERE public_id = ?)`, role, projectID, userID) return err } func (s *Store) RemoveProjectMember(projectID, userID string) error { var err error _, err = s.DB.Exec(`DELETE FROM project_members WHERE project_id = (SELECT id FROM projects WHERE public_id = ?) AND user_id = (SELECT id FROM users WHERE public_id = ?)`, projectID, userID) return err } func (s *Store) ListProjectMembers(projectID string) ([]models.ProjectMember, error) { var rows *sql.Rows var err error var members []models.ProjectMember var m models.ProjectMember var created time.Time rows, err = s.DB.Query(`SELECT p.public_id, u.public_id, m.role, m.created_at FROM project_members m JOIN projects p ON p.id = m.project_id JOIN users u ON u.id = m.user_id WHERE m.project_id = (SELECT id FROM projects WHERE public_id = ?) ORDER BY m.role`, projectID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&m.ProjectID, &m.UserID, &m.Role, &created) if err != nil { return nil, err } m.CreatedAt = created.Unix() members = append(members, m) } return members, rows.Err() } func (s *Store) GetProjectMemberRole(projectID, userID string) (string, error) { var role string var row *sql.Row var err error row = s.DB.QueryRow(`SELECT role FROM project_members WHERE project_id = (SELECT id FROM projects WHERE public_id = ?) AND user_id = (SELECT id FROM users WHERE public_id = ?)`, projectID, userID) err = row.Scan(&role) if err != nil { return "", err } return role, nil } func (s *Store) CreateRepo(repo models.Repo) (models.Repo, error) { var id string var err error var now time.Time var nowUnix int64 if repo.ID == "" { id, err = util.NewID() if err != nil { return repo, err } repo.ID = id } now = time.Now().UTC() nowUnix = now.Unix() repo.CreatedAt = nowUnix _, err = s.DB.Exec(`INSERT INTO repos (public_id, project_id, name, type, path, created_by, created_at) VALUES (?, (SELECT id FROM projects WHERE public_id = ?), ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?)`, repo.ID, repo.ProjectID, repo.Name, repo.Type, repo.Path, repo.CreatedBy, now) return repo, err } func (s *Store) RepoNameExists(projectID string, name string, repoType string) (bool, error) { var count int var row *sql.Row var err error row = s.DB.QueryRow(`SELECT COUNT(1) FROM repos WHERE project_id = (SELECT id FROM projects WHERE public_id = ?) AND name = ? AND type = ?`, projectID, name, repoType) err = row.Scan(&count) if err != nil { return false, err } return count > 0, nil } func (s *Store) GetRepo(id string) (models.Repo, error) { var repo models.Repo var row *sql.Row var created time.Time var err error row = s.DB.QueryRow(`SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by WHERE r.public_id = ?`, id) err = row.Scan(&repo.ID, &repo.ProjectID, &repo.Name, &repo.Type, &repo.Path, &repo.CreatedBy, &created) if err != nil { return repo, err } repo.CreatedAt = created.Unix() return repo, nil } func (s *Store) GetRepoStorageIDs(id string) (int64, int64, error) { var row *sql.Row var projectID int64 var repoID int64 var err error row = s.DB.QueryRow(`SELECT p.id, r.id FROM repos r JOIN projects p ON p.id = r.project_id WHERE r.public_id = ?`, id) err = row.Scan(&projectID, &repoID) if err != nil { return 0, 0, err } return projectID, repoID, nil } func (s *Store) GetRepoByProjectNameType(projectID string, name string, repoType string) (models.Repo, error) { var repo models.Repo var row *sql.Row var err error var created time.Time row = s.DB.QueryRow(`SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by WHERE r.project_id = (SELECT id FROM projects WHERE public_id = ?) AND r.name = ? AND r.type = ?`, projectID, name, repoType) err = row.Scan(&repo.ID, &repo.ProjectID, &repo.Name, &repo.Type, &repo.Path, &repo.CreatedBy, &created) if err != nil { return repo, err } repo.CreatedAt = created.Unix() return repo, nil } func (s *Store) ListRepos(projectID string) ([]models.Repo, error) { var rows *sql.Rows var err error var repos []models.Repo var r models.Repo var isForeign int var created time.Time rows, err = s.DB.Query(` SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at, 0 AS is_foreign FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by WHERE r.project_id = (SELECT id FROM projects WHERE public_id = ?) UNION ALL SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at, 1 AS is_foreign FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by JOIN project_repos pr ON pr.repo_id = r.id WHERE pr.project_id = (SELECT id FROM projects WHERE public_id = ?) AND r.project_id <> (SELECT id FROM projects WHERE public_id = ?) ORDER BY 3 `, projectID, projectID, projectID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&r.ID, &r.ProjectID, &r.Name, &r.Type, &r.Path, &r.CreatedBy, &created, &isForeign) if err != nil { return nil, err } r.CreatedAt = created.Unix() r.IsForeign = isForeign != 0 repos = append(repos, r) } return repos, rows.Err() } func (s *Store) ListReposOwned(projectID string) ([]models.Repo, error) { var rows *sql.Rows var err error var repos []models.Repo var r models.Repo var created time.Time rows, err = s.DB.Query(` SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by WHERE r.project_id = (SELECT id FROM projects WHERE public_id = ?) ORDER BY r.name `, projectID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&r.ID, &r.ProjectID, &r.Name, &r.Type, &r.Path, &r.CreatedBy, &created) if err != nil { return nil, err } r.CreatedAt = created.Unix() repos = append(repos, r) } return repos, rows.Err() } func (s *Store) ListAllRepos() ([]models.Repo, error) { var rows *sql.Rows var err error var repos []models.Repo var r models.Repo var created time.Time rows, err = s.DB.Query(` SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by ORDER BY r.name `) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&r.ID, &r.ProjectID, &r.Name, &r.Type, &r.Path, &r.CreatedBy, &created) if err != nil { return nil, err } r.CreatedAt = created.Unix() repos = append(repos, r) } return repos, rows.Err() } func (s *Store) ListReposByProjectIDs(projectIDs []string) ([]models.Repo, error) { var rows *sql.Rows var err error var repos []models.Repo var r models.Repo var created time.Time var placeholders []string var args []interface{} var i int var selected []interface{} if len(projectIDs) == 0 { return []models.Repo{}, nil } placeholders = make([]string, len(projectIDs)) args = make([]interface{}, len(projectIDs)) for i = 0; i < len(projectIDs); i++ { placeholders[i] = "?" selected = append(selected, projectIDs[i]) } args = make([]interface{}, len(selected)) for i = 0; i < len(selected); i++ { args[i] = selected[i] } rows, err = s.DB.Query(` SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by WHERE p.public_id IN (`+strings.Join(placeholders, ",")+`) ORDER BY r.name `, args...) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&r.ID, &r.ProjectID, &r.Name, &r.Type, &r.Path, &r.CreatedBy, &created) if err != nil { return nil, err } r.CreatedAt = created.Unix() repos = append(repos, r) } return repos, rows.Err() } func (s *Store) ListProjectIDsForUser(userID string) ([]string, error) { var rows *sql.Rows var err error var ids []string var id string rows, err = s.DB.Query(`SELECT p.public_id FROM project_members m JOIN projects p ON p.id = m.project_id WHERE m.user_id = (SELECT id FROM users WHERE public_id = ?)`, userID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&id) if err != nil { return nil, err } ids = append(ids, id) } return ids, rows.Err() } func (s *Store) UpdateRepo(repo models.Repo) error { var err error _, err = s.DB.Exec(`UPDATE repos SET name = ?, path = ? WHERE public_id = ?`, repo.Name, repo.Path, repo.ID) return err } func (s *Store) DeleteRepo(id string) error { var err error _, err = s.DB.Exec(`DELETE FROM repos WHERE public_id = ?`, id) return err } func (s *Store) AttachRepoToProject(projectID string, repoID string) error { var err error _, err = s.DB.Exec(`INSERT INTO project_repos (project_id, repo_id, created_at) VALUES ((SELECT id FROM projects WHERE public_id = ?), (SELECT id FROM repos WHERE public_id = ?), ?)`, projectID, repoID, time.Now().UTC()) return err } func (s *Store) DetachRepoFromProject(projectID string, repoID string) error { var err error _, err = s.DB.Exec(`DELETE FROM project_repos WHERE project_id = (SELECT id FROM projects WHERE public_id = ?) AND repo_id = (SELECT id FROM repos WHERE public_id = ?)`, projectID, repoID) return err } func (s *Store) GetRepoProjectIDs(repoID string) ([]string, error) { var rows *sql.Rows var err error var ids []string var id string rows, err = s.DB.Query(` SELECT p.public_id FROM repos r JOIN projects p ON p.id = r.project_id WHERE r.public_id = ? UNION SELECT p.public_id FROM project_repos pr JOIN projects p ON p.id = pr.project_id WHERE pr.repo_id = (SELECT id FROM repos WHERE public_id = ?) `, repoID, repoID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&id) if err != nil { return nil, err } ids = append(ids, id) } return ids, rows.Err() } func (s *Store) ListAvailableReposForProject(projectID string, query string, limit int, offset int) ([]models.Repo, error) { var rows *sql.Rows var err error var repos []models.Repo var r models.Repo var created time.Time if limit <= 0 { limit = 100 } if offset < 0 { offset = 0 } if query == "" { rows, err = s.DB.Query(` SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by WHERE p.public_id <> ? AND r.id NOT IN ( SELECT pr.repo_id FROM project_repos pr JOIN projects px ON px.id = pr.project_id WHERE px.public_id = ? ) ORDER BY r.name LIMIT ? OFFSET ? `, projectID, projectID, limit, offset) } else { rows, err = s.DB.Query(` SELECT r.public_id, p.public_id, r.name, r.type, r.path, u.public_id, r.created_at FROM repos r JOIN projects p ON p.id = r.project_id JOIN users u ON u.id = r.created_by WHERE p.public_id <> ? AND r.id NOT IN ( SELECT pr.repo_id FROM project_repos pr JOIN projects px ON px.id = pr.project_id WHERE px.public_id = ? ) AND r.name LIKE ? ORDER BY r.name LIMIT ? OFFSET ? `, projectID, projectID, "%"+query+"%", limit, offset) } if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&r.ID, &r.ProjectID, &r.Name, &r.Type, &r.Path, &r.CreatedBy, &created) if err != nil { return nil, err } r.CreatedAt = created.Unix() r.IsForeign = false repos = append(repos, r) } return repos, rows.Err() } func (s *Store) CreateIssue(issue models.Issue) (models.Issue, error) { var id string var err error var now time.Time var nowUnix int64 if issue.ID == "" { id, err = util.NewID() if err != nil { return issue, err } issue.ID = id } now = time.Now().UTC() nowUnix = now.Unix() issue.CreatedAt = nowUnix issue.UpdatedAt = nowUnix if issue.Status == "" { issue.Status = "open" } _, err = s.DB.Exec(` INSERT INTO issues (public_id, project_id, title, body, status, created_by, assignee_id, created_at, updated_at) VALUES (?, (SELECT id FROM projects WHERE public_id = ?), ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), (SELECT id FROM users WHERE public_id = ?), ?, ?) `, issue.ID, issue.ProjectID, issue.Title, issue.Body, issue.Status, issue.CreatedBy, issue.AssigneeID, now, now) return issue, err } func (s *Store) UpdateIssue(issue models.Issue) error { var err error var now time.Time var nowUnix int64 now = time.Now().UTC() nowUnix = now.Unix() issue.UpdatedAt = nowUnix _, err = s.DB.Exec(`UPDATE issues SET title = ?, body = ?, status = ?, assignee_id = (SELECT id FROM users WHERE public_id = ?), updated_at = ? WHERE public_id = ?`, issue.Title, issue.Body, issue.Status, issue.AssigneeID, now, issue.ID) return err } func (s *Store) GetIssue(id string) (models.Issue, error) { var issue models.Issue var row *sql.Row var created time.Time var updated time.Time var err error row = s.DB.QueryRow(`SELECT i.public_id, p.public_id, i.title, i.body, i.status, cu.public_id, COALESCE(au.public_id, ''), i.created_at, i.updated_at FROM issues i JOIN projects p ON p.id = i.project_id JOIN users cu ON cu.id = i.created_by LEFT JOIN users au ON au.id = i.assignee_id WHERE i.public_id = ?`, id) err = row.Scan(&issue.ID, &issue.ProjectID, &issue.Title, &issue.Body, &issue.Status, &issue.CreatedBy, &issue.AssigneeID, &created, &updated) if err != nil { return issue, err } issue.CreatedAt = created.Unix() issue.UpdatedAt = updated.Unix() return issue, nil } func (s *Store) ListIssues(projectID string) ([]models.Issue, error) { var rows *sql.Rows var err error var issues []models.Issue var issue models.Issue var created time.Time var updated time.Time rows, err = s.DB.Query(`SELECT i.public_id, p.public_id, i.title, i.body, i.status, cu.public_id, COALESCE(au.public_id, ''), i.created_at, i.updated_at FROM issues i JOIN projects p ON p.id = i.project_id JOIN users cu ON cu.id = i.created_by LEFT JOIN users au ON au.id = i.assignee_id WHERE i.project_id = (SELECT id FROM projects WHERE public_id = ?) ORDER BY i.created_at DESC`, projectID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&issue.ID, &issue.ProjectID, &issue.Title, &issue.Body, &issue.Status, &issue.CreatedBy, &issue.AssigneeID, &created, &updated) if err != nil { return nil, err } issue.CreatedAt = created.Unix() issue.UpdatedAt = updated.Unix() issues = append(issues, issue) } return issues, rows.Err() } func (s *Store) AddIssueComment(comment models.IssueComment) (models.IssueComment, error) { var id string var err error var now time.Time var nowUnix int64 if comment.ID == "" { id, err = util.NewID() if err != nil { return comment, err } comment.ID = id } now = time.Now().UTC() nowUnix = now.Unix() comment.CreatedAt = nowUnix _, err = s.DB.Exec(`INSERT INTO issue_comments (public_id, issue_id, body, created_by, created_at) VALUES (?, (SELECT id FROM issues WHERE public_id = ?), ?, (SELECT id FROM users WHERE public_id = ?), ?)`, comment.ID, comment.IssueID, comment.Body, comment.CreatedBy, now) return comment, err } func (s *Store) CreateWikiPage(page models.WikiPage) (models.WikiPage, error) { var id string var err error var now time.Time var nowUnix int64 if page.ID == "" { id, err = util.NewID() if err != nil { return page, err } page.ID = id } now = time.Now().UTC() nowUnix = now.Unix() page.UpdatedAt = nowUnix _, err = s.DB.Exec(`INSERT INTO wiki_pages (public_id, project_id, title, slug, body, created_by, updated_at) VALUES (?, (SELECT id FROM projects WHERE public_id = ?), ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?)`, page.ID, page.ProjectID, page.Title, page.Slug, page.Body, page.CreatedBy, now) return page, err } func (s *Store) UpdateWikiPage(page models.WikiPage) error { var err error var now time.Time var nowUnix int64 now = time.Now().UTC() nowUnix = now.Unix() page.UpdatedAt = nowUnix _, err = s.DB.Exec(`UPDATE wiki_pages SET title = ?, body = ?, updated_at = ? WHERE public_id = ?`, page.Title, page.Body, now, page.ID) return err } func (s *Store) ListWikiPages(projectID string) ([]models.WikiPage, error) { var rows *sql.Rows var err error var pages []models.WikiPage var page models.WikiPage var updated time.Time rows, err = s.DB.Query(`SELECT w.public_id, p.public_id, w.title, w.slug, w.body, u.public_id, w.updated_at FROM wiki_pages w JOIN projects p ON p.id = w.project_id JOIN users u ON u.id = w.created_by WHERE w.project_id = (SELECT id FROM projects WHERE public_id = ?) ORDER BY w.title`, projectID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&page.ID, &page.ProjectID, &page.Title, &page.Slug, &page.Body, &page.CreatedBy, &updated) if err != nil { return nil, err } page.UpdatedAt = updated.Unix() pages = append(pages, page) } return pages, rows.Err() } func (s *Store) GetWikiPage(id string) (models.WikiPage, error) { var page models.WikiPage var row *sql.Row var updated time.Time var err error row = s.DB.QueryRow(`SELECT w.public_id, p.public_id, w.title, w.slug, w.body, u.public_id, w.updated_at FROM wiki_pages w JOIN projects p ON p.id = w.project_id JOIN users u ON u.id = w.created_by WHERE w.public_id = ?`, id) err = row.Scan(&page.ID, &page.ProjectID, &page.Title, &page.Slug, &page.Body, &page.CreatedBy, &updated) if err != nil { return page, err } page.UpdatedAt = updated.Unix() return page, nil } func (s *Store) CreateUpload(upload models.Upload) (models.Upload, error) { var id string var err error var now time.Time var nowUnix int64 if upload.ID == "" { id, err = util.NewID() if err != nil { return upload, err } upload.ID = id } now = time.Now().UTC() nowUnix = now.Unix() upload.CreatedAt = nowUnix _, err = s.DB.Exec(`INSERT INTO uploads (public_id, project_id, filename, content_type, size, storage_path, created_by, created_at) VALUES (?, (SELECT id FROM projects WHERE public_id = ?), ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?)`, upload.ID, upload.ProjectID, upload.Filename, upload.ContentType, upload.Size, upload.StoragePath, upload.CreatedBy, now) return upload, err } func (s *Store) GetUpload(id string) (models.Upload, error) { var upload models.Upload var row *sql.Row var created time.Time var err error row = s.DB.QueryRow(`SELECT up.public_id, p.public_id, up.filename, up.content_type, up.size, up.storage_path, u.public_id, up.created_at FROM uploads up JOIN projects p ON p.id = up.project_id JOIN users u ON u.id = up.created_by WHERE up.public_id = ?`, id) err = row.Scan(&upload.ID, &upload.ProjectID, &upload.Filename, &upload.ContentType, &upload.Size, &upload.StoragePath, &upload.CreatedBy, &created) if err != nil { return upload, err } upload.CreatedAt = created.Unix() return upload, nil } func (s *Store) ListUploads(projectID string) ([]models.Upload, error) { var rows *sql.Rows var err error var uploads []models.Upload var upload models.Upload var created time.Time rows, err = s.DB.Query(`SELECT up.public_id, p.public_id, up.filename, up.content_type, up.size, up.storage_path, u.public_id, up.created_at FROM uploads up JOIN projects p ON p.id = up.project_id JOIN users u ON u.id = up.created_by WHERE up.project_id = (SELECT id FROM projects WHERE public_id = ?) ORDER BY up.created_at DESC`, projectID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&upload.ID, &upload.ProjectID, &upload.Filename, &upload.ContentType, &upload.Size, &upload.StoragePath, &upload.CreatedBy, &created) if err != nil { return nil, err } upload.CreatedAt = created.Unix() uploads = append(uploads, upload) } return uploads, rows.Err() } func (s *Store) RequireAdmin(user models.User) error { if !user.IsAdmin { return errors.New("admin required") } return nil } func mustID() string { var id string var err error id, err = util.NewID() if err != nil { panic(err) } return id } func nullIfEmpty(value string) interface{} { if value == "" { return nil } return value }