1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-29 12:30:11 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
ebacfd43be
form: fix captcha, matrix, telegram
new issue though: discord/telegram/matrix aren't linked when email
confirmation is used!
2023-06-21 20:00:48 +01:00
e4a7172517
messages: assign tokens to jf users on userpage
pins generated on the user page are assigned to that user, no other
jellyifn user can verify them.
2023-06-21 18:26:08 +01:00
3747eaa3a7
messages: refactor dc/tg, fix tg
less external access to Discord/TelegramDaemon internals, will be easier
to keep user/admin-side uses functioning similarly. Also changed their
internal token stores to use a map, and store an expiry. verifiedTokens
is also now a map in telegram. Also fixed issue where token wasn't being
deleted after use on the user page.
2023-06-21 18:02:33 +01:00
761d8d1c03
userpage: refresh pin when contact changed > once 2023-06-21 17:07:02 +01:00
4e7f720214
userpage: hide bg on login, dont refresh page ever 2023-06-21 17:02:57 +01:00
757c3a8aed
userpage: move cards around 2023-06-21 13:31:43 +01:00
87b0ae6614
userpage: adjust message row span depending on length 2023-06-21 13:30:09 +01:00
12 changed files with 286 additions and 233 deletions

View File

@ -332,18 +332,12 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
respondBool(400, false, gc) respondBool(400, false, gc)
return return
} }
tokenIndex := -1 tgToken, ok := app.telegram.TokenVerified(req.Token)
for i, v := range app.telegram.verifiedTokens { app.telegram.DeleteVerifiedToken(req.Token)
if v.Token == req.Token { if !ok {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} }
tgToken := app.telegram.verifiedTokens[tokenIndex]
tgUser := TelegramUser{ tgUser := TelegramUser{
ChatID: tgToken.ChatID, ChatID: tgToken.ChatID,
Username: tgToken.Username, Username: tgToken.Username,
@ -352,17 +346,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok { if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
tgUser.Lang = lang tgUser.Lang = lang
} }
if app.storage.GetTelegram() == nil {
app.storage.telegram = telegramStore{}
}
app.storage.SetTelegramKey(req.ID, tgUser) app.storage.SetTelegramKey(req.ID, tgUser)
err := app.storage.storeTelegramUsers()
if err != nil {
app.err.Printf("Failed to store Telegram users: %v", err)
} else {
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
}
linkExistingOmbiDiscordTelegram(app) linkExistingOmbiDiscordTelegram(app)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -462,19 +446,8 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
// @tags Other // @tags Other
func (app *appContext) TelegramVerified(gc *gin.Context) { func (app *appContext) TelegramVerified(gc *gin.Context) {
pin := gc.Param("pin") pin := gc.Param("pin")
tokenIndex := -1 _, ok := app.telegram.TokenVerified(pin)
for i, v := range app.telegram.verifiedTokens { respondBool(200, ok, gc)
if v.Token == pin {
tokenIndex = i
break
}
}
// if tokenIndex != -1 {
// length := len(app.telegram.verifiedTokens)
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
// app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
// }
respondBool(200, tokenIndex != -1, gc)
} }
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code. // @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
@ -492,27 +465,13 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
return return
} }
pin := gc.Param("pin") pin := gc.Param("pin")
tokenIndex := -1 token, ok := app.telegram.TokenVerified(pin)
for i, v := range app.telegram.verifiedTokens { if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
if v.Token == pin { app.discord.DeleteVerifiedUser(pin)
tokenIndex = i respondBool(400, false, gc)
break return
}
} }
if app.config.Section("telegram").Key("require_unique").MustBool(false) { respondBool(200, ok, gc)
for _, u := range app.storage.GetTelegram() {
if app.telegram.verifiedTokens[tokenIndex].Username == u.Username {
respondBool(400, false, gc)
return
}
}
}
// if tokenIndex != -1 {
// length := len(app.telegram.verifiedTokens)
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
// app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
// }
respondBool(200, tokenIndex != -1, gc)
} }
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code. // @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
@ -530,15 +489,11 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
return return
} }
pin := gc.Param("pin") pin := gc.Param("pin")
_, ok := app.discord.verifiedTokens[pin] user, ok := app.discord.UserVerified(pin)
if app.config.Section("discord").Key("require_unique").MustBool(false) { if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.ID) {
for _, u := range app.storage.GetDiscord() { delete(app.discord.verifiedTokens, pin)
if app.discord.verifiedTokens[pin].ID == u.ID { respondBool(400, false, gc)
delete(app.discord.verifiedTokens, pin) return
respondBool(400, false, gc)
return
}
}
} }
respondBool(200, ok, gc) respondBool(200, ok, gc)
} }

View File

