From 55a4bb73af2c63c12cc9fe5fe30c99f6513a1f4c Mon Sep 17 00:00:00 2001 From: Mitchell Date: Sat, 24 Aug 2019 12:47:53 -0500 Subject: [PATCH] feat: add user event log to admin page --- .../clip-player-control.tsx | 2 +- .../app/components/sound-list/sound-list.tsx | 4 +- .../upload-history/upload-history.tsx | 16 +++--- client/app/model/index.ts | 1 + client/app/model/user-event-log.ts | 11 ++++ client/app/pages/admin/admin.tsx | 52 ++++++++++++++++++- client/app/scss/style.scss | 15 ++++++ client/app/scss/table.scss | 6 +++ client/app/services/index.ts | 1 + client/app/services/user-event-log.service.ts | 9 ++++ server/bothandlers/sounds.go | 31 +++++++++-- server/webserver/middleware/jwt.go | 2 +- server/webserver/model/index.go | 4 +- server/webserver/model/sound.go | 4 +- server/webserver/model/user_event_log.go | 51 ++++++++++++++++++ server/webserver/routes/sound.go | 13 +++-- server/webserver/routes/user_event_log.go | 33 ++++++++++++ server/webserver/server.go | 1 + 18 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 client/app/model/user-event-log.ts create mode 100644 client/app/services/user-event-log.service.ts create mode 100644 server/webserver/model/user_event_log.go create mode 100644 server/webserver/routes/user_event_log.go diff --git a/client/app/components/clip-player-control/clip-player-control.tsx b/client/app/components/clip-player-control/clip-player-control.tsx index 2452640..458cc08 100644 --- a/client/app/components/clip-player-control/clip-player-control.tsx +++ b/client/app/components/clip-player-control/clip-player-control.tsx @@ -43,7 +43,7 @@ export class ClipPlayerControl extends React.Component { return ( this.checkExtension(sound.extension) && ( -
+
{ ? soundList.map((sound: SoundType, index: number) => { return (
-
{(sound.prefix || '') + sound.name}
+
+ {(type === 'sounds' && sound.prefix ? sound.prefix : '') + sound.name} +
diff --git a/client/app/components/upload-history/upload-history.tsx b/client/app/components/upload-history/upload-history.tsx index 02602a9..2f83f5e 100644 --- a/client/app/components/upload-history/upload-history.tsx +++ b/client/app/components/upload-history/upload-history.tsx @@ -14,14 +14,14 @@ export const UploadHistory = ({ sounds, showDiscordPlay }: IProps) => { return (
Upload History
- +
- + - - - + + + @@ -29,15 +29,15 @@ export const UploadHistory = ({ sounds, showDiscordPlay }: IProps) => { const formattedDate = DateTime.fromISO(s.created_at).toLocaleString(); return ( - - - + + + + + + ); + }); + } + render() { - return
TODO:
; + return ( +
+
+
User Event Log
+
DateDate SoundExtUsernameEmailExtUserEmail
+ {formattedDate} {s.name} + {s.extension} {s.user.username} + {s.user.email} diff --git a/client/app/model/index.ts b/client/app/model/index.ts index 13ff3d9..4dd648d 100644 --- a/client/app/model/index.ts +++ b/client/app/model/index.ts @@ -2,4 +2,5 @@ export * from './claims'; export * from './permissions'; export * from './sound'; export * from './user'; +export * from './user-event-log'; export * from './video-archive'; diff --git a/client/app/model/user-event-log.ts b/client/app/model/user-event-log.ts new file mode 100644 index 0000000..f3c6f75 --- /dev/null +++ b/client/app/model/user-event-log.ts @@ -0,0 +1,11 @@ +import { IUser } from './user'; + +export interface IUserEventLog { + content: string; + created_at: string; + deleted_at?: string; + id: number; + updated_at: string; + user: IUser; + user_id: string; +} diff --git a/client/app/pages/admin/admin.tsx b/client/app/pages/admin/admin.tsx index 4021d53..756aa1a 100644 --- a/client/app/pages/admin/admin.tsx +++ b/client/app/pages/admin/admin.tsx @@ -1,11 +1,59 @@ import React from 'react'; +import { IUserEventLog } from '../../model'; +import { UserEventLogService } from '../../services'; interface IProps {} -interface IState {} +interface IState { + userEventLogs: IUserEventLog[]; +} export class Admin extends React.Component { + constructor(props: IProps) { + super(props); + this.state = { + userEventLogs: [], + }; + } + componentDidMount() { + UserEventLogService.getUserEventLogs().then(userEventLogs => { + this.setState({ + userEventLogs, + }); + }); + } + + renderUserEventLogs() { + return this.state.userEventLogs.map(({ id, user, content, created_at }, index) => { + return ( +
{id}{created_at}{user.username}{content}
+ + + + + + + + + {this.renderUserEventLogs()} +
IDTimestampUserContent
+
+
+ ); } } diff --git a/client/app/scss/style.scss b/client/app/scss/style.scss index 761d232..5282adf 100644 --- a/client/app/scss/style.scss +++ b/client/app/scss/style.scss @@ -32,10 +32,19 @@ body { display: flex; } +.flex--center { + align-items: center; + justify-content: center; +} + .flex--v-center { align-items: center; } +.flex--h-center { + justify-content: center; +} + .content { padding: 20px; @include tinyScreen { @@ -77,3 +86,9 @@ body { display: none; } } + +.hide-small { + @include smallScreen { + display: none; + } +} diff --git a/client/app/scss/table.scss b/client/app/scss/table.scss index db7c044..3068ab1 100644 --- a/client/app/scss/table.scss +++ b/client/app/scss/table.scss @@ -4,7 +4,13 @@ thead { text-align: left; } + td, + th { + padding: 10px 5px; + } +} +.table--ellipsis { td, th { white-space: nowrap; diff --git a/client/app/services/index.ts b/client/app/services/index.ts index 09b1411..eaebee2 100644 --- a/client/app/services/index.ts +++ b/client/app/services/index.ts @@ -2,3 +2,4 @@ export * from './axios.service'; export * from './oauth.service'; export * from './sound.service'; export * from './storage.service'; +export * from './user-event-log.service'; diff --git a/client/app/services/user-event-log.service.ts b/client/app/services/user-event-log.service.ts new file mode 100644 index 0000000..5a505b1 --- /dev/null +++ b/client/app/services/user-event-log.service.ts @@ -0,0 +1,9 @@ +import { IUserEventLog } from '../model'; +import { axios } from './axios.service'; + +export class UserEventLogService { + public static async getUserEventLogs(): Promise { + const resp = await axios.get('/api/user-event-log'); + return resp.data.data; + } +} diff --git a/server/bothandlers/sounds.go b/server/bothandlers/sounds.go index 359e08b..3ecc4ff 100644 --- a/server/bothandlers/sounds.go +++ b/server/bothandlers/sounds.go @@ -10,6 +10,9 @@ import ( "sync" "time" + "github.com/mgerb/go-discord-bot/server/db" + "github.com/mgerb/go-discord-bot/server/webserver/model" + "github.com/bwmarrin/discordgo" "github.com/mgerb/go-discord-bot/server/config" "github.com/mgerb/go-discord-bot/server/util" @@ -104,10 +107,10 @@ func (conn *AudioConnection) handleMessage(m *discordgo.MessageCreate) { conn.clipAudio(m) case "random": - conn.PlayRandomAudio(m) + conn.PlayRandomAudio(m, nil) default: - conn.PlayAudio(command, m) + conn.PlayAudio(command, m, nil) } } } @@ -167,20 +170,20 @@ func (conn *AudioConnection) summon(m *discordgo.MessageCreate) { } // play a random sound clip -func (conn *AudioConnection) PlayRandomAudio(m *discordgo.MessageCreate) { +func (conn *AudioConnection) PlayRandomAudio(m *discordgo.MessageCreate, userID *string) { files, _ := ioutil.ReadDir(config.Config.SoundsPath) if len(files) > 0 { randomIndex := rand.Intn(len(files)) arr := strings.Split(files[randomIndex].Name(), ".") if len(arr) > 0 && arr[0] != "" { - conn.PlayAudio(arr[0], m) + conn.PlayAudio(arr[0], m, userID) } } } // PlayAudio - play audio in channel that user is in // if MessageCreate is null play in current channel -func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCreate) { +func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCreate, userID *string) { // summon bot to channel if new message passed in if m != nil { @@ -204,6 +207,24 @@ func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCre select { case conn.SoundQueue <- soundName: + var newUserID string + fromWebUI := false + + // from discord + if m != nil && m.Author != nil { + newUserID = m.Author.ID + } else { + fromWebUI = true + newUserID = *userID + } + + // log event when user plays sound clip + err := model.LogSoundPlayedEvent(db.GetConn(), newUserID, soundName, fromWebUI) + + if err != nil { + log.Error(err) + } + default: break } diff --git a/server/webserver/middleware/jwt.go b/server/webserver/middleware/jwt.go index 3b69cb8..71f4db2 100644 --- a/server/webserver/middleware/jwt.go +++ b/server/webserver/middleware/jwt.go @@ -20,7 +20,7 @@ const ( // CustomClaims - type CustomClaims struct { - ID string `json:"id"` + UserID string `json:"id"` Username string `json:"username"` Discriminator string `json:"discriminator"` Email string `json:"email"` diff --git a/server/webserver/model/index.go b/server/webserver/model/index.go index 82071b4..a76c660 100644 --- a/server/webserver/model/index.go +++ b/server/webserver/model/index.go @@ -1,9 +1,11 @@ package model -var Migrations []interface{} = []interface{}{ +// Migrations - list of database migrations +var Migrations = []interface{}{ &Message{}, &Attachment{}, &User{}, &VideoArchive{}, &Sound{}, + &UserEventLog{}, } diff --git a/server/webserver/model/sound.go b/server/webserver/model/sound.go index 57aaf24..89d9461 100644 --- a/server/webserver/model/sound.go +++ b/server/webserver/model/sound.go @@ -17,11 +17,11 @@ type Sound struct { User User `json:"user"` } -func SoundCreate(conn *gorm.DB, sound *Sound) error { +func SoundSave(conn *gorm.DB, sound *Sound) error { return conn.Create(sound).Error } -func SoundList(conn *gorm.DB) ([]Sound, error) { +func SoundGet(conn *gorm.DB) ([]Sound, error) { sound := []Sound{} err := conn.Set("gorm:auto_preload", true).Find(&sound).Error return sound, err diff --git a/server/webserver/model/user_event_log.go b/server/webserver/model/user_event_log.go new file mode 100644 index 0000000..9493fe0 --- /dev/null +++ b/server/webserver/model/user_event_log.go @@ -0,0 +1,51 @@ +package model + +import ( + "time" + + "github.com/jinzhu/gorm" +) + +// UserEventLog - logger for user events +type UserEventLog struct { + ID uint `gorm:"primary_key; auto_increment; not null" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at"` + Content string `json:"content"` + User User `json:"user"` + UserID string `json:"user_id"` +} + +// UserEventLogSave - +func UserEventLogSave(conn *gorm.DB, m *UserEventLog) error { + return conn.Save(m).Error +} + +// UserEventLogGet - returns all messages - must use paging +func UserEventLogGet(conn *gorm.DB, page int) ([]*UserEventLog, error) { + userEventLog := []*UserEventLog{} + err := conn.Offset(page*100).Limit(100).Order("created_at desc", true).Preload("User").Find(&userEventLog).Error + return userEventLog, err +} + +// LogSoundPlayedEvent - log event when user plays sound clip +func LogSoundPlayedEvent(conn *gorm.DB, userID, soundName string, fromWebUI bool) error { + + var content string + + // from discord + if !fromWebUI { + content = "played sound clip: " + soundName + } else { + content = "played sound clip from web UI: " + soundName + } + + // log play event + userEventLog := &UserEventLog{ + UserID: userID, + Content: content, + } + + return UserEventLogSave(conn, userEventLog) +} diff --git a/server/webserver/routes/sound.go b/server/webserver/routes/sound.go index 8ccdfa6..4676d83 100644 --- a/server/webserver/routes/sound.go +++ b/server/webserver/routes/sound.go @@ -23,7 +23,7 @@ func AddSoundRoutes(group *gin.RouterGroup) { } func listSoundHandler(c *gin.Context) { - archives, err := model.SoundList(db.GetConn()) + archives, err := model.SoundGet(db.GetConn()) if err != nil { response.InternalError(c, err) @@ -36,6 +36,9 @@ func listSoundHandler(c *gin.Context) { func postSoundPlayHandler(c *gin.Context) { connections := bothandlers.ActiveConnections + oc, _ := c.Get("claims") + claims, _ := oc.(*middleware.CustomClaims) + params := struct { Name string `json:"name"` }{} @@ -47,9 +50,9 @@ func postSoundPlayHandler(c *gin.Context) { if len(connections) == 1 && params.Name != "" { for _, con := range connections { if params.Name == "random" { - con.PlayRandomAudio(nil) + con.PlayRandomAudio(nil, &claims.UserID) } else { - con.PlayAudio(params.Name, nil) + con.PlayAudio(params.Name, nil, &claims.UserID) } } } @@ -89,7 +92,7 @@ func postSoundHandler(c *gin.Context) { log.Info(claims.Username, "uploaded", config.Config.SoundsPath+"/"+file.Filename) // save who uploaded the clip into the database - uploadSaveDB(claims.ID, file.Filename) + uploadSaveDB(claims.UserID, file.Filename) if err != nil { log.Error(err) @@ -106,7 +109,7 @@ func uploadSaveDB(userID, filename string) { extension := splitFilename[len(splitFilename)-1] name := strings.Join(splitFilename[:len(splitFilename)-1], ".") - model.SoundCreate(db.GetConn(), &model.Sound{ + model.SoundSave(db.GetConn(), &model.Sound{ UserID: userID, Name: name, Extension: extension, diff --git a/server/webserver/routes/user_event_log.go b/server/webserver/routes/user_event_log.go new file mode 100644 index 0000000..f69b53a --- /dev/null +++ b/server/webserver/routes/user_event_log.go @@ -0,0 +1,33 @@ +package routes + +import ( + "strconv" + + "github.com/gin-gonic/gin" + "github.com/mgerb/go-discord-bot/server/db" + "github.com/mgerb/go-discord-bot/server/webserver/middleware" + "github.com/mgerb/go-discord-bot/server/webserver/model" + "github.com/mgerb/go-discord-bot/server/webserver/response" +) + +// AddUserEventLogRoutes - +func AddUserEventLogRoutes(group *gin.RouterGroup) { + group.GET("/user-event-log", middleware.AuthorizedJWT(), middleware.AuthPermissions(middleware.PermAdmin), listEventLogHandler) +} + +func listEventLogHandler(c *gin.Context) { + + page, err := strconv.Atoi(c.Query("page")) + + if err != nil { + page = 0 + } + + userEventLogs, err := model.UserEventLogGet(db.GetConn(), page) + + if err != nil { + response.InternalError(c, err) + } else { + response.Success(c, userEventLogs) + } +} diff --git a/server/webserver/server.go b/server/webserver/server.go index 8431ddc..f371a21 100644 --- a/server/webserver/server.go +++ b/server/webserver/server.go @@ -31,6 +31,7 @@ func getRouter() *gin.Engine { routes.AddConfigRoutes(api) routes.AddSoundRoutes(api) routes.AddVideoArchiveRoutes(api) + routes.AddUserEventLogRoutes(api) router.NoRoute(func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.String(), "/api/") {