- {props.submitButton}
+ {/* {props.submitButton} */}
+ {props.submitLoading ? (
+
+ ) : (
+ props.submitButton
+ )}
)}
diff --git a/v1/frontend/src/screens/Dashboard.css b/v1/frontend/src/screens/Dashboard.css
new file mode 100644
index 0000000..195aeb3
--- /dev/null
+++ b/v1/frontend/src/screens/Dashboard.css
@@ -0,0 +1,7 @@
+.dashboard {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ height: 100vh;
+ width: 100%;
+}
diff --git a/v1/frontend/src/screens/Dashboard.jsx b/v1/frontend/src/screens/Dashboard.jsx
new file mode 100644
index 0000000..40ec5dc
--- /dev/null
+++ b/v1/frontend/src/screens/Dashboard.jsx
@@ -0,0 +1,14 @@
+import { CircleGreenBackground, Heart } from '../assets';
+import ChannelSideBar from '../components/ChannelSideBar';
+import './Dashboard.css';
+
+function Dashboard() {
+ return (
+
+ );
+}
+
+export default Dashboard;
diff --git a/v1/frontend/src/screens/SignIn.jsx b/v1/frontend/src/screens/SignIn.jsx
index 1ef9e73..2f04f9f 100644
--- a/v1/frontend/src/screens/SignIn.jsx
+++ b/v1/frontend/src/screens/SignIn.jsx
@@ -1,11 +1,14 @@
import { useEffect, useState } from 'react';
import { SmallModal } from '../components/Modal';
-import { Login } from '../../wailsjs/go/main/App';
+import { Login, SignedIn } from '../../wailsjs/go/main/App';
import { Eye, EyeSlash, Logo } from '../assets';
+import { Navigate, useNavigate } from 'react-router-dom';
import './SignIn.css';
+import { NavDashboard } from '../Navigation';
function SignIn() {
const [error, setError] = useState('');
+ const navigate = useNavigate();
const [password, setPassword] = useState('');
const updatePassword = (event) => setPassword(event.target.value);
const [showPassword, setShowPassword] = useState(false);
@@ -14,12 +17,25 @@ function SignIn() {
const [username, setUsername] = useState('');
const updateUsername = (event) => setUsername(event.target.value);
+ useEffect(() => {
+ SignedIn()
+ .then((signedIn) => {
+ if (signedIn) {
+ navigate(NavDashboard);
+ }
+ })
+ .catch((error) => {
+ setError(error);
+ });
+ }, []);
+
useEffect(() => {
if (signingIn) {
Login(username, password)
.then(() => {
setUsername('');
setPassword('');
+ navigate(NavDashboard);
})
.catch((error) => {
setError(error);
diff --git a/v1/frontend/src/screens/Start.css b/v1/frontend/src/screens/Start.css
new file mode 100644
index 0000000..e69de29
diff --git a/v1/frontend/src/screens/Start.jsx b/v1/frontend/src/screens/Start.jsx
new file mode 100644
index 0000000..e69de29
diff --git a/v1/internal/config/config.go b/v1/internal/config/config.go
index f0c3dff..492ea49 100644
--- a/v1/internal/config/config.go
+++ b/v1/internal/config/config.go
@@ -11,6 +11,8 @@ const (
configDirNix = ".rum-goggles"
configDirWin = "RumGoggles"
+ imageDir = "images"
+
logFile = "rumgoggles.log"
sqlFile = "rumgoggles.db"
)
@@ -32,6 +34,22 @@ func Database() (string, error) {
return path, nil
}
+func ImageDir() (string, error) {
+ cfgDir, err := configDir()
+ if err != nil {
+ return "", pkgErr("error getting config directory", err)
+ }
+
+ dir := filepath.Join(cfgDir, imageDir)
+
+ err = os.MkdirAll(dir, 0750)
+ if err != nil {
+ return "", fmt.Errorf("error making directory: %v", err)
+ }
+
+ return dir, nil
+}
+
// TODO: implement log rotation
// Rotate log file every week?
// Keep most recent 4 logs?
diff --git a/v1/internal/models/account.go b/v1/internal/models/account.go
index 5548af7..05b7493 100644
--- a/v1/internal/models/account.go
+++ b/v1/internal/models/account.go
@@ -6,36 +6,59 @@ import (
)
const (
- accountColumns = "id, username, cookies"
+ accountColumns = "id, uid, username, cookies, profile_image, api_key"
accountTable = "account"
)
type Account struct {
- ID *int64
- Username *string
- Cookies *string
+ ID *int64 `json:"id"`
+ UID *string `json:"uid"`
+ Username *string `json:"username"`
+ Cookies *string `json:"cookies"`
+ ProfileImage *string `json:"profile_image"`
+ ApiKey *string `json:"api_key"`
+}
+
+func (a *Account) values() []any {
+ return []any{a.ID, a.UID, a.Username, a.Cookies, a.ProfileImage, a.ApiKey}
+}
+
+func (a *Account) valuesNoID() []any {
+ return a.values()[1:]
+}
+
+func (a *Account) valuesEndID() []any {
+ vals := a.values()
+ return append(vals[1:], vals[0])
}
type sqlAccount struct {
- id sql.NullInt64
- username sql.NullString
- cookies sql.NullString
+ id sql.NullInt64
+ uid sql.NullString
+ username sql.NullString
+ cookies sql.NullString
+ profileImage sql.NullString
+ apiKey sql.NullString
}
func (sa *sqlAccount) scan(r Row) error {
- return r.Scan(&sa.id, &sa.username, &sa.cookies)
+ return r.Scan(&sa.id, &sa.uid, &sa.username, &sa.cookies, &sa.profileImage, &sa.apiKey)
}
func (sa sqlAccount) toAccount() *Account {
var a Account
a.ID = toInt64(sa.id)
+ a.UID = toString(sa.uid)
a.Username = toString(sa.username)
a.Cookies = toString(sa.cookies)
+ a.ProfileImage = toString(sa.profileImage)
+ a.ApiKey = toString(sa.apiKey)
return &a
}
type AccountService interface {
+ All() ([]Account, error)
AutoMigrate() error
ByUsername(username string) (*Account, error)
Create(a *Account) error
@@ -55,10 +78,41 @@ type accountService struct {
Database *sql.DB
}
+func (as *accountService) All() ([]Account, error) {
+ selectQ := fmt.Sprintf(`
+ SELECT %s
+ FROM "%s"
+ `, accountColumns, accountTable)
+
+ rows, err := as.Database.Query(selectQ)
+ if err != nil {
+ return nil, pkgErr("error executing select query", err)
+ }
+ defer rows.Close()
+
+ accounts := []Account{}
+ for rows.Next() {
+ sa := &sqlAccount{}
+
+ err = sa.scan(rows)
+ if err != nil {
+ return nil, pkgErr("error scanning row", err)
+ }
+
+ accounts = append(accounts, *sa.toAccount())
+ }
+ err = rows.Err()
+ if err != nil && err != sql.ErrNoRows {
+ return nil, pkgErr("error iterating over rows", err)
+ }
+
+ return accounts, nil
+}
+
func (as *accountService) AutoMigrate() error {
err := as.createAccountTable()
if err != nil {
- return err
+ return pkgErr(fmt.Sprintf("error creating %s table", accountTable), err)
}
return nil
@@ -68,14 +122,17 @@ func (as *accountService) createAccountTable() error {
createQ := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS "%s" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ uid TEXT UNIQUE,
username TEXT UNIQUE NOT NULL,
- cookies TEXT
+ cookies TEXT,
+ profile_image TEXT,
+ api_key TEXT
)
`, accountTable)
_, err := as.Database.Exec(createQ)
if err != nil {
- return fmt.Errorf("error creating table: %v", err)
+ return fmt.Errorf("error executing create query: %v", err)
}
return nil
@@ -103,7 +160,7 @@ func (as *accountService) ByUsername(username string) (*Account, error) {
if err == sql.ErrNoRows {
return nil, nil
}
- return nil, pkgErr(fmt.Sprintf("error querying \"%s\" by username", accountTable), err)
+ return nil, pkgErr("error executing select query", err)
}
return sa.toAccount(), nil
@@ -124,22 +181,31 @@ func (as *accountService) Create(a *Account) error {
VALUES (%s)
`, accountTable, columns, values(columns))
- _, err = as.Database.Exec(insertQ, a.Username, a.Cookies)
+ _, err = as.Database.Exec(insertQ, a.valuesNoID()...)
if err != nil {
- return pkgErr(fmt.Sprintf("error inserting %s", accountTable), err)
+ return pkgErr("error executing insert query", err)
}
return nil
}
func (as *accountService) DestructiveReset() error {
+ err := as.dropAccountTable()
+ if err != nil {
+ return pkgErr(fmt.Sprintf("error dropping %s table", accountTable), err)
+ }
+
+ return nil
+}
+
+func (as *accountService) dropAccountTable() error {
dropQ := fmt.Sprintf(`
DROP TABLE IF EXISTS "%s"
`, accountTable)
_, err := as.Database.Exec(dropQ)
if err != nil {
- return fmt.Errorf("error dropping table: %v", err)
+ return fmt.Errorf("error executing drop query: %v", err)
}
return nil
@@ -162,9 +228,9 @@ func (as *accountService) Update(a *Account) error {
WHERE id=?
`, accountTable, set(columns))
- _, err = as.Database.Exec(updateQ, a.Username, a.Cookies, a.ID)
+ _, err = as.Database.Exec(updateQ, a.valuesEndID()...)
if err != nil {
- return pkgErr(fmt.Sprintf("error updating %s", accountTable), err)
+ return pkgErr(fmt.Sprintf("error executing update query", accountTable), err)
}
return nil
diff --git a/v1/internal/models/accountchannel.go b/v1/internal/models/accountchannel.go
new file mode 100644
index 0000000..3ec36e2
--- /dev/null
+++ b/v1/internal/models/accountchannel.go
@@ -0,0 +1,94 @@
+package models
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+const (
+ accountChannelColumns = "a.id, a.uid, a.username, a.cookies, a.profile_image, a.api_key, c.id, c.account_id, c.cid, c.name, c.profile_image, c.api_key"
+)
+
+type AccountChannel struct {
+ Account
+ Channel
+}
+
+type sqlAccountChannel struct {
+ sqlAccount
+ sqlChannel
+}
+
+func (sac *sqlAccountChannel) scan(r Row) error {
+ return r.Scan(
+ &sac.sqlAccount.id,
+ &sac.sqlAccount.uid,
+ &sac.sqlAccount.username,
+ &sac.sqlAccount.cookies,
+ &sac.sqlAccount.profileImage,
+ &sac.sqlAccount.apiKey,
+ &sac.sqlChannel.id,
+ &sac.sqlChannel.accountID,
+ &sac.sqlChannel.cid,
+ &sac.sqlChannel.name,
+ &sac.sqlChannel.profileImage,
+ &sac.sqlChannel.apiKey,
+ )
+}
+
+func (sac *sqlAccountChannel) toAccountChannel() *AccountChannel {
+ var ac AccountChannel
+
+ ac.Account = *sac.toAccount()
+ ac.Channel = *sac.toChannel()
+
+ return &ac
+}
+
+type AccountChannelService interface {
+ All() ([]AccountChannel, error)
+}
+
+func NewAccountChannelService(db *sql.DB) AccountChannelService {
+ return &accountChannelService{
+ Database: db,
+ }
+}
+
+var _ AccountChannelService = &accountChannelService{}
+
+type accountChannelService struct {
+ Database *sql.DB
+}
+
+func (as *accountChannelService) All() ([]AccountChannel, error) {
+ selectQ := fmt.Sprintf(`
+ SELECT %s
+ FROM "%s" a
+ LEFT JOIN "%s" c ON a.id=c.account_id
+ `, accountChannelColumns, accountTable, channelTable)
+
+ rows, err := as.Database.Query(selectQ)
+ if err != nil {
+ return nil, pkgErr("error executing select query", err)
+ }
+ defer rows.Close()
+
+ accountChannels := []AccountChannel{}
+ for rows.Next() {
+ sac := &sqlAccountChannel{}
+
+ err = sac.scan(rows)
+ if err != nil {
+ return nil, pkgErr("error scanning row", err)
+ }
+
+ accountChannels = append(accountChannels, *sac.toAccountChannel())
+ }
+ err = rows.Err()
+ if err != nil && err != sql.ErrNoRows {
+ return nil, pkgErr("error iterating over rows", err)
+ }
+
+ return accountChannels, nil
+}
diff --git a/v1/internal/models/channel.go b/v1/internal/models/channel.go
new file mode 100644
index 0000000..1e3fa3c
--- /dev/null
+++ b/v1/internal/models/channel.go
@@ -0,0 +1,232 @@
+package models
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+const (
+ channelColumns = "id, account_id, cid, name, profile_image, api_key"
+ channelTable = "channel"
+)
+
+type Channel struct {
+ ID *int64 `json:"id"`
+ AccountID *int64 `json:"account_id"`
+ CID *string `json:"cid"`
+ Name *string `json:"name"`
+ ProfileImage *string `json:"profile_image"`
+ ApiKey *string `json:"api_key"`
+}
+
+func (c *Channel) values() []any {
+ return []any{c.ID, c.AccountID, c.CID, c.Name, c.ProfileImage, c.ApiKey}
+}
+
+func (c *Channel) valuesNoID() []any {
+ return c.values()[1:]
+}
+
+func (c *Channel) valuesEndID() []any {
+ vals := c.values()
+ return append(vals[1:], vals[0])
+}
+
+type sqlChannel struct {
+ id sql.NullInt64
+ accountID sql.NullInt64
+ cid sql.NullString
+ name sql.NullString
+ profileImage sql.NullString
+ apiKey sql.NullString
+}
+
+func (sc *sqlChannel) scan(r Row) error {
+ return r.Scan(&sc.id, &sc.accountID, &sc.cid, &sc.name, &sc.profileImage, &sc.apiKey)
+}
+
+func (sc sqlChannel) toChannel() *Channel {
+ var c Channel
+ c.ID = toInt64(sc.id)
+ c.AccountID = toInt64(sc.accountID)
+ c.CID = toString(sc.cid)
+ c.Name = toString(sc.name)
+ c.ProfileImage = toString(sc.profileImage)
+ c.ApiKey = toString(sc.apiKey)
+
+ return &c
+}
+
+type ChannelService interface {
+ AutoMigrate() error
+ ByName(name string) (*Channel, error)
+ Create(c *Channel) error
+ DestructiveReset() error
+}
+
+func NewChannelService(db *sql.DB) ChannelService {
+ return &channelService{
+ Database: db,
+ }
+}
+
+var _ ChannelService = &channelService{}
+
+type channelService struct {
+ Database *sql.DB
+}
+
+func (cs *channelService) AutoMigrate() error {
+ err := cs.createChannelTable()
+ if err != nil {
+ return pkgErr(fmt.Sprintf("error creating %s table", channelTable), err)
+ }
+
+ return nil
+}
+
+func (cs *channelService) createChannelTable() error {
+ createQ := fmt.Sprintf(`
+ CREATE TABLE IF NOT EXISTS "%s" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ account_id INTEGER NOT NULL,
+ cid TEXT UNIQUE NOT NULL,
+ name TEXT UNIQUE NOT NULL,
+ profile_image TEXT,
+ api_key TEXT NOT NULL,
+ FOREIGN KEY (account_id) REFERENCES "%s" (id)
+ )
+ `, channelTable, accountTable)
+
+ _, err := cs.Database.Exec(createQ)
+ if err != nil {
+ return fmt.Errorf("error executing create query: %v", err)
+ }
+
+ return nil
+}
+
+func (cs *channelService) ByName(name string) (*Channel, error) {
+ err := runChannelValFuncs(
+ &Channel{Name: &name},
+ channelRequireName,
+ )
+ if err != nil {
+ return nil, pkgErr("", err)
+ }
+
+ selectQ := fmt.Sprintf(`
+ SELECT %s
+ FROM "%s"
+ WHERE name=?
+ `, channelColumns, channelTable)
+
+ var sc sqlChannel
+ row := cs.Database.QueryRow(selectQ, name)
+ err = sc.scan(row)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ return nil, pkgErr("error executing select query", err)
+ }
+
+ return sc.toChannel(), nil
+}
+
+func (cs *channelService) Create(c *Channel) error {
+ err := runChannelValFuncs(
+ c,
+ channelRequireAccountID,
+ channelRequireApiKey,
+ channelRequireCID,
+ channelRequireName,
+ )
+ if err != nil {
+ return pkgErr("invalid channel", err)
+ }
+
+ columns := columnsNoID(channelColumns)
+ insertQ := fmt.Sprintf(`
+ INSERT INTO "%s" (%s)
+ VALUES (%s)
+ `, channelTable, columns, values(columns))
+
+ _, err = cs.Database.Exec(insertQ, c.valuesNoID()...)
+ if err != nil {
+ return pkgErr("error executing insert query", err)
+ }
+
+ return nil
+}
+
+func (cs *channelService) DestructiveReset() error {
+ err := cs.dropChannelTable()
+ if err != nil {
+ return pkgErr(fmt.Sprintf("error dropping %s table", channelTable), err)
+ }
+
+ return nil
+}
+
+func (cs *channelService) dropChannelTable() error {
+ dropQ := fmt.Sprintf(`
+ DROP TABLE IF EXISTS "%s"
+ `, channelTable)
+
+ _, err := cs.Database.Exec(dropQ)
+ if err != nil {
+ return fmt.Errorf("error executing drop query: %v", err)
+ }
+
+ return nil
+}
+
+type channelValFunc func(*Channel) error
+
+func runChannelValFuncs(c *Channel, fns ...channelValFunc) error {
+ if c == nil {
+ return fmt.Errorf("channel cannot be nil")
+ }
+
+ for _, fn := range fns {
+ err := fn(c)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func channelRequireAccountID(c *Channel) error {
+ if c.AccountID == nil || *c.AccountID <= 0 {
+ return ErrChannelInvalidAccountID
+ }
+
+ return nil
+}
+
+func channelRequireApiKey(c *Channel) error {
+ if c.ApiKey == nil || *c.ApiKey == "" {
+ return ErrChannelInvalidApiKey
+ }
+
+ return nil
+}
+
+func channelRequireCID(c *Channel) error {
+ if c.CID == nil || *c.CID == "" {
+ return ErrChannelInvalidCID
+ }
+
+ return nil
+}
+
+func channelRequireName(c *Channel) error {
+ if c.Name == nil || *c.Name == "" {
+ return ErrChannelInvalidName
+ }
+
+ return nil
+}
diff --git a/v1/internal/models/error.go b/v1/internal/models/error.go
index 36a3e59..f011ab8 100644
--- a/v1/internal/models/error.go
+++ b/v1/internal/models/error.go
@@ -7,6 +7,11 @@ const (
ErrAccountInvalidUsername ValidatorError = "invalid account username"
ErrAccountInvalidID ValidatorError = "invalid account id"
+
+ ErrChannelInvalidAccountID ValidatorError = "invalid channel account id"
+ ErrChannelInvalidApiKey ValidatorError = "invalid channel API key"
+ ErrChannelInvalidCID ValidatorError = "invalid channel CID"
+ ErrChannelInvalidName ValidatorError = "invalid channel name"
)
func pkgErr(prefix string, err error) error {
diff --git a/v1/internal/models/services.go b/v1/internal/models/services.go
index 8a93cb3..cccde7e 100644
--- a/v1/internal/models/services.go
+++ b/v1/internal/models/services.go
@@ -7,23 +7,27 @@ import (
type migrationFunc func() error
-type service struct {
+type table struct {
name string
automigrate migrationFunc
destructivereset migrationFunc
}
type Services struct {
- AccountS AccountService
- Database *sql.DB
- services []service
+ AccountS AccountService
+ AccountChannelS AccountChannelService
+ ChannelS ChannelService
+ Database *sql.DB
+ tables []table
}
func (s *Services) AutoMigrate() error {
- for _, service := range s.services {
- err := service.automigrate()
- if err != nil {
- return pkgErr(fmt.Sprintf("error auto-migrating %s service", service.name), err)
+ for _, table := range s.tables {
+ if table.automigrate != nil {
+ err := table.automigrate()
+ if err != nil {
+ return pkgErr(fmt.Sprintf("error auto-migrating %s table", table.name), err)
+ }
}
}
@@ -40,10 +44,12 @@ func (s *Services) Close() error {
}
func (s *Services) DestructiveReset() error {
- for _, service := range s.services {
- err := service.destructivereset()
- if err != nil {
- return pkgErr(fmt.Sprintf("error destructive-resetting %s service", service.name), err)
+ for _, table := range s.tables {
+ if table.destructivereset != nil {
+ err := table.destructivereset()
+ if err != nil {
+ return pkgErr(fmt.Sprintf("error destructive-resetting %s table", table.name), err)
+ }
}
}
@@ -78,7 +84,24 @@ func WithDatabase(file string) ServicesInit {
func WithAccountService() ServicesInit {
return func(s *Services) error {
s.AccountS = NewAccountService(s.Database)
- s.services = append(s.services, service{accountTable, s.AccountS.AutoMigrate, s.AccountS.DestructiveReset})
+ s.tables = append(s.tables, table{accountTable, s.AccountS.AutoMigrate, s.AccountS.DestructiveReset})
+
+ return nil
+ }
+}
+
+func WithAccountChannelService() ServicesInit {
+ return func(s *Services) error {
+ s.AccountChannelS = NewAccountChannelService(s.Database)
+
+ return nil
+ }
+}
+
+func WithChannelService() ServicesInit {
+ return func(s *Services) error {
+ s.ChannelS = NewChannelService(s.Database)
+ s.tables = append(s.tables, table{channelTable, s.ChannelS.AutoMigrate, s.ChannelS.DestructiveReset})
return nil
}
diff --git a/v1/main.go b/v1/main.go
index 52f3064..7bc52ba 100644
--- a/v1/main.go
+++ b/v1/main.go
@@ -2,7 +2,10 @@ package main
import (
"embed"
+ "net/http"
+ "strings"
+ "github.com/tylertravisty/rum-goggles/v1/internal/config"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
@@ -21,7 +24,8 @@ func main() {
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
- Assets: assets,
+ Assets: assets,
+ Handler: http.HandlerFunc(GetImage),
},
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
OnShutdown: app.shutdown,
@@ -35,3 +39,12 @@ func main() {
println("Error:", err.Error())
}
}
+
+func GetImage(w http.ResponseWriter, r *http.Request) {
+ path := strings.Replace(r.RequestURI, "wails://wails", "", 1)
+ prefix, err := config.ImageDir()
+ if err != nil {
+ return
+ }
+ http.ServeFile(w, r, prefix+path)
+}