@ -301,10 +301,10 @@ func (app *appContext) GetMyPIN(gc *gin.Context) {
resp := GetMyPINDTO{} resp := GetMyPINDTO{}
switch service { switch service {
case "discord": case "discord":
resp.PIN = app.discord.NewAuthToken() resp.PIN = app.discord.NewAssignedAuthToken(gc.GetString("jfId"))
break break
case "telegram": case "telegram":
resp.PIN = app.telegram.NewAuthToken() resp.PIN = app.telegram.NewAssignedAuthToken(gc.GetString("jfId"))
break break
default: default:
respond(400, "invalid service", gc) respond(400, "invalid service", gc)
@ -322,19 +322,15 @@ func (app *appContext) GetMyPIN(gc *gin.Context) {
// @tags User Page // @tags User Page
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) { func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
pin := gc.Param("pin") pin := gc.Param("pin")
dcUser, ok := app.discord.verifiedTokens[pin] dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
app.discord.DeleteVerifiedUser(pin)
if !ok { if !ok {
respondBool(200, false, gc) respondBool(200, false, gc)
return return
} }
if app.config.Section("discord").Key("require_unique").MustBool(false) { if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(dcUser.ID) {
for _, u := range app.storage.GetDiscord() { respondBool(400, false, gc)
if app.discord.verifiedTokens[pin].ID == u.ID { return
delete(app.discord.verifiedTokens, pin)
respondBool(400, false, gc)
return
}
}
} }
existingUser, ok := app.storage.GetDiscordKey(gc.GetString("jfId")) existingUser, ok := app.storage.GetDiscordKey(gc.GetString("jfId"))
if ok { if ok {
@ -354,30 +350,24 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
// @tags User Page // @tags User Page
func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) { func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
pin := gc.Param("pin") pin := gc.Param("pin")
tokenIndex := -1 token, ok := app.telegram.AssignedTokenVerified(pin, gc.GetString("jfId"))
for i, v := range app.telegram.verifiedTokens { app.telegram.DeleteVerifiedToken(pin)
if v.Token == pin { if !ok {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
respondBool(200, false, gc) respondBool(200, false, gc)
return return
} }
if app.config.Section("telegram").Key("require_unique").MustBool(false) { if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
for _, u := range app.storage.GetTelegram() { respondBool(400, false, gc)
if app.telegram.verifiedTokens[tokenIndex].Username == u.Username { return
respondBool(400, false, gc)
return
}
}
} }
tgUser := TelegramUser{ tgUser := TelegramUser{
ChatID: app.telegram.verifiedTokens[tokenIndex].ChatID, ChatID: token.ChatID,
Username: app.telegram.verifiedTokens[tokenIndex].Username, Username: token.Username,
Contact: true, Contact: true,
} }
if lang, ok := app.telegram.languages[tgUser.ChatID]; ok {
tgUser.Lang = lang
}
existingUser, ok := app.storage.GetTelegramKey(gc.GetString("jfId")) existingUser, ok := app.storage.GetTelegramKey(gc.GetString("jfId"))
if ok { if ok {

View File

@ -121,7 +121,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
return return
} }
} else { } else {
discordUser, discordVerified = app.discord.verifiedTokens[req.DiscordPIN] discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN)
if !discordVerified { if !discordVerified {
f = func(gc *gin.Context) { f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code) app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code)
@ -193,7 +193,8 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
} }
} }
telegramTokenIndex := -1 var tgToken TelegramVerifiedToken
telegramVerified := false
if telegramEnabled { if telegramEnabled {
if req.TelegramPIN == "" { if req.TelegramPIN == "" {
if app.config.Section("telegram").Key("required").MustBool(false) { if app.config.Section("telegram").Key("required").MustBool(false) {
@ -205,13 +206,8 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
return return
} }
} else { } else {
for i, v := range app.telegram.verifiedTokens { tgToken, telegramVerified = app.telegram.TokenVerified(req.TelegramPIN)
if v.Token == req.TelegramPIN { if !telegramVerified {
telegramTokenIndex = i
break
}
}
if telegramTokenIndex == -1 {
f = func(gc *gin.Context) { f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Telegram PIN was invalid", req.Code) app.debug.Printf("%s: New user failed: Telegram PIN was invalid", req.Code)
respond(401, "errorInvalidPIN", gc) respond(401, "errorInvalidPIN", gc)
@ -219,17 +215,13 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false success = false
return return
} }
if app.config.Section("telegram").Key("require_unique").MustBool(false) { if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(tgToken.Username) {
for _, u := range app.storage.GetTelegram() { f = func(gc *gin.Context) {
if app.telegram.verifiedTokens[telegramTokenIndex].Username == u.Username { app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code)
f = func(gc *gin.Context) { respond(400, "errorAccountLinked", gc)
app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code)
respond(400, "errorAccountLinked", gc)
}
success = false
return
}
} }
success = false
return
} }
} }
} }
@ -352,7 +344,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.err.Printf("Failed to store user duration: %v", err) app.err.Printf("Failed to store user duration: %v", err)
} }
} }
if discordEnabled && discordVerified { if discordVerified {
discordUser.Contact = req.DiscordContact discordUser.Contact = req.DiscordContact
if app.storage.discord == nil { if app.storage.discord == nil {
app.storage.discord = discordStore{} app.storage.discord = discordStore{}
@ -364,8 +356,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
delete(app.discord.verifiedTokens, req.DiscordPIN) delete(app.discord.verifiedTokens, req.DiscordPIN)
} }
} }
if telegramEnabled && telegramTokenIndex != -1 { if telegramVerified {
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
tgUser := TelegramUser{ tgUser := TelegramUser{
ChatID: tgToken.ChatID, ChatID: tgToken.ChatID,
Username: tgToken.Username, Username: tgToken.Username,
@ -377,13 +368,8 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
if app.storage.telegram == nil { if app.storage.telegram == nil {
app.storage.telegram = telegramStore{} app.storage.telegram = telegramStore{}
} }
app.telegram.DeleteVerifiedToken(req.TelegramPIN)
app.storage.SetTelegramKey(user.ID, tgUser) app.storage.SetTelegramKey(user.ID, tgUser)
if err := app.storage.storeTelegramUsers(); err != nil {
app.err.Printf("Failed to store Telegram users: %v", err)
} else {
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
}
} }
if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) { if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) {
if profile.Ombi != nil && len(profile.Ombi) != 0 { if profile.Ombi != nil && len(profile.Ombi) != 0 {
@ -394,17 +380,17 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
} else { } else {
app.info.Println("Created Ombi user") app.info.Println("Created Ombi user")
if (discordEnabled && discordVerified) || (telegramEnabled && telegramTokenIndex != -1) { if discordVerified || telegramVerified {
ombiUser, status, err := app.getOmbiUser(id) ombiUser, status, err := app.getOmbiUser(id)
if status != 200 || err != nil { if status != 200 || err != nil {
app.err.Printf("Failed to get Ombi user (%d): %v", status, err) app.err.Printf("Failed to get Ombi user (%d): %v", status, err)
} else { } else {
dID := "" dID := ""
tUser := "" tUser := ""
if discordEnabled && discordVerified { if discordVerified {
dID = discordUser.ID dID = discordUser.ID
} }
if telegramEnabled && telegramTokenIndex != -1 { if telegramVerified {
u, _ := app.storage.GetTelegramKey(user.ID) u, _ := app.storage.GetTelegramKey(user.ID)
tUser = u.Username tUser = u.Username
} }
@ -431,7 +417,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.err.Printf("Failed to store Matrix users: %v", err) app.err.Printf("Failed to store Matrix users: %v", err)
} }
} }
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified { if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified {
name := app.getAddressOrName(user.ID) name := app.getAddressOrName(user.ID)
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name) app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
msg, err := app.email.constructWelcome(req.Username, expiry, app, false) msg, err := app.email.constructWelcome(req.Username, expiry, app, false)

View File

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
dg "github.com/bwmarrin/discordgo" dg "github.com/bwmarrin/discordgo"
) )
@ -12,8 +13,8 @@ type DiscordDaemon struct {
ShutdownChannel chan string ShutdownChannel chan string
bot *dg.Session bot *dg.Session
username string username string
tokens []string tokens map[string]VerifToken // Map of pins to tokens.
verifiedTokens map[string]DiscordUser // Map of tokens to discord users. verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
channelID, channelName, inviteChannelID, inviteChannelName string channelID, channelName, inviteChannelID, inviteChannelName string
guildID string guildID string
serverChannelName, serverName string serverChannelName, serverName string
@ -37,7 +38,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
Stopped: false, Stopped: false,
ShutdownChannel: make(chan string), ShutdownChannel: make(chan string),
bot: bot, bot: bot,
tokens: []string{}, tokens: map[string]VerifToken{},
verifiedTokens: map[string]DiscordUser{}, verifiedTokens: map[string]DiscordUser{},
users: map[string]DiscordUser{}, users: map[string]DiscordUser{},
app: app, app: app,
@ -58,7 +59,15 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD". // NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
func (d *DiscordDaemon) NewAuthToken() string { func (d *DiscordDaemon) NewAuthToken() string {
pin := genAuthToken() pin := genAuthToken()
d.tokens = append(d.tokens, pin) d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: ""}
return pin
}
// NewAssignedAuthToken generates an 8-character pin in the form "A1-2B-CD",
// and assigns it for access only with the given Jellyfin ID.
func (d *DiscordDaemon) NewAssignedAuthToken(id string) string {
pin := genAuthToken()
d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: id}
return pin return pin
} }
@ -431,14 +440,8 @@ func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang st
func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang string) { func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang string) {
pin := i.ApplicationCommandData().Options[0].StringValue() pin := i.ApplicationCommandData().Options[0].StringValue()
tokenIndex := -1 user, ok := d.tokens[pin]
for i, token := range d.tokens { if !ok || time.Now().After(user.Expiry) {
if pin == token {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
// Type: dg.InteractionResponseChannelMessageWithSource, // Type: dg.InteractionResponseChannelMessageWithSource,
Type: dg.InteractionResponseChannelMessageWithSource, Type: dg.InteractionResponseChannelMessageWithSource,
@ -450,6 +453,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
if err != nil { if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err) d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
} }
delete(d.tokens, pin)
return return
} }
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
@ -463,9 +467,10 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
if err != nil { if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err) d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
} }
d.verifiedTokens[pin] = d.users[i.Interaction.Member.User.ID] dcUser := d.users[i.Interaction.Member.User.ID]
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1] dcUser.JellyfinID = user.JellyfinID
d.tokens = d.tokens[:len(d.tokens)-1] d.verifiedTokens[pin] = dcUser
delete(d.tokens, pin)
} }
func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang string) { func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang string) {
@ -606,14 +611,8 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
d.app.debug.Println("Discord: Ignoring message as user was not found") d.app.debug.Println("Discord: Ignoring message as user was not found")
return return
} }
tokenIndex := -1 user, ok := d.tokens[sects[0]]
for i, token := range d.tokens { if !ok || time.Now().After(user.Expiry) {
if sects[0] == token {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
_, err := s.ChannelMessageSend( _, err := s.ChannelMessageSend(
m.ChannelID, m.ChannelID,
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"), d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
@ -621,6 +620,7 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
if err != nil { if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err) d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
} }
delete(d.tokens, sects[0])
return return
} }
_, err := s.ChannelMessageSend( _, err := s.ChannelMessageSend(
@ -630,9 +630,10 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
if err != nil { if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err) d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
} }
d.verifiedTokens[sects[0]] = d.users[m.Author.ID] dcUser := d.users[m.Author.ID]
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1] dcUser.JellyfinID = user.JellyfinID
d.tokens = d.tokens[:len(d.tokens)-1] d.verifiedTokens[sects[0]] = dcUser
delete(d.tokens, sects[0])
} }
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error { func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
@ -686,3 +687,38 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
} }
return nil return nil
} }
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
user, ok = d.verifiedTokens[pin]
// delete(d.verifiedTokens, pin)
return
}
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
// Returns false if the given Jellyfin ID does not match the one in the user.
func (d *DiscordDaemon) AssignedUserVerified(pin string, jfID string) (user DiscordUser, ok bool) {
user, ok = d.verifiedTokens[pin]
if ok && user.JellyfinID != jfID {
ok = false
}
// delete(d.verifiedUsers, pin)
return
}
// UserExists returns whether or not a user with the given ID exists.
func (d *DiscordDaemon) UserExists(id string) (ok bool) {
ok = false
for _, u := range d.app.storage.GetDiscord() {
if u.ID == id {
ok = true
break
}
}
return
}
// DeleteVerifiedUser removes the token with the given PIN.
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
delete(d.verifiedTokens, pin)
}

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}"> <html lang="en" class="{{ .cssClass }}">
<head> <head>
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css"> <link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }} {{ template "header.html" . }}
<title>{{ .strings.successHeader }} - jfa-go</title> <title>{{ .strings.successHeader }} - jfa-go</title>
</head> </head>

