package indentalUserDB import ( "encoding/base64" "errors" "fmt" "forge.lightcrystal.systems/nilix/quartzgun/auth" "forge.lightcrystal.systems/nilix/quartzgun/cookie" "golang.org/x/crypto/bcrypt" "os" "strconv" "strings" "sync" "time" ) type IndentalUserDB struct { Users map[string]*auth.User Basis string mtx sync.RWMutex } func CreateIndentalUserDB(filePath string) *IndentalUserDB { u, err := readDB(filePath) if err == nil { return &IndentalUserDB{ Users: u, Basis: filePath, } } else { return &IndentalUserDB{ Users: map[string]*auth.User{}, Basis: filePath, } } } func (self *IndentalUserDB) InitiateSession(user string, password string, ttl int) (string, error) { self.mtx.RLock() if _, exists := self.Users[user]; !exists { self.mtx.RUnlock() return "", errors.New("User not in DB") } if bcrypt.CompareHashAndPassword([]byte(self.Users[user].Pass), []byte(password)) != nil { self.mtx.RUnlock() return "", errors.New("Incorrect password") } self.mtx.RUnlock() sessionId := cookie.GenToken(64) self.mtx.Lock() self.Users[user].Session = sessionId self.Users[user].LoginTime = time.Now() self.Users[user].LastSeen = time.Now() self.mtx.Unlock() // SetData calls Lock & Unlock and writes DB to file self.SetData(user, "token_expiry", strconv.Itoa(ttl)) return sessionId, nil } func (self *IndentalUserDB) GrantToken(user, password string, ttl int) (string, error) { self.mtx.RLock() if _, exists := self.Users[user]; !exists { self.mtx.RUnlock() return "", errors.New("User not in DB") } if bcrypt.CompareHashAndPassword([]byte(self.Users[user].Pass), []byte(password)) != nil { self.mtx.RUnlock() return "", errors.New("Incorrect password") } self.mtx.RUnlock() sessionId := cookie.GenToken(64) self.mtx.Lock() self.Users[user].Session = sessionId self.Users[user].LoginTime = time.Now() self.Users[user].LastSeen = time.Now() self.mtx.Unlock() // SetData calls Lock & Unlock and writes DB to file self.SetData(user, "token_expiry", strconv.Itoa(ttl)) return base64.StdEncoding.EncodeToString([]byte(user + "\n" + sessionId)), nil } func (self *IndentalUserDB) ValidateUser(user string, sessionId string) (bool, error) { self.mtx.RLock() if _, exists := self.Users[user]; !exists { self.mtx.RUnlock() return false, errors.New("User not in DB") } validated := self.Users[user].Session == sessionId self.mtx.RUnlock() // GetData takes RLock expiry, err3 := self.GetData(user, "token_expiry") expiryInt, err4 := strconv.ParseInt(expiry.(string), 10, 64) self.mtx.RLock() expiryTime := self.Users[user].LastSeen.Add(time.Minute * time.Duration(expiryInt)) self.mtx.RUnlock() if validated { if err3 == nil && err4 == nil && time.Now().After(expiryTime) { self.EndSession(user) return true, errors.New("Cookie or token expired") } else { self.mtx.Lock() defer self.mtx.Unlock() self.Users[user].LastSeen = time.Now() writeDB(self.Basis, self.Users) } } return validated, nil } func (self *IndentalUserDB) ValidateToken(token string) (bool, error) { data, err := base64.StdEncoding.DecodeString(token) if err == nil { parts := strings.Split(string(data), "\n") if len(parts) == 2 { return self.ValidateUser(parts[0], parts[1]) } } return false, errors.New("Token was not in a valid format: b64(USER\nSESSION)") } func (self *IndentalUserDB) ValidateTokenWithScopes(token string, scopes map[string]string) (bool, error) { data, err := base64.StdEncoding.DecodeString(token) if err == nil { parts := strings.Split(string(data), "\n") n := 0 for k, v := range scopes { s, _ := self.GetData(parts[0], k) if s.(string) == v { n++ } } validated, err2 := self.ValidateToken(token) if validated { if n == len(scopes) { return validated, nil } else { return false, errors.New("User does not have the proper scopes") } } else { return validated, err2 } } return false, err } func (self *IndentalUserDB) EndSession(user string) error { self.mtx.RLock() if _, exists := self.Users[user]; !exists { self.mtx.RUnlock() return errors.New("User not in DB") } self.mtx.RUnlock() self.mtx.Lock() defer self.mtx.Unlock() self.Users[user].Session = "" self.Users[user].LastSeen = time.Now() writeDB(self.Basis, self.Users) return nil } func (self *IndentalUserDB) DeleteUser(user string) error { self.mtx.RLock() if _, exists := self.Users[user]; !exists { self.mtx.RUnlock() return errors.New("User not in DB") } self.mtx.RUnlock() self.mtx.Lock() defer self.mtx.Unlock() delete(self.Users, user) writeDB(self.Basis, self.Users) return nil } func (self *IndentalUserDB) ChangePassword(user string, password string, oldPassword string) error { self.mtx.RLock() if _, exists := self.Users[user]; !exists { self.mtx.RUnlock() return errors.New("User not in DB") } if bcrypt.CompareHashAndPassword([]byte(self.Users[user].Pass), []byte(oldPassword)) != nil { self.mtx.RUnlock() return errors.New("Incorrect password") } self.mtx.RUnlock() hash, _ := bcrypt.GenerateFromPassword([]byte(password), 10) self.mtx.Lock() defer self.mtx.Unlock() self.Users[user].Pass = string(hash[:]) writeDB(self.Basis, self.Users) return nil } func (self *IndentalUserDB) AddUser(user string, password string) error { self.mtx.RLock() if _, exists := self.Users[user]; exists { self.mtx.RUnlock() return errors.New("User already in DB") } self.mtx.RUnlock() hash, _ := bcrypt.GenerateFromPassword([]byte(password), 10) self.mtx.Lock() defer self.mtx.Unlock() self.Users[user] = &auth.User{ Name: user, Pass: string(hash[:]), LastSeen: time.UnixMicro(0), LoginTime: time.UnixMicro(0), Session: "", Data: map[string]interface{}{}, } writeDB(self.Basis, self.Users) return nil } func (self *IndentalUserDB) GetLastLoginTime(user string) (time.Time, error) { self.mtx.RLock() self.mtx.RUnlock() if usr, exists := self.Users[user]; exists { return usr.LoginTime, nil } return time.UnixMicro(0), errors.New("User not in DB") } func (self *IndentalUserDB) GetLastTimeSeen(user string) (time.Time, error) { self.mtx.RLock() self.mtx.RUnlock() if usr, exists := self.Users[user]; exists { return usr.LastSeen, nil } return time.UnixMicro(0), errors.New("User not in DB") } func (self *IndentalUserDB) SetData(user string, key string, value interface{}) error { self.mtx.RLock() if _, exists := self.Users[user]; !exists { self.mtx.RUnlock() return errors.New("User not in DB") } self.mtx.RUnlock() self.mtx.Lock() defer self.mtx.Unlock() self.Users[user].Data[key] = value writeDB(self.Basis, self.Users) return nil } func (self *IndentalUserDB) GetData(user string, key string) (interface{}, error) { self.mtx.RLock() defer self.mtx.RUnlock() if _, usrExists := self.Users[user]; !usrExists { return nil, errors.New("User not in DB") } data, exists := self.Users[user].Data[key] if !exists { return nil, errors.New("Key not found in user data") } return data, nil } const timeFmt = "2006-01-02T15:04Z" func readDB(filePath string) (map[string]*auth.User, error) { f, err := os.ReadFile(filePath) if err != nil { return nil, err } fileData := string(f[:]) users := map[string]*auth.User{} lines := strings.Split(fileData, "\n") indentLevel := "" var name string var pass string var session string var loginTime time.Time var lastSeen time.Time var data map[string]interface{} for _, l := range lines { if strings.HasPrefix(l, indentLevel) { switch indentLevel { case "": name = l indentLevel = "\t" case "\t": if strings.Contains(l, ":") { kvp := strings.Split(l, ":") k := strings.TrimSpace(kvp[0]) v := strings.TrimSpace(strings.Join(kvp[1:], ":")) switch k { case "pass": pass = v case "session": session = v case "loginTime": loginTime, _ = time.Parse(timeFmt, v) case "lastSeen": lastSeen, _ = time.Parse(timeFmt, v) } } else { data = map[string]interface{}{} indentLevel = "\t\t" } case "\t\t": if strings.Contains(l, ":") { kvp := strings.Split(l, ":") k := strings.TrimSpace(kvp[0]) v := strings.TrimSpace(kvp[1]) data[k] = v } } } else { if indentLevel != "\t\t" { panic("Malformed indental file") } else { users[name] = &auth.User{ Name: name, Pass: pass, Session: session, LoginTime: loginTime, LastSeen: lastSeen, Data: data, } indentLevel = "" } } } return users, nil } func writeDB(filePath string, users map[string]*auth.User) error { f, err := os.Create(filePath) if err != nil { return err } defer f.Close() for _, user := range users { f.WriteString(fmt.Sprintf("%s\n\tpass: %s\n\tsession: %s\n\tloginTime: %s\n\tlastSeen: %s\n\tdata\n", user.Name, user.Pass, user.Session, user.LoginTime.UTC().Format(timeFmt), user.LastSeen.UTC().Format(timeFmt))) for k, v := range user.Data { f.WriteString(fmt.Sprintf("\t\t%s: %s\n", k, v)) } f.WriteString("\n") } f.Sync() return nil }