View File

@ -69,20 +69,11 @@
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span> <span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span> <span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
</div> </div>
<div class="page-container"> <div class="page-container unfocused">
<div class="card @low dark:~d_neutral mb-4" id="card-user"> <div class="card @low dark:~d_neutral mb-4" id="card-user">
<span class="heading mb-2"></span> <span class="heading mb-2"></span>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="card @low dark:~d_neutral unfocused" id="card-status">
<span class="heading mb-2">{{ .strings.expiry }}</span>
<aside class="aside ~warning user-expiry my-4"></aside>
<div class="user-expiry-countdown"></div>
</div>
<div class="card @low dark:~d_neutral flex-col" id="card-contact">
<span class="heading mb-2">{{ .strings.contactMethods }}</span>
<div class="content flex justify-between flex-col h-100"></div>
</div>
{{ if index . "PageMessageEnabled" }} {{ if index . "PageMessageEnabled" }}
{{ if .PageMessageEnabled }} {{ if .PageMessageEnabled }}
<div class="card @low dark:~d_neutral content" id="card-message"> <div class="card @low dark:~d_neutral content" id="card-message">
@ -90,6 +81,15 @@
</div> </div>
{{ end }} {{ end }}
{{ end }} {{ end }}
<div class="card @low dark:~d_neutral flex-col" id="card-contact">
<span class="heading mb-2">{{ .strings.contactMethods }}</span>
<div class="content flex justify-between flex-col h-100"></div>
</div>
<div class="card @low dark:~d_neutral unfocused" id="card-status">
<span class="heading mb-2">{{ .strings.expiry }}</span>
<aside class="aside ~warning user-expiry my-4"></aside>
<div class="user-expiry-countdown"></div>
</div>
</div> </div>
</div> </div>
<script src="{{ .urlBase }}/js/user.js" type="module"></script> <script src="{{ .urlBase }}/js/user.js" type="module"></script>

View File

@ -236,7 +236,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
user.GET(p+"/discord/verified/:pin", app.MyDiscordVerifiedInvite) user.GET(p+"/discord/verified/:pin", app.MyDiscordVerifiedInvite)
user.GET(p+"/telegram/verified/:pin", app.MyTelegramVerifiedInvite) user.GET(p+"/telegram/verified/:pin", app.MyTelegramVerifiedInvite)
user.POST(p+"/matrix/user", app.MatrixSendMyPIN) user.POST(p+"/matrix/user", app.MatrixSendMyPIN)
user.POST(p+"/matrix/verified/:userID/:pin", app.MatrixCheckMyPIN) user.GET(p+"/matrix/verified/:userID/:pin", app.MatrixCheckMyPIN)
user.DELETE(p+"/discord", app.UnlinkMyDiscord) user.DELETE(p+"/discord", app.UnlinkMyDiscord)
user.DELETE(p+"/telegram", app.UnlinkMyTelegram) user.DELETE(p+"/telegram", app.UnlinkMyTelegram)
user.DELETE(p+"/matrix", app.UnlinkMyMatrix) user.DELETE(p+"/matrix", app.UnlinkMyMatrix)

View File

@ -70,6 +70,9 @@ func (st *Storage) DeleteEmailsKey(k string) {
// GetDiscord returns a copy of the store. // GetDiscord returns a copy of the store.
func (st *Storage) GetDiscord() discordStore { func (st *Storage) GetDiscord() discordStore {
if st.discord == nil {
st.discord = discordStore{}
}
return st.discord return st.discord
} }
@ -82,6 +85,9 @@ func (st *Storage) GetDiscordKey(k string) (DiscordUser, bool) {
// SetDiscordKey stores value v in key k. // SetDiscordKey stores value v in key k.
func (st *Storage) SetDiscordKey(k string, v DiscordUser) { func (st *Storage) SetDiscordKey(k string, v DiscordUser) {
st.discordLock.Lock() st.discordLock.Lock()
if st.discord == nil {
st.discord = discordStore{}
}
st.discord[k] = v st.discord[k] = v
st.storeDiscordUsers() st.storeDiscordUsers()
st.discordLock.Unlock() st.discordLock.Unlock()
@ -97,6 +103,9 @@ func (st *Storage) DeleteDiscordKey(k string) {
// GetTelegram returns a copy of the store. // GetTelegram returns a copy of the store.
func (st *Storage) GetTelegram() telegramStore { func (st *Storage) GetTelegram() telegramStore {
if st.telegram == nil {
st.telegram = telegramStore{}
}
return st.telegram return st.telegram
} }
@ -109,6 +118,9 @@ func (st *Storage) GetTelegramKey(k string) (TelegramUser, bool) {
// SetTelegramKey stores value v in key k. // SetTelegramKey stores value v in key k.
func (st *Storage) SetTelegramKey(k string, v TelegramUser) { func (st *Storage) SetTelegramKey(k string, v TelegramUser) {
st.telegramLock.Lock() st.telegramLock.Lock()
if st.telegram == nil {
st.telegram = telegramStore{}
}
st.telegram[k] = v st.telegram[k] = v
st.storeTelegramUsers() st.storeTelegramUsers()
st.telegramLock.Unlock() st.telegramLock.Unlock()
@ -124,6 +136,9 @@ func (st *Storage) DeleteTelegramKey(k string) {
// GetMatrix returns a copy of the store. // GetMatrix returns a copy of the store.
func (st *Storage) GetMatrix() matrixStore { func (st *Storage) GetMatrix() matrixStore {
if st.matrix == nil {
st.matrix = matrixStore{}
}
return st.matrix return st.matrix
} }
@ -136,6 +151,9 @@ func (st *Storage) GetMatrixKey(k string) (MatrixUser, bool) {
// SetMatrixKey stores value v in key k. // SetMatrixKey stores value v in key k.
func (st *Storage) SetMatrixKey(k string, v MatrixUser) { func (st *Storage) SetMatrixKey(k string, v MatrixUser) {
st.matrixLock.Lock() st.matrixLock.Lock()
if st.matrix == nil {
st.matrix = matrixStore{}
}
st.matrix[k] = v st.matrix[k] = v
st.storeMatrixUsers() st.storeMatrixUsers()
st.matrixLock.Unlock() st.matrixLock.Unlock()
@ -163,6 +181,7 @@ type DiscordUser struct {
Discriminator string Discriminator string
Lang string Lang string
Contact bool Contact bool
JellyfinID string `json:"-"` // Used internally in discord.go
} }
type EmailAddress struct { type EmailAddress struct {

View File

@ -9,10 +9,20 @@ import (
tg "github.com/go-telegram-bot-api/telegram-bot-api" tg "github.com/go-telegram-bot-api/telegram-bot-api"
) )
const (
VERIF_TOKEN_EXPIRY_SEC = 10 * 60
)
type TelegramVerifiedToken struct { type TelegramVerifiedToken struct {
Token string ChatID int64
ChatID int64 Username string
Username string JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
}
// VerifToken stores details about a pending user verification token.
type VerifToken struct {
Expiry time.Time
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
} }
type TelegramDaemon struct { type TelegramDaemon struct {
@ -20,9 +30,9 @@ type TelegramDaemon struct {
ShutdownChannel chan string ShutdownChannel chan string
bot *tg.BotAPI bot *tg.BotAPI
username string username string
tokens []string tokens map[string]VerifToken // Map of pins to tokens.
verifiedTokens []TelegramVerifiedToken verifiedTokens map[string]TelegramVerifiedToken // Map of token pins to the responsible ChatID+Username.
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start. languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
link string link string
app *appContext app *appContext
} }
@ -40,8 +50,8 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
ShutdownChannel: make(chan string), ShutdownChannel: make(chan string),
bot: bot, bot: bot,
username: bot.Self.UserName, username: bot.Self.UserName,
tokens: []string{}, tokens: map[string]VerifToken{},
verifiedTokens: []TelegramVerifiedToken{}, verifiedTokens: map[string]TelegramVerifiedToken{},
languages: map[int64]string{}, languages: map[int64]string{},
link: "https://t.me/" + bot.Self.UserName, link: "https://t.me/" + bot.Self.UserName,
app: app, app: app,
@ -72,7 +82,15 @@ var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD". // NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
func (t *TelegramDaemon) NewAuthToken() string { func (t *TelegramDaemon) NewAuthToken() string {
pin := genAuthToken() pin := genAuthToken()
t.tokens = append(t.tokens, pin) t.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: ""}
return pin
}
// NewAssignedAuthToken generates an 8-character pin in the form "A1-2B-CD",
// and assigns it for access only with the given Jellyfin ID.
func (t *TelegramDaemon) NewAssignedAuthToken(id string) string {
pin := genAuthToken()
t.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: id}
return pin return pin
} }
@ -212,29 +230,58 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
} }
func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string) { func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string) {
tokenIndex := -1 token, ok := t.tokens[upd.Message.Text]
for i, token := range t.tokens { if !ok || time.Now().After(token.Expiry) {
if upd.Message.Text == token {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN")) err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
if err != nil { if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
} }
delete(t.tokens, upd.Message.Text)
return return
} }
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess")) err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
if err != nil { if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
} }
t.verifiedTokens = append(t.verifiedTokens, TelegramVerifiedToken{ t.verifiedTokens[upd.Message.Text] = TelegramVerifiedToken{
Token: upd.Message.Text, ChatID: upd.Message.Chat.ID,
ChatID: upd.Message.Chat.ID, Username: upd.Message.Chat.UserName,
Username: upd.Message.Chat.UserName, JellyfinID: token.JellyfinID,
}) }
t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1] delete(t.tokens, upd.Message.Text)
t.tokens = t.tokens[:len(t.tokens)-1] }
// TokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
func (t *TelegramDaemon) TokenVerified(pin string) (token TelegramVerifiedToken, ok bool) {
token, ok = t.verifiedTokens[pin]
// delete(t.verifiedTokens, pin)
return
}
// AssignedTokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
// Returns false if the given Jellyfin ID does not match the one in the token.
func (t *TelegramDaemon) AssignedTokenVerified(pin string, jfID string) (token TelegramVerifiedToken, ok bool) {
token, ok = t.verifiedTokens[pin]
if ok && token.JellyfinID != jfID {
ok = false
}
// delete(t.verifiedTokens, pin)
return
}
// UserExists returns whether or not a user with the given username exists.
func (t *TelegramDaemon) UserExists(username string) (ok bool) {
ok = false
for _, u := range t.app.storage.GetTelegram() {
if u.Username == username {
ok = true
break
}
}
return
}
// DeleteVerifiedToken removes the token with the given PIN.
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
delete(t.verifiedTokens, pin)
} }

View File

@ -176,33 +176,31 @@ let captchaInput = document.getElementById("captcha-input") as HTMLInputElement;
const captchaCheckbox = document.getElementById("captcha-success") as HTMLSpanElement; const captchaCheckbox = document.getElementById("captcha-success") as HTMLSpanElement;
let prevCaptcha = ""; let prevCaptcha = "";
function baseValidator(oncomplete: (valid: boolean) => void): void { let baseValidator = (oncomplete: (valid: boolean) => void): void => {
let captchaChecked = false; if (window.captcha && !window.reCAPTCHA && (captchaInput.value != prevCaptcha)) {
let captchaChange = false; prevCaptcha = captchaInput.value;
if (window.captcha && !window.reCAPTCHA) { _post("/captcha/verify/" + window.code + "/" + captchaID + "/" + captchaInput.value, null, (req: XMLHttpRequest) => {
captchaChange = captchaInput.value != prevCaptcha; if (req.readyState == 4) {
if (captchaChange) { if (req.status == 204) {
prevCaptcha = captchaInput.value; captchaCheckbox.innerHTML = `<i class="ri-check-line"></i>`;
_post("/captcha/verify/" + window.code + "/" + captchaID + "/" + captchaInput.value, null, (req: XMLHttpRequest) => { captchaCheckbox.classList.add("~positive");
if (req.readyState == 4) { captchaCheckbox.classList.remove("~critical");
if (req.status == 204) { captchaVerified = true;
captchaCheckbox.innerHTML = `<i class="ri-check-line"></i>`; } else {
captchaCheckbox.classList.add("~positive"); captchaCheckbox.innerHTML = `<i class="ri-close-line"></i>`;
captchaCheckbox.classList.remove("~critical"); captchaCheckbox.classList.add("~critical");
captchaVerified = true; captchaCheckbox.classList.remove("~positive");
captchaChecked = true; captchaVerified = false;
} else {
captchaCheckbox.innerHTML = `<i class="ri-close-line"></i>`;
captchaCheckbox.classList.add("~critical");
captchaCheckbox.classList.remove("~positive");
captchaVerified = false;
captchaChecked = true;
return;
}
} }
}); _baseValidator(oncomplete, captchaVerified);
} }
});
} else {
_baseValidator(oncomplete, captchaVerified);
} }
}
function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void {
if (window.emailRequired) { if (window.emailRequired) {
if (!emailField.value.includes("@")) { if (!emailField.value.includes("@")) {
oncomplete(false); oncomplete(false);
@ -221,18 +219,11 @@ function baseValidator(oncomplete: (valid: boolean) => void): void {
oncomplete(false); oncomplete(false);
return; return;
} }
if (window.captcha && !window.reCAPTCHA) { if (window.captcha && !window.reCAPTCHA && !captchaValid) {
if (!captchaChange) { oncomplete(false);
oncomplete(captchaVerified); return;
return;
}
while (!captchaChecked) {
continue;
}
oncomplete(captchaVerified);
} else {
oncomplete(true);
} }
oncomplete(true);
} }
interface GreCAPTCHA { interface GreCAPTCHA {

View File

@ -59,6 +59,7 @@ export class ServiceLinker {
protected _waiting: HTMLSpanElement; protected _waiting: HTMLSpanElement;
protected _verified = false; protected _verified = false;
protected _name: string; protected _name: string;
protected _pin: string;
get verified(): boolean { return this._verified; } get verified(): boolean { return this._verified; }
@ -76,7 +77,7 @@ export class ServiceLinker {
setTimeout(this._checkVerified, 1500); setTimeout(this._checkVerified, 1500);
return; return;
} }
_get(this._conf.verifiedURL + this._conf.pin, null, (req: XMLHttpRequest) => { _get(this._conf.verifiedURL + this._pin, null, (req: XMLHttpRequest) => {
if (req.readyState != 4) return; if (req.readyState != 4) return;
if (req.status == 401) { if (req.status == 401) {
this._conf.modal.close(); this._conf.modal.close();
@ -111,14 +112,16 @@ export class ServiceLinker {
toggleLoader(this._waiting); toggleLoader(this._waiting);
this._pinAcquired = false; this._pinAcquired = false;
this._pin = "";
if (this._conf.pin) { if (this._conf.pin) {
this._pinAcquired = true; this._pinAcquired = true;
this._conf.modal.modal.querySelector(".pin").textContent = this._conf.pin; this._pin = this._conf.pin;
this._conf.modal.modal.querySelector(".pin").textContent = this._pin;
} else if (this._conf.pinURL) { } else if (this._conf.pinURL) {
_get(this._conf.pinURL, null, (req: XMLHttpRequest) => { _get(this._conf.pinURL, null, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) { if (req.readyState == 4 && req.status == 200) {
this._conf.pin = req.response["pin"]; this._pin = req.response["pin"];
this._conf.modal.modal.querySelector(".pin").textContent = this._conf.pin; this._conf.modal.modal.querySelector(".pin").textContent = this._pin;
this._pinAcquired = true; this._pinAcquired = true;
} }
}); });
@ -240,7 +243,7 @@ export class Matrix {
this._input.value = ""; this._input.value = "";
}); });
private _verifyCode = () => _post(this._conf.verifiedURL + this._userID + "/" + this._input.value, null, (req: XMLHttpRequest) => { private _verifyCode = () => _get(this._conf.verifiedURL + this._userID + "/" + this._input.value, null, (req: XMLHttpRequest) => {
if (req.readyState != 4) return; if (req.readyState != 4) return;
removeLoader(this._submit); removeLoader(this._submit);
const valid = req.response["success"] as boolean; const valid = req.response["success"] as boolean;
@ -261,6 +264,6 @@ export class Matrix {
this._submit.classList.remove("~critical"); this._submit.classList.remove("~critical");
}, 800); }, 800);
} }
}, true); });
} }

View File

@ -48,6 +48,7 @@ window.modals = {} as Modals;
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
const grid = document.querySelector(".grid");
var rootCard = document.getElementById("card-user"); var rootCard = document.getElementById("card-user");
var contactCard = document.getElementById("card-contact"); var contactCard = document.getElementById("card-contact");
var statusCard = document.getElementById("card-status"); var statusCard = document.getElementById("card-status");
@ -174,7 +175,7 @@ class ContactMethods {
const deleteButton = row.querySelector(".user-contact-delete") as HTMLButtonElement; const deleteButton = row.querySelector(".user-contact-delete") as HTMLButtonElement;
deleteButton.onclick = () => _delete("/my/" + name, null, (req: XMLHttpRequest) => { deleteButton.onclick = () => _delete("/my/" + name, null, (req: XMLHttpRequest) => {
if (req.readyState != 4) return; if (req.readyState != 4) return;
window.location.reload(); document.dispatchEvent(new CustomEvent("details-reload"));
}); });
} }
@ -288,7 +289,7 @@ const addEditEmail = (add: boolean): void => {
toggleLoader(submit); toggleLoader(submit);
_post("/my/email", {"email": input.value}, (req: XMLHttpRequest) => { _post("/my/email", {"email": input.value}, (req: XMLHttpRequest) => {
if (req.readyState == 4 && (req.status == 303 || req.status == 200)) { if (req.readyState == 4 && (req.status == 303 || req.status == 200)) {
window.location.reload(); document.dispatchEvent(new CustomEvent("details-reload"));
} }
}, true, (req: XMLHttpRequest) => { }, true, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 401) { if (req.readyState == 4 && req.status == 401) {
@ -311,7 +312,7 @@ const discordConf: ServiceConfiguration = {
accountLinkedError: window.lang.notif("errorAccountLinked"), accountLinkedError: window.lang.notif("errorAccountLinked"),
successError: window.lang.notif("verified"), successError: window.lang.notif("verified"),
successFunc: (modalClosed: boolean) => { successFunc: (modalClosed: boolean) => {
if (modalClosed) window.location.reload(); if (modalClosed) document.dispatchEvent(new CustomEvent("details-reload"));
} }
}; };
@ -326,7 +327,7 @@ const telegramConf: ServiceConfiguration = {
accountLinkedError: window.lang.notif("errorAccountLinked"), accountLinkedError: window.lang.notif("errorAccountLinked"),
successError: window.lang.notif("verified"), successError: window.lang.notif("verified"),
successFunc: (modalClosed: boolean) => { successFunc: (modalClosed: boolean) => {
if (modalClosed) window.location.reload(); if (modalClosed) document.dispatchEvent(new CustomEvent("details-reload"));
} }
}; };
@ -341,7 +342,7 @@ const matrixConf: MatrixConfiguration = {
unknownError: window.lang.notif("errorUnknown"), unknownError: window.lang.notif("errorUnknown"),
successError: window.lang.notif("verified"), successError: window.lang.notif("verified"),
successFunc: () => { successFunc: () => {
setTimeout(() => window.location.reload(), 1200); setTimeout(() => document.dispatchEvent(new CustomEvent("details-reload")), 1200);
} }
}; };
@ -391,13 +392,13 @@ document.addEventListener("details-reload", () => {
expiryCard.expiry = details.expiry; expiryCard.expiry = details.expiry;
let messageCard = document.getElementById("card-message");
if (details.accounts_admin) { if (details.accounts_admin) {
let messageCard = document.getElementById("card-message")
if (typeof(messageCard) == "undefined" || messageCard == null) { if (typeof(messageCard) == "undefined" || messageCard == null) {
messageCard = document.createElement("div"); messageCard = document.createElement("div");
messageCard.classList.add("card", "@low", "dark:~d_neutral", "content"); messageCard.classList.add("card", "@low", "dark:~d_neutral", "content");
messageCard.id = "card-message"; messageCard.id = "card-message";
contactCard.parentElement.appendChild(messageCard); contactCard.parentElement.insertBefore(messageCard, contactCard);
} }
if (!messageCard.textContent) { if (!messageCard.textContent) {
messageCard.innerHTML = ` messageCard.innerHTML = `
@ -406,6 +407,23 @@ document.addEventListener("details-reload", () => {
`; `;
} }
} }
if (typeof(messageCard) != "undefined" && messageCard != null) {
let largestNonMessageCardHeight = 0;
const cards = grid.querySelectorAll(".card") as NodeListOf<HTMLElement>;
for (let i = 0; i < cards.length; i++) {
if (cards[i].id == "card-message") continue;
if (computeRealHeight(cards[i]) > largestNonMessageCardHeight) {
largestNonMessageCardHeight = computeRealHeight(cards[i]);
}
}
let rowSpan = Math.ceil(computeRealHeight(messageCard) / largestNonMessageCardHeight);
if (rowSpan > 0)
messageCard.style.gridRow = `span ${rowSpan}`;
}
} }
}); });
}); });
@ -413,10 +431,18 @@ document.addEventListener("details-reload", () => {
const login = new Login(window.modals.login as Modal, "/my/"); const login = new Login(window.modals.login as Modal, "/my/");
login.onLogin = () => { login.onLogin = () => {
console.log("Logged in."); console.log("Logged in.");
document.querySelector(".page-container").classList.remove("unfocused");
document.dispatchEvent(new CustomEvent("details-reload")); document.dispatchEvent(new CustomEvent("details-reload"));
}; };
const computeRealHeight = (el: HTMLElement): number => {
let children = el.children as HTMLCollectionOf<HTMLElement>;
let total = 0;
for (let i = 0; i < children.length; i++) {
total += children[i].offsetHeight;
}
return total;
}
login.bindLogout(document.getElementById("logout-button")); login.bindLogout(document.getElementById("logout-button"));