Add password validation
This commit is contained in:
46
.air.toml
Normal file
46
.air.toml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "tmp\\main.exe"
|
||||||
|
cmd = "go build -o ./tmp/main.exe ."
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata", "uploads"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
17
.env
Normal file
17
.env
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
SERVER_HOST=localhost
|
||||||
|
SERVER_PORT=8000
|
||||||
|
|
||||||
|
DOMAIN=filekeeper.fossy.my.id
|
||||||
|
|
||||||
|
CORS_PROTO=https
|
||||||
|
CORS_LIST=filekeeper.fossy.my.id:443,fossy.my.id:443
|
||||||
|
CORS_METHODS=POST,GET
|
||||||
|
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=test123
|
||||||
|
DB_NAME=filekeeper
|
||||||
|
|
||||||
|
SESSION_NAME=Session
|
||||||
|
SESSION_MAX_AGE=604800
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/tmp
|
||||||
|
/uploads
|
||||||
|
/public/output.css
|
||||||
|
/log
|
||||||
|
|
||||||
|
*_templ.txt
|
||||||
|
*_templ.go
|
9
Makefile
Normal file
9
Makefile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
run: generate tailwindcss
|
||||||
|
@go build -o ./tmp/main.exe
|
||||||
|
@./tmp/main.exe
|
||||||
|
|
||||||
|
generate:
|
||||||
|
@templ generate
|
||||||
|
|
||||||
|
tailwindcss:
|
||||||
|
@npx tailwindcss -i ./public/input.css -o ./public/output.css
|
39
db/database.go
Normal file
39
db/database.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
_ "gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
gormLogger "gorm.io/gorm/logger"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", utils.Getenv("DB_USERNAME"), utils.Getenv("DB_PASSWORD"), utils.Getenv("DB_HOST"), utils.Getenv("DB_PORT"), utils.Getenv("DB_NAME"))
|
||||||
|
DB, err = gorm.Open(mysql.Open(connection), &gorm.Config{}, &gorm.Config{
|
||||||
|
Logger: gormLogger.Default.LogMode(gormLogger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to connect database")
|
||||||
|
}
|
||||||
|
file, err := os.ReadFile("schema.sql")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error opening file: %v", err)
|
||||||
|
}
|
||||||
|
querys := strings.Split(string(file), "\n")
|
||||||
|
for _, query := range querys {
|
||||||
|
err := DB.Exec(query).Error
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
db/model/user/user.go
Normal file
77
db/model/user/user.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
users map[string]*types.UserWithExpired
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
var UserCache *Cache
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
|
||||||
|
UserCache = &Cache{users: make(map[string]*types.UserWithExpired)}
|
||||||
|
ticker := time.NewTicker(time.Hour * 8)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
<-ticker.C
|
||||||
|
currentTime := time.Now()
|
||||||
|
cacheClean := 0
|
||||||
|
log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second()))
|
||||||
|
|
||||||
|
UserCache.mu.Lock()
|
||||||
|
for _, user := range UserCache.users {
|
||||||
|
if currentTime.Sub(user.AccessAt) > time.Hour*8 {
|
||||||
|
delete(UserCache.users, user.Email)
|
||||||
|
cacheClean++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserCache.mu.Unlock()
|
||||||
|
|
||||||
|
log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(email string) (*types.UserWithExpired, error) {
|
||||||
|
UserCache.mu.Lock()
|
||||||
|
defer UserCache.mu.Unlock()
|
||||||
|
|
||||||
|
if user, ok := UserCache.users[email]; ok {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData types.UserWithExpired
|
||||||
|
err := db.DB.Table("users").Where("email = ?", email).First(&userData).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
UserCache.users[email] = &types.UserWithExpired{
|
||||||
|
UserID: userData.UserID,
|
||||||
|
Username: userData.Username,
|
||||||
|
Email: userData.Email,
|
||||||
|
Password: userData.Password,
|
||||||
|
AccessAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &userData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCache(email string) {
|
||||||
|
UserCache.mu.Lock()
|
||||||
|
defer UserCache.mu.Unlock()
|
||||||
|
|
||||||
|
delete(UserCache.users, email)
|
||||||
|
}
|
39
email/email.go
Normal file
39
email/email.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/gomail.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SmtpServer struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Email interface {
|
||||||
|
Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSmtpServer(Host string, Port int, User string, Password string) *SmtpServer {
|
||||||
|
return &SmtpServer{
|
||||||
|
Host: Host,
|
||||||
|
Port: Port,
|
||||||
|
User: User,
|
||||||
|
Password: Password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mail *SmtpServer) Send(dst string, subject string, body string) error {
|
||||||
|
m := gomail.NewMessage()
|
||||||
|
m.SetHeader("From", mail.User)
|
||||||
|
m.SetHeader("To", dst)
|
||||||
|
m.SetHeader("Subject", subject)
|
||||||
|
m.SetBody("text/html", body)
|
||||||
|
d := gomail.NewDialer(mail.Host, mail.Port, mail.User, mail.Password)
|
||||||
|
|
||||||
|
if err := d.DialAndSend(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
20
go.mod
Normal file
20
go.mod
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module github.com/fossyy/filekeeper
|
||||||
|
|
||||||
|
go 1.22.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/a-h/templ v0.2.648
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
golang.org/x/crypto v0.21.0
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
|
gorm.io/driver/mysql v1.5.6
|
||||||
|
gorm.io/gorm v1.25.8
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
)
|
25
go.sum
Normal file
25
go.sum
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
github.com/a-h/templ v0.2.648 h1:A1ggHGIE7AONOHrFaDTM8SrqgqHL6fWgWCijQ21Zy9I=
|
||||||
|
github.com/a-h/templ v0.2.648/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||||
|
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||||
|
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
gorm.io/gorm v1.25.8 h1:WAGEZ/aEcznN4D03laj8DKnehe1e9gYQAjW8xyPRdeo=
|
||||||
|
gorm.io/gorm v1.25.8/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
62
handler/download/download.go
Normal file
62
handler/download/download.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package downloadHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/middleware"
|
||||||
|
"github.com/fossyy/filekeeper/session"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/types/models"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
downloadView "github.com/fossyy/filekeeper/view/download"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("Session")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
|
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Error(err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storeSession, err := session.Store.Get(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, &session.SessionNotFound{}) {
|
||||||
|
storeSession.Destroy(w)
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userSession := middleware.GetUser(storeSession)
|
||||||
|
|
||||||
|
var files []models.File
|
||||||
|
db.DB.Table("files").Where("owner_id = ?", userSession.UserID).Find(&files)
|
||||||
|
var filesData []types.FileData
|
||||||
|
for i := 0; i < len(files); i++ {
|
||||||
|
filesData = append(filesData, types.FileData{
|
||||||
|
ID: files[i].ID.String(),
|
||||||
|
Name: files[i].Name,
|
||||||
|
Size: utils.ConvertFileSize(files[i].Size),
|
||||||
|
Downloaded: files[i].Downloaded,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
component := downloadView.Main("Download Page", filesData)
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
56
handler/download/file/file.go
Normal file
56
handler/download/file/file.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package downloadFileHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/types/models"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileID := r.PathValue("id")
|
||||||
|
|
||||||
|
var file models.File
|
||||||
|
err := db.DB.Table("files").Where("id = ?", fileID).First(&file).Error
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadDir := "uploads"
|
||||||
|
|
||||||
|
currentDir, _ := os.Getwd()
|
||||||
|
basePath := filepath.Join(currentDir, uploadDir)
|
||||||
|
saveFolder := filepath.Join(basePath, file.OwnerID.String(), file.ID.String())
|
||||||
|
|
||||||
|
if filepath.Dir(saveFolder) != filepath.Join(basePath, file.OwnerID.String()) {
|
||||||
|
log.Error("invalid path")
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openFile, err := os.OpenFile(filepath.Join(saveFolder, file.Name), os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
defer openFile.Close()
|
||||||
|
|
||||||
|
stat, err := openFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename="+stat.Name())
|
||||||
|
http.ServeContent(w, r, stat.Name(), stat.ModTime(), openFile)
|
||||||
|
return
|
||||||
|
}
|
23
handler/error/error.go
Normal file
23
handler/error/error.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package errorHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
errorView "github.com/fossyy/filekeeper/view/error"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ALL(w http.ResponseWriter, r *http.Request) {
|
||||||
|
component := errorView.Main("Not Found")
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
149
handler/forgotPassword/forgotPassword.go
Normal file
149
handler/forgotPassword/forgotPassword.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package forgotPasswordHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
"github.com/fossyy/filekeeper/email"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/types/models"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
emailView "github.com/fossyy/filekeeper/view/email"
|
||||||
|
forgotPasswordView "github.com/fossyy/filekeeper/view/forgotPassword"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ForgotPassword struct {
|
||||||
|
User *models.User
|
||||||
|
Code string
|
||||||
|
mu sync.Mutex
|
||||||
|
CreateTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
var mailServer *email.SmtpServer
|
||||||
|
var ListForgotPassword map[string]*ForgotPassword
|
||||||
|
var UserForgotPassword = make(map[string]string)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
ListForgotPassword = make(map[string]*ForgotPassword)
|
||||||
|
mailServer = email.NewSmtpServer("mail.fossy.my.id", 25, "test@fossy.my.id", "Test123456")
|
||||||
|
ticker := time.NewTicker(time.Minute)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
<-ticker.C
|
||||||
|
currentTime := time.Now()
|
||||||
|
cacheClean := 0
|
||||||
|
log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second()))
|
||||||
|
|
||||||
|
for _, data := range ListForgotPassword {
|
||||||
|
data.mu.Lock()
|
||||||
|
if currentTime.Sub(data.CreateTime) > time.Minute*1 {
|
||||||
|
delete(ListForgotPassword, data.User.Email)
|
||||||
|
delete(UserForgotPassword, data.Code)
|
||||||
|
cacheClean++
|
||||||
|
}
|
||||||
|
data.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
component := forgotPasswordView.Main("Forgot Password Page", types.Message{
|
||||||
|
Code: 3,
|
||||||
|
Message: "",
|
||||||
|
})
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func POST(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error parsing form", http.StatusBadRequest)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emailForm := r.Form.Get("email")
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err = db.DB.Table("users").Where("email = ?", emailForm).First(&user).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
component := forgotPasswordView.Main(fmt.Sprintf("Account with this email address %s is not found", emailForm), types.Message{
|
||||||
|
Code: 0,
|
||||||
|
Message: "",
|
||||||
|
})
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = verifyForgot(&user)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
component := forgotPasswordView.EmailSend("Forgot Password Page")
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyForgot(user *models.User) error {
|
||||||
|
var code string
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
data, ok := ListForgotPassword[user.Email]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
code = utils.GenerateRandomString(64)
|
||||||
|
} else {
|
||||||
|
code = data.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
err := emailView.ForgotPassword(user.Username, fmt.Sprintf("https://%s/forgot-password/verify/%s", utils.Getenv("DOMAIN"), code)).Render(context.Background(), &buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userData := &ForgotPassword{
|
||||||
|
User: user,
|
||||||
|
Code: code,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
UserForgotPassword[code] = user.Email
|
||||||
|
ListForgotPassword[user.Email] = userData
|
||||||
|
|
||||||
|
err = mailServer.Send(user.Email, "Password Change Request", buffer.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
109
handler/forgotPassword/verify/verify.go
Normal file
109
handler/forgotPassword/verify/verify.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package forgotPasswordVerifyHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
"github.com/fossyy/filekeeper/db/model/user"
|
||||||
|
forgotPasswordHandler "github.com/fossyy/filekeeper/handler/forgotPassword"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/session"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
forgotPasswordView "github.com/fossyy/filekeeper/view/forgotPassword"
|
||||||
|
signupView "github.com/fossyy/filekeeper/view/signup"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
code := r.PathValue("code")
|
||||||
|
|
||||||
|
email := forgotPasswordHandler.UserForgotPassword[code]
|
||||||
|
_, ok := forgotPasswordHandler.ListForgotPassword[email]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
component := forgotPasswordView.NewPasswordForm("Forgot Password Page", types.Message{
|
||||||
|
Code: 3,
|
||||||
|
Message: "",
|
||||||
|
})
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func POST(w http.ResponseWriter, r *http.Request) {
|
||||||
|
code := r.PathValue("code")
|
||||||
|
|
||||||
|
email := forgotPasswordHandler.UserForgotPassword[code]
|
||||||
|
data, ok := forgotPasswordHandler.ListForgotPassword[email]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password := r.Form.Get("password")
|
||||||
|
isValid := utils.ValidatePassword(password)
|
||||||
|
if !isValid {
|
||||||
|
component := signupView.Main("Sign up Page", types.Message{
|
||||||
|
Code: 0,
|
||||||
|
Message: "Password is invalid",
|
||||||
|
})
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashedPassword, err := utils.HashPassword(password)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.DB.Table("users").Where("email = ?", data.User.Email).Update("password", hashedPassword).Error
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(forgotPasswordHandler.ListForgotPassword, data.User.Email)
|
||||||
|
delete(forgotPasswordHandler.UserForgotPassword, data.Code)
|
||||||
|
|
||||||
|
session.RemoveAllSession(data.User.Email)
|
||||||
|
|
||||||
|
user.DeleteCache(data.User.Email)
|
||||||
|
|
||||||
|
component := forgotPasswordView.ChangeSuccess("Forgot Password Page")
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
23
handler/index/index.go
Normal file
23
handler/index/index.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package indexHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/view/index"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
component := indexView.Main("main page")
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
44
handler/logout/logout.go
Normal file
44
handler/logout/logout.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package logoutHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/session"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("Session")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSession, err := session.Store.Get(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, &session.SessionNotFound{}) {
|
||||||
|
storeSession.Destroy(w)
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Store.Delete(cookie.Value)
|
||||||
|
session.RemoveSession(storeSession.Values["user"].(types.User).Email, cookie.Value)
|
||||||
|
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: utils.Getenv("SESSION_NAME"),
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
25
handler/misc/misc.go
Normal file
25
handler/misc/misc.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package miscHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Robot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/public/robots.txt", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Favicon(w http.ResponseWriter, r *http.Request) {
|
||||||
|
//currentDir, _ := os.Getwd()
|
||||||
|
//fmt.Println(currentDir)
|
||||||
|
//logo := "../../../favicon.ico"
|
||||||
|
//basePath := filepath.Join(currentDir, "public")
|
||||||
|
//logoPath := filepath.Join(basePath, logo)
|
||||||
|
//fmt.Println(filepath.Dir(logoPath))
|
||||||
|
//if filepath.Dir(logoPath) != basePath {
|
||||||
|
// log.Print("invalid logo path", logoPath)
|
||||||
|
// w.WriteHeader(500)
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//http.ServeContent()
|
||||||
|
http.Redirect(w, r, "/public/favicon.ico", http.StatusSeeOther)
|
||||||
|
}
|
92
handler/signin/signin.go
Normal file
92
handler/signin/signin.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package signinHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/fossyy/filekeeper/db/model/user"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/session"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
signinView "github.com/fossyy/filekeeper/view/signin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
component := signinView.Main("Sign in Page", types.Message{
|
||||||
|
Code: 3,
|
||||||
|
Message: "",
|
||||||
|
})
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func POST(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error parsing form", http.StatusBadRequest)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email := r.Form.Get("email")
|
||||||
|
password := r.Form.Get("password")
|
||||||
|
userData, err := user.Get(email)
|
||||||
|
if err != nil {
|
||||||
|
component := signinView.Main("Sign in Page", types.Message{
|
||||||
|
Code: 0,
|
||||||
|
Message: "Incorrect Username or Password",
|
||||||
|
})
|
||||||
|
log.Error(err.Error())
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if email == userData.Email && utils.CheckPasswordHash(password, userData.Password) {
|
||||||
|
storeSession := session.Store.Create()
|
||||||
|
storeSession.Values["user"] = types.User{
|
||||||
|
UserID: userData.UserID,
|
||||||
|
Email: email,
|
||||||
|
Username: userData.Username,
|
||||||
|
Authenticated: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSession.Save(w)
|
||||||
|
session.AppendSession(email, storeSession)
|
||||||
|
|
||||||
|
cookie, err := r.Cookie("redirect")
|
||||||
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "redirect",
|
||||||
|
MaxAge: -1,
|
||||||
|
})
|
||||||
|
http.Redirect(w, r, cookie.Value, http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component := signinView.Main("Sign in Page", types.Message{
|
||||||
|
Code: 0,
|
||||||
|
Message: "Incorrect Username or Password",
|
||||||
|
})
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
179
handler/signup/signup.go
Normal file
179
handler/signup/signup.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package signupHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
"github.com/fossyy/filekeeper/email"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/types/models"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
emailView "github.com/fossyy/filekeeper/view/email"
|
||||||
|
signupView "github.com/fossyy/filekeeper/view/signup"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnverifiedUser struct {
|
||||||
|
User *models.User
|
||||||
|
Code string
|
||||||
|
mu sync.Mutex
|
||||||
|
CreateTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
var mailServer *email.SmtpServer
|
||||||
|
var VerifyUser map[string]*UnverifiedUser
|
||||||
|
var VerifyEmail map[string]string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
mailServer = email.NewSmtpServer("mail.fossy.my.id", 25, "test@fossy.my.id", "Test123456")
|
||||||
|
VerifyUser = make(map[string]*UnverifiedUser)
|
||||||
|
VerifyEmail = make(map[string]string)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Minute)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
<-ticker.C
|
||||||
|
currentTime := time.Now()
|
||||||
|
cacheClean := 0
|
||||||
|
log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second()))
|
||||||
|
|
||||||
|
for _, data := range VerifyUser {
|
||||||
|
data.mu.Lock()
|
||||||
|
if currentTime.Sub(data.CreateTime) > time.Minute*1 {
|
||||||
|
delete(VerifyUser, data.Code)
|
||||||
|
delete(VerifyEmail, data.User.Email)
|
||||||
|
cacheClean++
|
||||||
|
}
|
||||||
|
data.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
component := signupView.Main("Sign up Page", types.Message{
|
||||||
|
Code: 3,
|
||||||
|
Message: "",
|
||||||
|
})
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func POST(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userEmail := r.Form.Get("email")
|
||||||
|
username := r.Form.Get("username")
|
||||||
|
password := r.Form.Get("password")
|
||||||
|
isValid := utils.ValidatePassword(password)
|
||||||
|
if !isValid {
|
||||||
|
component := signupView.Main("Sign up Page", types.Message{
|
||||||
|
Code: 0,
|
||||||
|
Message: "Password is invalid",
|
||||||
|
})
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashedPassword, err := utils.HashPassword(password)
|
||||||
|
|
||||||
|
newUser := models.User{
|
||||||
|
UserID: uuid.New(),
|
||||||
|
Username: username,
|
||||||
|
Email: userEmail,
|
||||||
|
Password: hashedPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
var data models.User
|
||||||
|
err = db.DB.Table("users").Where("email = ? OR username = ?", userEmail, username).First(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = verifyEmail(&newUser)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
component := signupView.EmailSend("Sign up Page")
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component := signupView.Main("Sign up Page", types.Message{
|
||||||
|
Code: 0,
|
||||||
|
Message: "Email or Username has been registered",
|
||||||
|
})
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyEmail(user *models.User) error {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
var code string
|
||||||
|
|
||||||
|
code = VerifyEmail[user.Email]
|
||||||
|
userData, ok := VerifyUser[code]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
code = utils.GenerateRandomString(64)
|
||||||
|
} else {
|
||||||
|
code = userData.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
err := emailView.RegistrationEmail(user.Username, fmt.Sprintf("https://%s/signup/verify/%s", utils.Getenv("DOMAIN"), code)).Render(context.Background(), &buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
unverifiedUser := UnverifiedUser{
|
||||||
|
User: user,
|
||||||
|
Code: code,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
VerifyUser[code] = &unverifiedUser
|
||||||
|
VerifyEmail[user.Email] = code
|
||||||
|
|
||||||
|
err = mailServer.Send(user.Email, "Account Registration Verification", buffer.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
54
handler/signup/verify/verify.go
Normal file
54
handler/signup/verify/verify.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package signupVerifyHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
signupHandler "github.com/fossyy/filekeeper/handler/signup"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
signupView "github.com/fossyy/filekeeper/view/signup"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
code := r.PathValue("code")
|
||||||
|
data, ok := signupHandler.VerifyUser[code]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.DB.Create(&data.User).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
component := signupView.Main("Sign up Page", types.Message{
|
||||||
|
Code: 0,
|
||||||
|
Message: "Email or Username has been registered",
|
||||||
|
})
|
||||||
|
err := component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(signupHandler.VerifyUser, code)
|
||||||
|
delete(signupHandler.VerifyEmail, data.User.Email)
|
||||||
|
|
||||||
|
component := signupView.VerifySuccess("Verify page")
|
||||||
|
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
174
handler/upload/initialisation/initialisation.go
Normal file
174
handler/upload/initialisation/initialisation.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package initialisation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/middleware"
|
||||||
|
"github.com/fossyy/filekeeper/session"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/types/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func POST(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("Session")
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSession, err := session.Store.Get(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, &session.SessionNotFound{}) {
|
||||||
|
storeSession.Destroy(w)
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userSession := middleware.GetUser(storeSession)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo types.FileInfo
|
||||||
|
if err := json.Unmarshal(body, &fileInfo); err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileData, err := getFile(fileInfo.Name, userSession.UserID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
upload, err := handleNewUpload(userSession, fileInfo)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, upload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondErrorJSON(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := GetUploadInfo(fileData.ID.String())
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Done {
|
||||||
|
respondJSON(w, map[string]bool{"Done": true})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondJSON(w, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFile(name string, ownerID uuid.UUID) (models.File, error) {
|
||||||
|
var data models.File
|
||||||
|
err := db.DB.Table("files").Where("name = ? AND owner_id = ?", name, ownerID).First(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleNewUpload(user types.User, file types.FileInfo) (models.FilesUploaded, error) {
|
||||||
|
uploadDir := "uploads"
|
||||||
|
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
|
||||||
|
log.Error(err.Error())
|
||||||
|
err := os.Mkdir(uploadDir, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return models.FilesUploaded{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileID := uuid.New()
|
||||||
|
ownerID := user.UserID
|
||||||
|
|
||||||
|
currentDir, _ := os.Getwd()
|
||||||
|
basePath := filepath.Join(currentDir, uploadDir)
|
||||||
|
saveFolder := filepath.Join(basePath, ownerID.String(), fileID.String())
|
||||||
|
if filepath.Dir(saveFolder) != filepath.Join(basePath, ownerID.String()) {
|
||||||
|
return models.FilesUploaded{}, errors.New("invalid path")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.MkdirAll(saveFolder, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return models.FilesUploaded{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newFile := models.File{
|
||||||
|
ID: fileID,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
Name: file.Name,
|
||||||
|
Size: file.Size,
|
||||||
|
Downloaded: 0,
|
||||||
|
}
|
||||||
|
err = db.DB.Create(&newFile).Error
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return models.FilesUploaded{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filesUploaded := models.FilesUploaded{
|
||||||
|
UploadID: uuid.New(),
|
||||||
|
FileID: fileID,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
Name: file.Name,
|
||||||
|
Size: file.Size,
|
||||||
|
Uploaded: -1,
|
||||||
|
Done: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.DB.Create(&filesUploaded).Error
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return models.FilesUploaded{}, err
|
||||||
|
}
|
||||||
|
return filesUploaded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUploadInfo(fileID string) (*models.FilesUploaded, error) {
|
||||||
|
var data *models.FilesUploaded
|
||||||
|
err := db.DB.Table("files_uploadeds").Where("file_id = ?", fileID).First(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondJSON(w http.ResponseWriter, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondErrorJSON(w http.ResponseWriter, err error, statusCode int) {
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
respondJSON(w, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(w http.ResponseWriter, err error, statusCode int) {
|
||||||
|
http.Error(w, err.Error(), statusCode)
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
142
handler/upload/upload.go
Normal file
142
handler/upload/upload.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package uploadHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/fossyy/filekeeper/db"
|
||||||
|
"github.com/fossyy/filekeeper/handler/upload/initialisation"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/middleware"
|
||||||
|
"github.com/fossyy/filekeeper/session"
|
||||||
|
filesView "github.com/fossyy/filekeeper/view/upload"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
component := filesView.Main("upload page")
|
||||||
|
if err := component.Render(r.Context(), w); err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func POST(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileID := r.PathValue("id")
|
||||||
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := r.Cookie("Session")
|
||||||
|
if err != nil {
|
||||||
|
handleCookieError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSession, err := session.Store.Get(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, &session.SessionNotFound{}) {
|
||||||
|
storeSession.Destroy(w)
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSession := middleware.GetUser(storeSession)
|
||||||
|
|
||||||
|
if r.FormValue("done") == "true" {
|
||||||
|
finalizeFileUpload(fileID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadDir := "uploads"
|
||||||
|
if err := createUploadDirectory(uploadDir); err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := initialisation.GetUploadInfo(fileID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("error getting upload info: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDir, _ := os.Getwd()
|
||||||
|
basePath := filepath.Join(currentDir, uploadDir)
|
||||||
|
saveFolder := filepath.Join(basePath, userSession.UserID.String(), file.FileID.String())
|
||||||
|
|
||||||
|
if filepath.Dir(saveFolder) != filepath.Join(basePath, userSession.UserID.String()) {
|
||||||
|
log.Error("invalid path")
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileByte, _, err := r.FormFile("chunk")
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fileByte.Close()
|
||||||
|
|
||||||
|
dst, err := os.OpenFile(filepath.Join(saveFolder, file.Name), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
if _, err := io.Copy(dst, fileByte); err != nil {
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawIndex := r.FormValue("index")
|
||||||
|
index, err := strconv.Atoi(rawIndex)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateIndex(index, fileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeFileUpload(fileID string) {
|
||||||
|
db.DB.Table("files_uploadeds").Where("file_id = ?", fileID).Updates(map[string]interface{}{
|
||||||
|
"Done": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUploadDirectory(uploadDir string) error {
|
||||||
|
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
|
||||||
|
if err := os.Mkdir(uploadDir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIndex(index int, fileID string) {
|
||||||
|
db.DB.Table("files_uploadeds").Where("file_id = ?", fileID).Updates(map[string]interface{}{
|
||||||
|
"Uploaded": index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCookieError(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
|
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleError(w, err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(w http.ResponseWriter, err error, status int) {
|
||||||
|
http.Error(w, err.Error(), status)
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
44
handler/user/user.go
Normal file
44
handler/user/user.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package userHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/middleware"
|
||||||
|
"github.com/fossyy/filekeeper/session"
|
||||||
|
userView "github.com/fossyy/filekeeper/view/user"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("Session")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storeSession, err := session.Store.Get(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, &session.SessionNotFound{}) {
|
||||||
|
storeSession.Destroy(w)
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userSession := middleware.GetUser(storeSession)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component := userView.Main("User Page", userSession.Email, userSession.Username)
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
60
logger/logger.go
Normal file
60
logger/logger.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AggregatedLogger struct {
|
||||||
|
infoLogger *log.Logger
|
||||||
|
warnLogger *log.Logger
|
||||||
|
errorLogger *log.Logger
|
||||||
|
panicLogger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if _, err := os.Stat("log"); os.IsNotExist(err) {
|
||||||
|
os.Mkdir("log", os.ModePerm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logger() *AggregatedLogger {
|
||||||
|
currentTime := time.Now()
|
||||||
|
formattedTime := currentTime.Format("2006-01-02-15-04")
|
||||||
|
file, err := os.OpenFile(fmt.Sprintf("log/log-%s.log", formattedTime), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return &AggregatedLogger{}
|
||||||
|
}
|
||||||
|
flag := log.Ldate | log.Ltime
|
||||||
|
writer := io.MultiWriter(os.Stdout, file)
|
||||||
|
infoLogger := log.New(writer, "INFO: ", flag)
|
||||||
|
warnLogger := log.New(writer, "WARN: ", flag)
|
||||||
|
errorLogger := log.New(writer, "ERROR: ", flag)
|
||||||
|
panicLogger := log.New(writer, "PANIC: ", flag)
|
||||||
|
|
||||||
|
return &AggregatedLogger{
|
||||||
|
infoLogger: infoLogger,
|
||||||
|
warnLogger: warnLogger,
|
||||||
|
errorLogger: errorLogger,
|
||||||
|
panicLogger: panicLogger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AggregatedLogger) Info(v ...interface{}) {
|
||||||
|
l.infoLogger.Println(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AggregatedLogger) Warn(v ...interface{}) {
|
||||||
|
l.warnLogger.Println(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AggregatedLogger) Error(v ...interface{}) {
|
||||||
|
l.errorLogger.Println(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AggregatedLogger) Panic(v ...interface{}) {
|
||||||
|
l.panicLogger.Panic(v...)
|
||||||
|
}
|
23
main.go
Normal file
23
main.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/fossyy/filekeeper/middleware"
|
||||||
|
"github.com/fossyy/filekeeper/routes"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
serverAddr := fmt.Sprintf("%s:%s", utils.Getenv("SERVER_HOST"), utils.Getenv("SERVER_PORT"))
|
||||||
|
server := http.Server{
|
||||||
|
Addr: serverAddr,
|
||||||
|
Handler: middleware.Handler(routes.SetupRoutes()),
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Listening on http://%s\n", serverAddr)
|
||||||
|
err := server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
146
middleware/middleware.go
Normal file
146
middleware/middleware.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
errorHandler "github.com/fossyy/filekeeper/handler/error"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/fossyy/filekeeper/session"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logger.Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrapper struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
request *http.Request
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) WriteHeader(code int) {
|
||||||
|
if code == http.StatusNotFound {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
errorHandler.ALL(w.ResponseWriter, w.request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
w.statusCode = code
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
address := strings.Split(utils.Getenv("CORS_LIST"), ",")
|
||||||
|
|
||||||
|
for _, addr := range address {
|
||||||
|
if request.Host == addr {
|
||||||
|
writer.Header().Set("Access-Control-Allow-Origin", fmt.Sprintf("%s://%s", utils.Getenv("CORS_PROTO"), addr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedWriter := &wrapper{
|
||||||
|
ResponseWriter: writer,
|
||||||
|
request: request,
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, OPTIONS", utils.Getenv("CORS_METHODS")))
|
||||||
|
writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
next.ServeHTTP(wrappedWriter, request)
|
||||||
|
log.Info(fmt.Sprintf("%s %s %s %v \n", utils.ClientIP(request), request.Method, request.RequestURI, wrappedWriter.statusCode))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Auth(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("Session")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "redirect",
|
||||||
|
Value: r.RequestURI,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Error(err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storeSession, err := session.Store.Get(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, &session.SessionNotFound{}) {
|
||||||
|
storeSession.Destroy(w)
|
||||||
|
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Error(err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userSession := GetUser(storeSession)
|
||||||
|
if userSession.Authenticated {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "redirect",
|
||||||
|
Value: r.RequestURI,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
http.Redirect(w, r, "/signin", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Guest(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("Session")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Error(err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storeSession, err := session.Store.Get(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, &session.SessionNotFound{}) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "Session",
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1,
|
||||||
|
})
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Error(err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userSession := GetUser(storeSession)
|
||||||
|
if !userSession.Authenticated {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUser(s *session.Session) types.User {
|
||||||
|
val := s.Values["user"]
|
||||||
|
var userSession = types.User{}
|
||||||
|
userSession, ok := val.(types.User)
|
||||||
|
if !ok {
|
||||||
|
return types.User{Authenticated: false}
|
||||||
|
}
|
||||||
|
return userSession
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 943 B |
222
public/index.html
Normal file
222
public/index.html
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
<style>
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-family: 'Arimo', sans-serif;
|
||||||
|
--font-sans: 'Arimo';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Libre Franklin', sans-serif;
|
||||||
|
--font-sans: 'Libre Franklin';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
<style>
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-family: 'Arimo', sans-serif;
|
||||||
|
--font-sans: 'Arimo';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Libre Franklin', sans-serif;
|
||||||
|
--font-sans: 'Libre Franklin';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
|
||||||
|
<div class="mx-auto w-full max-w-md space-y-8">
|
||||||
|
<header class="text-center">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-3xl font-bold text-white">Forgot password</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Enter your email below to reset your password</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<form class="space-y-4" method="post" action="">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Password</label>
|
||||||
|
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="password" name="password" required />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="confirmPassword">Confirm Password</label>
|
||||||
|
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="confirmPassword" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-start mt-3 ml-4 p-1">
|
||||||
|
<ul>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="matchSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="matchSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="matchGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="matchBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="matchStatusText" class="font-medium text-sm ml-3 text-red-700"> Passwords do not match</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="lengthSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="lengthSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="lengthGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="lengthBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="lengthStatusText" class="font-medium text-sm ml-3 text-red-700"> Password length must be at least 8 characters</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="requirementsSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="requirementsSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="requirementsGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="requirementsBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="requirementsStatusText" class="font-medium text-sm ml-3 text-red-700">The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit" id="submit" name="submit" disabled>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var isMatch = false
|
||||||
|
var isValid = false
|
||||||
|
var isSecure = false
|
||||||
|
function validatePasswords() {
|
||||||
|
var password = document.getElementById('password').value;
|
||||||
|
var confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
var matchGoodPath = document.getElementById('matchGoodPath');
|
||||||
|
var matchBadPath = document.getElementById('matchBadPath');
|
||||||
|
var lengthGoodPath = document.getElementById('lengthGoodPath');
|
||||||
|
var lengthBadPath = document.getElementById('lengthBadPath');
|
||||||
|
var requirementsGoodPath = document.getElementById('requirementsGoodPath');
|
||||||
|
var requirementsBadPath = document.getElementById('requirementsBadPath');
|
||||||
|
var matchSvgContainer = document.getElementById('matchSvgContainer');
|
||||||
|
var lengthSvgContainer = document.getElementById('lengthSvgContainer');
|
||||||
|
var requirementsSvgContainer = document.getElementById('requirementsSvgContainer');
|
||||||
|
var matchStatusText = document.getElementById('matchStatusText');
|
||||||
|
var lengthStatusText = document.getElementById('lengthStatusText');
|
||||||
|
var requirementsStatusText = document.getElementById('requirementsStatusText');
|
||||||
|
var symbolRegex = /[!@#$%^&*]/;
|
||||||
|
var uppercaseRegex = /[A-Z]/;
|
||||||
|
var numberRegex = /\d/g;
|
||||||
|
|
||||||
|
|
||||||
|
if (password === confirmPassword && password.length > 0 && confirmPassword.length > 0 && password.length === confirmPassword.length) {
|
||||||
|
matchSvgContainer.classList.remove('bg-red-200');
|
||||||
|
matchSvgContainer.classList.add('bg-green-200');
|
||||||
|
matchStatusText.classList.remove('text-red-700');
|
||||||
|
matchStatusText.classList.add('text-green-700');
|
||||||
|
matchGoodPath.style.display = 'inline';
|
||||||
|
matchBadPath.style.display = 'none';
|
||||||
|
matchStatusText.textContent = "Passwords match";
|
||||||
|
console.log("anjay")
|
||||||
|
isMatch = true
|
||||||
|
} else {
|
||||||
|
matchSvgContainer.classList.remove('bg-green-200');
|
||||||
|
matchSvgContainer.classList.add('bg-red-200');
|
||||||
|
matchStatusText.classList.remove('text-green-700');
|
||||||
|
matchStatusText.classList.add('text-red-700');
|
||||||
|
matchGoodPath.style.display = 'none';
|
||||||
|
matchBadPath.style.display = 'inline';
|
||||||
|
matchStatusText.textContent = "Passwords do not match";
|
||||||
|
isMatch = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length >= 8) {
|
||||||
|
lengthSvgContainer.classList.remove('bg-red-200');
|
||||||
|
lengthSvgContainer.classList.add('bg-green-200');
|
||||||
|
lengthStatusText.classList.remove('text-red-700');
|
||||||
|
lengthStatusText.classList.add('text-green-700');
|
||||||
|
lengthGoodPath.style.display = 'inline';
|
||||||
|
lengthBadPath.style.display = 'none';
|
||||||
|
lengthStatusText.textContent = "Password length meets requirement";
|
||||||
|
isValid = true
|
||||||
|
} else {
|
||||||
|
lengthSvgContainer.classList.remove('bg-green-200');
|
||||||
|
lengthSvgContainer.classList.add('bg-red-200');
|
||||||
|
lengthStatusText.classList.remove('text-green-700');
|
||||||
|
lengthStatusText.classList.add('text-red-700');
|
||||||
|
lengthGoodPath.style.display = 'none';
|
||||||
|
lengthBadPath.style.display = 'inline';
|
||||||
|
lengthStatusText.textContent = "Password length must be at least 8 characters";
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbolCheck = symbolRegex.test(password);
|
||||||
|
var uppercaseCheck = uppercaseRegex.test(password);
|
||||||
|
var numberCount = (password.match(numberRegex) || []).length;
|
||||||
|
|
||||||
|
if (symbolCheck && uppercaseCheck && numberCount >= 3) {
|
||||||
|
requirementsSvgContainer.classList.remove('bg-red-200');
|
||||||
|
requirementsSvgContainer.classList.add('bg-green-200');
|
||||||
|
requirementsStatusText.classList.remove('text-red-700');
|
||||||
|
requirementsStatusText.classList.add('text-green-700');
|
||||||
|
requirementsGoodPath.style.display = 'inline';
|
||||||
|
requirementsBadPath.style.display = 'none';
|
||||||
|
requirementsStatusText.textContent = "Password meets additional requirements";
|
||||||
|
isSecure = true
|
||||||
|
} else {
|
||||||
|
requirementsSvgContainer.classList.remove('bg-green-200');
|
||||||
|
requirementsSvgContainer.classList.add('bg-red-200');
|
||||||
|
requirementsStatusText.classList.remove('text-green-700');
|
||||||
|
requirementsStatusText.classList.add('text-red-700');
|
||||||
|
requirementsGoodPath.style.display = 'none';
|
||||||
|
requirementsBadPath.style.display = 'inline';
|
||||||
|
requirementsStatusText.textContent = "The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers";
|
||||||
|
isSecure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(isSecure)
|
||||||
|
console.log(isValid)
|
||||||
|
console.log(isSecure)
|
||||||
|
if (isSecure && isValid && isSecure && password === confirmPassword) {
|
||||||
|
document.getElementById("submit").disabled = false;
|
||||||
|
} else {
|
||||||
|
document.getElementById("submit").disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('password').addEventListener('input', validatePasswords);
|
||||||
|
document.getElementById('confirmPassword').addEventListener('input', validatePasswords);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
3
public/input.css
Normal file
3
public/input.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
1
public/robots.txt
Normal file
1
public/robots.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
User-agent: *
|
173
public/upload.js
Normal file
173
public/upload.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
document.addEventListener("dragover", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("drop", async function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const file = event.dataTransfer.files[0]
|
||||||
|
await handleFile(file)
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('dropzone-file').addEventListener('change', async function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const file = event.target.files[0]
|
||||||
|
await handleFile(file)
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleFile(file){
|
||||||
|
const chunkSize = 2 * 1024 * 1024;
|
||||||
|
const chunks = Math.ceil(file.size / chunkSize);
|
||||||
|
const data = JSON.stringify({
|
||||||
|
"name": file.name,
|
||||||
|
"size": file.size,
|
||||||
|
"chunk": chunks,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('/upload/init', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}).then(async response => {
|
||||||
|
const responseData = await response.json()
|
||||||
|
console.log(responseData)
|
||||||
|
if (responseData.Done === false) {
|
||||||
|
addNewUploadElement(file)
|
||||||
|
const fileChunks = await splitFile(file, chunkSize);
|
||||||
|
await uploadChunks(file.name,file.size, fileChunks, responseData.Uploaded, responseData.FileID);
|
||||||
|
} else {
|
||||||
|
alert("file already uploaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error uploading file:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewUploadElement(file){
|
||||||
|
const newDiv = document.createElement('div');
|
||||||
|
newDiv.innerHTML = `
|
||||||
|
<div class="p-6 rounded-lg shadow bg-gray-800 border-gray-700">
|
||||||
|
<div class="mb-2 flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-x-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
|
||||||
|
<path fill="#90CAF9" d="M40 45L8 45 8 3 30 3 40 13z"></path>
|
||||||
|
<path fill="#E1F5FE" d="M38.5 14L29 14 29 4.5z"></path>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-white">${ file.name }</p>
|
||||||
|
<p class="text-xs text-gray-500">${ convertFileSize(file.size) }</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline-flex items-center gap-x-2">
|
||||||
|
<a class="text-gray-500 hover:text-gray-800" href="#">
|
||||||
|
<svg class="flex-shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||||
|
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round">
|
||||||
|
<rect width="4" height="16" x="6" y="4" />
|
||||||
|
<rect width="4" height="16" x="14" y="4" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a class="text-gray-500 hover:text-gray-800" href="#">
|
||||||
|
<svg class="flex-shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||||
|
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round">
|
||||||
|
<path d="M3 6h18" />
|
||||||
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||||
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||||
|
<line x1="10" x2="10" y1="11" y2="17" />
|
||||||
|
<line x1="14" x2="14" y1="11" y2="17" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-x-3 whitespace-nowrap">
|
||||||
|
<div id="progress-${ file.name }-1" class="flex w-full h-2 rounded-full overflow-hidden bg-gray-200"
|
||||||
|
role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
|
||||||
|
<div id="progress-${ file.name }-2"
|
||||||
|
class="flex flex-col justify-center rounded-full overflow-hidden bg-teal-500 text-xs text-white text-center whitespace-nowrap transition duration-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<span id="progress-${ file.name }-3" class="text-sm text-white ">Starting...</span>
|
||||||
|
</div>
|
||||||
|
<div id="progress-${ file.name }-4" class="text-sm text-gray-500">Uploading 0%</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('container').appendChild(newDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertFileSize(sizeInBytes) {
|
||||||
|
if (sizeInBytes < 1024) {
|
||||||
|
return sizeInBytes + ' B';
|
||||||
|
} else if (sizeInBytes < 1024 * 1024) {
|
||||||
|
return (sizeInBytes / 1024).toFixed(2) + ' KB';
|
||||||
|
} else if (sizeInBytes < 1024 * 1024 * 1024) {
|
||||||
|
return (sizeInBytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||||
|
} else {
|
||||||
|
return (sizeInBytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function splitFile(file, chunkSize) {
|
||||||
|
const fileSize = file.size;
|
||||||
|
const chunks = Math.ceil(fileSize / chunkSize);
|
||||||
|
const fileChunks = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks; i++) {
|
||||||
|
const start = i * chunkSize;
|
||||||
|
const end = Math.min(fileSize, start + chunkSize);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
fileChunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadChunks(name, size, chunks, uploadedChunk= -1, FileID) {
|
||||||
|
let byteUploaded = 0
|
||||||
|
var progress1 = document.getElementById(`progress-${name}-1`);
|
||||||
|
var progress2 = document.getElementById(`progress-${name}-2`);
|
||||||
|
var progress3 = document.getElementById(`progress-${name}-3`);
|
||||||
|
var progress4 = document.getElementById(`progress-${name}-4`);
|
||||||
|
for (let index = 0; index < chunks.length; index++) {
|
||||||
|
const percentComplete = Math.round((index + 1) / chunks.length * 100);
|
||||||
|
const chunk = chunks[index];
|
||||||
|
if (!(index <= uploadedChunk)) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('chunk', chunk);
|
||||||
|
formData.append('index', index);
|
||||||
|
formData.append('done', false);
|
||||||
|
|
||||||
|
progress1.setAttribute("aria-valuenow", percentComplete);
|
||||||
|
progress2.style.width = `${percentComplete}%`;
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
await fetch(`/upload/${FileID}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const totalTime = (endTime - startTime) / 1000;
|
||||||
|
const uploadSpeed = chunk.size / totalTime / 1024 / 1024;
|
||||||
|
byteUploaded += chunk.size
|
||||||
|
progress3.innerText = `${uploadSpeed.toFixed(2)} MB/s`;
|
||||||
|
progress4.innerText = `Uploading ${percentComplete}% - ${convertFileSize(byteUploaded)} of ${ convertFileSize(size)}`;
|
||||||
|
} else {
|
||||||
|
progress1.setAttribute("aria-valuenow", percentComplete);
|
||||||
|
progress2.style.width = `${percentComplete}%`;
|
||||||
|
byteUploaded += chunk.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('done', true);
|
||||||
|
return fetch(`/upload/${FileID}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
97
public/validatePassword.js
Normal file
97
public/validatePassword.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
var isMatch = false
|
||||||
|
var isValid = false
|
||||||
|
var isSecure = false
|
||||||
|
function validatePasswords() {
|
||||||
|
var password = document.getElementById('password').value;
|
||||||
|
var confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
var matchGoodPath = document.getElementById('matchGoodPath');
|
||||||
|
var matchBadPath = document.getElementById('matchBadPath');
|
||||||
|
var lengthGoodPath = document.getElementById('lengthGoodPath');
|
||||||
|
var lengthBadPath = document.getElementById('lengthBadPath');
|
||||||
|
var requirementsGoodPath = document.getElementById('requirementsGoodPath');
|
||||||
|
var requirementsBadPath = document.getElementById('requirementsBadPath');
|
||||||
|
var matchSvgContainer = document.getElementById('matchSvgContainer');
|
||||||
|
var lengthSvgContainer = document.getElementById('lengthSvgContainer');
|
||||||
|
var requirementsSvgContainer = document.getElementById('requirementsSvgContainer');
|
||||||
|
var matchStatusText = document.getElementById('matchStatusText');
|
||||||
|
var lengthStatusText = document.getElementById('lengthStatusText');
|
||||||
|
var requirementsStatusText = document.getElementById('requirementsStatusText');
|
||||||
|
var symbolRegex = /[!@#$%^&*]/;
|
||||||
|
var uppercaseRegex = /[A-Z]/;
|
||||||
|
var numberRegex = /\d/g;
|
||||||
|
|
||||||
|
|
||||||
|
if (password === confirmPassword && password.length > 0 && confirmPassword.length > 0 && password.length === confirmPassword.length) {
|
||||||
|
matchSvgContainer.classList.remove('bg-red-200');
|
||||||
|
matchSvgContainer.classList.add('bg-green-200');
|
||||||
|
matchStatusText.classList.remove('text-red-700');
|
||||||
|
matchStatusText.classList.add('text-green-700');
|
||||||
|
matchGoodPath.style.display = 'inline';
|
||||||
|
matchBadPath.style.display = 'none';
|
||||||
|
matchStatusText.textContent = "Passwords match";
|
||||||
|
console.log("anjay")
|
||||||
|
isMatch = true
|
||||||
|
} else {
|
||||||
|
matchSvgContainer.classList.remove('bg-green-200');
|
||||||
|
matchSvgContainer.classList.add('bg-red-200');
|
||||||
|
matchStatusText.classList.remove('text-green-700');
|
||||||
|
matchStatusText.classList.add('text-red-700');
|
||||||
|
matchGoodPath.style.display = 'none';
|
||||||
|
matchBadPath.style.display = 'inline';
|
||||||
|
matchStatusText.textContent = "Passwords do not match";
|
||||||
|
isMatch = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length >= 8) {
|
||||||
|
lengthSvgContainer.classList.remove('bg-red-200');
|
||||||
|
lengthSvgContainer.classList.add('bg-green-200');
|
||||||
|
lengthStatusText.classList.remove('text-red-700');
|
||||||
|
lengthStatusText.classList.add('text-green-700');
|
||||||
|
lengthGoodPath.style.display = 'inline';
|
||||||
|
lengthBadPath.style.display = 'none';
|
||||||
|
lengthStatusText.textContent = "Password length meets requirement";
|
||||||
|
isValid = true
|
||||||
|
} else {
|
||||||
|
lengthSvgContainer.classList.remove('bg-green-200');
|
||||||
|
lengthSvgContainer.classList.add('bg-red-200');
|
||||||
|
lengthStatusText.classList.remove('text-green-700');
|
||||||
|
lengthStatusText.classList.add('text-red-700');
|
||||||
|
lengthGoodPath.style.display = 'none';
|
||||||
|
lengthBadPath.style.display = 'inline';
|
||||||
|
lengthStatusText.textContent = "Password length must be at least 8 characters";
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbolCheck = symbolRegex.test(password);
|
||||||
|
var uppercaseCheck = uppercaseRegex.test(password);
|
||||||
|
var numberCount = (password.match(numberRegex) || []).length;
|
||||||
|
|
||||||
|
if (symbolCheck && uppercaseCheck && numberCount >= 3) {
|
||||||
|
requirementsSvgContainer.classList.remove('bg-red-200');
|
||||||
|
requirementsSvgContainer.classList.add('bg-green-200');
|
||||||
|
requirementsStatusText.classList.remove('text-red-700');
|
||||||
|
requirementsStatusText.classList.add('text-green-700');
|
||||||
|
requirementsGoodPath.style.display = 'inline';
|
||||||
|
requirementsBadPath.style.display = 'none';
|
||||||
|
requirementsStatusText.textContent = "Password meets additional requirements";
|
||||||
|
isSecure = true
|
||||||
|
} else {
|
||||||
|
requirementsSvgContainer.classList.remove('bg-green-200');
|
||||||
|
requirementsSvgContainer.classList.add('bg-red-200');
|
||||||
|
requirementsStatusText.classList.remove('text-green-700');
|
||||||
|
requirementsStatusText.classList.add('text-red-700');
|
||||||
|
requirementsGoodPath.style.display = 'none';
|
||||||
|
requirementsBadPath.style.display = 'inline';
|
||||||
|
requirementsStatusText.textContent = "The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers";
|
||||||
|
isSecure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSecure && isValid && isSecure && password === confirmPassword) {
|
||||||
|
document.getElementById("submit").disabled = false;
|
||||||
|
} else {
|
||||||
|
document.getElementById("submit").disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('password').addEventListener('input', validatePasswords);
|
||||||
|
document.getElementById('confirmPassword').addEventListener('input', validatePasswords);
|
166
routes/routes.go
Normal file
166
routes/routes.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
downloadHandler "github.com/fossyy/filekeeper/handler/download"
|
||||||
|
downloadFileHandler "github.com/fossyy/filekeeper/handler/download/file"
|
||||||
|
forgotPasswordHandler "github.com/fossyy/filekeeper/handler/forgotPassword"
|
||||||
|
forgotPasswordVerifyHandler "github.com/fossyy/filekeeper/handler/forgotPassword/verify"
|
||||||
|
indexHandler "github.com/fossyy/filekeeper/handler/index"
|
||||||
|
logoutHandler "github.com/fossyy/filekeeper/handler/logout"
|
||||||
|
miscHandler "github.com/fossyy/filekeeper/handler/misc"
|
||||||
|
signinHandler "github.com/fossyy/filekeeper/handler/signin"
|
||||||
|
signupHandler "github.com/fossyy/filekeeper/handler/signup"
|
||||||
|
signupVerifyHandler "github.com/fossyy/filekeeper/handler/signup/verify"
|
||||||
|
uploadHandler "github.com/fossyy/filekeeper/handler/upload"
|
||||||
|
"github.com/fossyy/filekeeper/handler/upload/initialisation"
|
||||||
|
userHandler "github.com/fossyy/filekeeper/handler/user"
|
||||||
|
"github.com/fossyy/filekeeper/middleware"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes() *http.ServeMux {
|
||||||
|
handler := http.NewServeMux()
|
||||||
|
|
||||||
|
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.RequestURI {
|
||||||
|
case "/":
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
indexHandler.GET(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.HandleFunc("/signin", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
middleware.Guest(signinHandler.GET, w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
middleware.Guest(signinHandler.POST, w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
signupRouter := http.NewServeMux()
|
||||||
|
handler.Handle("/signup/", http.StripPrefix("/signup", signupRouter))
|
||||||
|
|
||||||
|
signupRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
middleware.Guest(signupHandler.GET, w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
middleware.Guest(signupHandler.POST, w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
signupRouter.HandleFunc("/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
middleware.Guest(signupVerifyHandler.GET, w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
forgotPasswordRouter := http.NewServeMux()
|
||||||
|
handler.Handle("/forgot-password/", http.StripPrefix("/forgot-password", forgotPasswordRouter))
|
||||||
|
forgotPasswordRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
middleware.Guest(forgotPasswordHandler.GET, w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
middleware.Guest(forgotPasswordHandler.POST, w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
forgotPasswordRouter.HandleFunc("/verify/{code}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
middleware.Guest(forgotPasswordVerifyHandler.GET, w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
middleware.Guest(forgotPasswordVerifyHandler.POST, w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
middleware.Auth(userHandler.GET, w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Upload router
|
||||||
|
uploadRouter := http.NewServeMux()
|
||||||
|
handler.Handle("/upload/", http.StripPrefix("/upload", uploadRouter))
|
||||||
|
|
||||||
|
uploadRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
middleware.Auth(uploadHandler.GET, w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadRouter.HandleFunc("/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodPost:
|
||||||
|
middleware.Auth(uploadHandler.POST, w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadRouter.HandleFunc("/init", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodPost:
|
||||||
|
middleware.Auth(initialisation.POST, w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Download router
|
||||||
|
downloadRouter := http.NewServeMux()
|
||||||
|
handler.Handle("/download/", http.StripPrefix("/download", downloadRouter))
|
||||||
|
downloadRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
middleware.Auth(downloadHandler.GET, w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
downloadRouter.HandleFunc("/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
downloadFileHandler.GET(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
middleware.Auth(logoutHandler.GET, w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
miscHandler.Robot(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/public/favicon.ico", http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
|
||||||
|
fileServer := http.FileServer(http.Dir("./public"))
|
||||||
|
handler.Handle("/public/", http.StripPrefix("/public", fileServer))
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
3
schema.sql
Normal file
3
schema.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (user_id VARCHAR(255) PRIMARY KEY NOT NULL,username VARCHAR(255) UNIQUE NOT NULL,email VARCHAR(255) UNIQUE NOT NULL,password TEXT NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS files (id VARCHAR(255) PRIMARY KEY NOT NULL,owner_id VARCHAR(255) NOT NULL,name TEXT NOT NULL,size BIGINT NOT NULL,downloaded BIGINT NOT NULL,FOREIGN KEY (owner_id) REFERENCES users(user_id));
|
||||||
|
CREATE TABLE IF NOT EXISTS files_uploadeds (upload_id VARCHAR(255) PRIMARY KEY NOT NULL,file_id VARCHAR(255) NOT NULL,owner_id VARCHAR(255) NOT NULL,name TEXT NOT NULL,size INT NOT NULL,uploaded INT NOT NULL DEFAULT 0,done BOOLEAN NOT NULL DEFAULT FALSE,FOREIGN KEY (file_id) REFERENCES files(id),FOREIGN KEY (owner_id) REFERENCES users(user_id));
|
100
session/session.go
Normal file
100
session/session.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/utils"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID string
|
||||||
|
Values map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreSession struct {
|
||||||
|
Sessions map[string]*Session
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var Store = StoreSession{Sessions: make(map[string]*Session)}
|
||||||
|
var userSessions = make(map[string][]string)
|
||||||
|
|
||||||
|
type SessionNotFound struct{}
|
||||||
|
|
||||||
|
func (e *SessionNotFound) Error() string {
|
||||||
|
return "session not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSession) Get(id string) (*Session, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if session, ok := s.Sessions[id]; ok {
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
return nil, &SessionNotFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSession) Create() *Session {
|
||||||
|
id := utils.GenerateRandomString(128)
|
||||||
|
session := &Session{
|
||||||
|
ID: id,
|
||||||
|
Values: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
s.Sessions[id] = session
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSession) Delete(id string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
delete(s.Sessions, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Save(w http.ResponseWriter) {
|
||||||
|
maxAge, _ := strconv.Atoi(utils.Getenv("SESSION_MAX_AGE"))
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: utils.Getenv("SESSION_NAME"),
|
||||||
|
Value: s.ID,
|
||||||
|
MaxAge: maxAge,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Destroy(w http.ResponseWriter) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: utils.Getenv("SESSION_NAME"),
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendSession(email string, session *Session) {
|
||||||
|
userSessions[email] = append(userSessions[email], session.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveSession(email string, id string) {
|
||||||
|
sessions := userSessions[email]
|
||||||
|
var updatedSessions []string
|
||||||
|
for _, userSession := range sessions {
|
||||||
|
if userSession != id {
|
||||||
|
updatedSessions = append(updatedSessions, userSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(updatedSessions) > 0 {
|
||||||
|
userSessions[email] = updatedSessions
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(userSessions, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveAllSession(email string) {
|
||||||
|
sessions := userSessions[email]
|
||||||
|
for _, session := range sessions {
|
||||||
|
delete(Store.Sessions, session)
|
||||||
|
}
|
||||||
|
delete(userSessions, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Getses() map[string][]string {
|
||||||
|
return userSessions
|
||||||
|
}
|
10
staging.bat
Normal file
10
staging.bat
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
REM Start the Go server using Air
|
||||||
|
start "" air
|
||||||
|
|
||||||
|
REM Watch for changes in Tailwind CSS
|
||||||
|
start "" npx tailwindcss -i ./public/input.css -o ./public/output.css --watch
|
||||||
|
|
||||||
|
REM Watch for changes in templates and proxy to Go server
|
||||||
|
start "" cmd /k "templ generate -watch -proxy=http://localhost:8000"
|
5
staging.sh
Normal file
5
staging.sh
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
air &
|
||||||
|
npx tailwindcss -i ./public/input.css -o ./public/output.css --watch &
|
||||||
|
templ generate -watch -proxy=http://localhost:8000
|
9
tailwind.config.js
Normal file
9
tailwind.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./view/**/*.{html,js,templ}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
28
types/models/models.go
Normal file
28
types/models/models.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
UserID uuid.UUID `gorm:"primaryKey;not null;unique"`
|
||||||
|
Username string `gorm:"unique;not null"`
|
||||||
|
Email string `gorm:"unique;not null"`
|
||||||
|
Password string `gorm:"not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
ID uuid.UUID `gorm:"primaryKey;not null;unique"`
|
||||||
|
OwnerID uuid.UUID `gorm:"not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
Size int `gorm:"not null"`
|
||||||
|
Downloaded int `gorm:"not null;default=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesUploaded struct {
|
||||||
|
UploadID uuid.UUID `gorm:"primaryKey;not null;unique"`
|
||||||
|
FileID uuid.UUID `gorm:"not null"`
|
||||||
|
OwnerID uuid.UUID `gorm:"not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
Size int `gorm:"not null"`
|
||||||
|
Uploaded int `gorm:"not null;default=0"`
|
||||||
|
Done bool `gorm:"not null;default=false"`
|
||||||
|
}
|
46
types/types.go
Normal file
46
types/types.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
Email string
|
||||||
|
Username string
|
||||||
|
Authenticated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserWithExpired struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
AccessAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Chunk int `json:"chunk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileInfoUploaded struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Chunk int `json:"chunk"`
|
||||||
|
UploadedChunk int `json:"uploaded_chunk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileData struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Size string
|
||||||
|
Downloaded int
|
||||||
|
}
|
142
utils/utils.go
Normal file
142
utils/utils.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/fossyy/filekeeper/logger"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Env struct {
|
||||||
|
value map[string]string
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var env *Env
|
||||||
|
var log *logger.AggregatedLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
env = &Env{value: map[string]string{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientIP(request *http.Request) string {
|
||||||
|
ip := request.Header.Get("X-Real-IP")
|
||||||
|
if ip == "" {
|
||||||
|
ip = request.Header.Get("X-Forwarded-For")
|
||||||
|
if ip == "" {
|
||||||
|
ip = request.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(ip, ",") {
|
||||||
|
ips := strings.Split(ip, ",")
|
||||||
|
ip = strings.TrimSpace(ips[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
ips := strings.Split(ip, ":")
|
||||||
|
ip = ips[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPasswordHash(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidatePassword(password string) bool {
|
||||||
|
if len(password) < 6 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasSymbol bool
|
||||||
|
hasNumber int
|
||||||
|
hasUppercase bool
|
||||||
|
)
|
||||||
|
|
||||||
|
symbols := []string{"!", "@", "#", "$", "%", "^", "&", "*"}
|
||||||
|
|
||||||
|
for _, symbol := range symbols {
|
||||||
|
if strings.Contains(password, symbol) {
|
||||||
|
hasSymbol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, char := range password {
|
||||||
|
switch {
|
||||||
|
case unicode.IsNumber(char):
|
||||||
|
hasNumber++
|
||||||
|
case unicode.IsUpper(char):
|
||||||
|
hasUppercase = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasSymbol && hasNumber >= 3 && hasUppercase
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertFileSize(byte int) string {
|
||||||
|
if byte < 1024 {
|
||||||
|
return fmt.Sprintf("%d B", byte)
|
||||||
|
} else if byte < 1024*1024 {
|
||||||
|
return fmt.Sprintf("%d KB", byte/1024)
|
||||||
|
} else if byte < 1024*1024*1024 {
|
||||||
|
return fmt.Sprintf("%d MB", byte/(1024*1024))
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%d GB", byte/(1024*1024*1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Getenv(key string) string {
|
||||||
|
env.mu.Lock()
|
||||||
|
defer env.mu.Unlock()
|
||||||
|
|
||||||
|
if val, ok := env.value[key]; ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
err := godotenv.Load(".env")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error loading .env file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val := os.Getenv(key)
|
||||||
|
env.value[key] = val
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateRandomString(length int) string {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
var result strings.Builder
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
randomIndex := seededRand.Intn(len(charset))
|
||||||
|
result.WriteString(string(charset[randomIndex]))
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeFilename(filename string) string {
|
||||||
|
invalidChars := []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"}
|
||||||
|
|
||||||
|
for _, char := range invalidChars {
|
||||||
|
filename = strings.ReplaceAll(filename, char, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename
|
||||||
|
}
|
76
view/download/download.templ
Normal file
76
view/download/download.templ
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package downloadView
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/view/layout"
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ component(title string, files []types.FileData){
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="dark min-h-screen p-4 sm:p-6 bg-gray-900 text-white">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<header class="text-center">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-3xl font-bold tracking-tighter sm:text-4xl">Download Files</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="mx-auto grid w-full max-w-3xl gap-4 px-4">
|
||||||
|
for _, file := range files {
|
||||||
|
<div class="rounded-lg border bg-card text-card-foreground shadow-sm">
|
||||||
|
<div class="flex space-y-4 flex-col p-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h2 class="text-lg font-bold tracking-wide">{ file.Name }</h2>
|
||||||
|
<p class="text-sm leading-none"> { file.Size }</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<button class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 rounded-md px-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-4 w-4"
|
||||||
|
>
|
||||||
|
<path d="M4 13.5V4a2 2 0 0 1 2-2h8.5L20 7.5V20a2 2 0 0 1-2 2h-5.5"></path>
|
||||||
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
|
<path d="M10.42 12.61a2.1 2.1 0 1 1 2.97 2.97L7.95 21 4 22l.99-3.95 5.43-5.44Z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Edit</span>
|
||||||
|
</button>
|
||||||
|
<a href={ templ.SafeURL("/download/" + file.ID) } class="inline-flex items-center justify-center p-5 text-base font-medium text-gray-500 rounded-lg bg-gray-50 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-4 w-4"
|
||||||
|
>
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" x2="12" y1="15" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Main(title string, files []types.FileData){
|
||||||
|
@component(title, files)
|
||||||
|
}
|
111
view/email/email.templ
Normal file
111
view/email/email.templ
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package emailView
|
||||||
|
|
||||||
|
templ RegistrationEmail(name string, link string) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Email Verification</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Email Verification</h1>
|
||||||
|
<p>Dear {name},</p>
|
||||||
|
<p>Please verify your email address by clicking the button below:</p>
|
||||||
|
<a href={ templ.SafeURL(link)} class="button">Verify Email</a>
|
||||||
|
<p>Or copy and paste this URL into a new tab of your browser: <a href={ templ.SafeURL(link)}><br/>{link}</a></p>
|
||||||
|
<p>If you did not request this verification, please disregard this email.</p>
|
||||||
|
<p>Thank you, <br/> The Filekeeper Team</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ ForgotPassword(name string, link string) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Email Verification</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Password Change Request</h1>
|
||||||
|
<p>Dear {name},</p>
|
||||||
|
<p>Please verify your password change request by clicking the button below:</p>
|
||||||
|
<a href={ templ.SafeURL(link)} class="button">Verify Password Change</a>
|
||||||
|
<p>Or copy and paste this URL into a new tab of your browser: <a href={ templ.SafeURL(link)}><br/>{link}</a></p>
|
||||||
|
<p>If you did not request this password change, please disregard this email.</p>
|
||||||
|
<p>Thank you, <br/> The Filekeeper Team</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
39
view/error/error.templ
Normal file
39
view/error/error.templ
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package errorView
|
||||||
|
|
||||||
|
import "github.com/fossyy/filekeeper/view/layout"
|
||||||
|
|
||||||
|
templ content(title string){
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="flex flex-col items-center justify-center w-full min-h-[calc(100vh-1rem)] py-10 text-center gap-4 md:gap-8">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl">404 Not Found</h1>
|
||||||
|
<p class="max-w-[600px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
|
||||||
|
The page you are looking for does not exist. It might have been moved or deleted.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="inline-flex h-10 items-center rounded-md border border-gray-200 border-gray-200 bg-white px-8 text-sm font-medium shadow-sm gap-2 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 dark:border-gray-800 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:focus-visible:ring-gray-300"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="m9 18 6-6-6-6"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Main(title string) {
|
||||||
|
@content(title)
|
||||||
|
}
|
169
view/forgotPassword/forgotPassword.templ
Normal file
169
view/forgotPassword/forgotPassword.templ
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package forgotPasswordView
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/view/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ content(title string, err types.Message) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
|
||||||
|
<div class="mx-auto w-full max-w-md space-y-8">
|
||||||
|
<header class="text-center">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-3xl font-bold text-white">Forgot password</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Enter your email below to reset your password</p>
|
||||||
|
switch err.Code {
|
||||||
|
case 0:
|
||||||
|
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<span class="font-medium">Danger alert!</span> {err.Message}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<form class="space-y-4" method="post" action="">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="email">Email</label>
|
||||||
|
<input type="email" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="email" name="email" placeholder="m@example.com" required="" />
|
||||||
|
</div>
|
||||||
|
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Main(title string, err types.Message) {
|
||||||
|
@content(title, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
templ NewPasswordForm(title string, err types.Message) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
|
||||||
|
<div class="mx-auto w-full max-w-md space-y-8">
|
||||||
|
<header class="text-center">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-3xl font-bold text-white">Forgot password</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Enter your email below to reset your password</p>
|
||||||
|
switch err.Code {
|
||||||
|
case 0:
|
||||||
|
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<span class="font-medium">Danger alert!</span> {err.Message}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<form class="space-y-4" method="post" action="">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Password</label>
|
||||||
|
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="password" name="password" required />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="confirmPassword">Confirm Password</label>
|
||||||
|
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="confirmPassword" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-start mt-3 ml-4 p-1">
|
||||||
|
<ul>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="matchSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="matchSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="matchGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="matchBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="matchStatusText" class="font-medium text-sm ml-3 text-red-700"> Passwords do not match</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="lengthSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="lengthSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="lengthGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="lengthBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="lengthStatusText" class="font-medium text-sm ml-3 text-red-700"> Password length must be at least 8 characters</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="requirementsSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="requirementsSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="requirementsGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="requirementsBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="requirementsStatusText" class="font-medium text-sm ml-3 text-red-700">The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit" id="submit" name="submit" disabled>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/public/validatePassword.js" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ EmailSend(title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<style>h1, h2, h3, h4, h5, h6 { font-family: 'Arimo', sans-serif; --font-sans: 'Arimo'; }</style>
|
||||||
|
<style>body { font-family: 'Libre Franklin', sans-serif; --font-sans: 'Libre Franklin'; }</style>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-[80vh] gap-6">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-16 w-16 text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
|
||||||
|
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 text-center">
|
||||||
|
<h1 class="text-3xl font-bold">Email Verification Sent</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
We've sent a verification email to your inbox. Please check your email and follow the instructions to change your password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ ChangeSuccess(title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<style>h1, h2, h3, h4, h5, h6 { font-family: 'Arimo', sans-serif; --font-sans: 'Arimo'; }</style>
|
||||||
|
<style>body { font-family: 'Libre Franklin', sans-serif; --font-sans: 'Libre Franklin'; }</style>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-[80vh] gap-6">
|
||||||
|
<div class="bg-green-500 text-white rounded-full p-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-8 w-8"
|
||||||
|
>
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 text-center">
|
||||||
|
<h1 class="text-3xl font-bold">Password Changed Successfully</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
Your password has been successfully updated. Feel free to continue enjoying our platform.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
103
view/index/index.templ
Normal file
103
view/index/index.templ
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package indexView
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/view/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ content(title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="bg-white">
|
||||||
|
<nav class="container mx-auto px-6 py-4 flex justify-between items-center">
|
||||||
|
<a href="#" class="text-blue-500 text-xl font-semibold">
|
||||||
|
C.
|
||||||
|
</a>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<a href="/signup" class="text-gray-600 hover:text-gray-800">
|
||||||
|
Sign up
|
||||||
|
</a>
|
||||||
|
<a href="/signin" class="text-gray-600 hover:text-gray-800">
|
||||||
|
Sign in
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<header class="container mx-auto px-6 py-16 text-center">
|
||||||
|
<h1 class="text-5xl font-bold text-gray-900 mb-2">Your files, always within reach.</h1>
|
||||||
|
<p class="text-gray-700 text-lg mb-8">
|
||||||
|
Store, access, and share your files from anywhere. We offer secure and reliable file storage, so you can
|
||||||
|
focus on what matters most.
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-center items-center space-x-4">
|
||||||
|
<div class="sm:flex sm:justify-center lg:justify-start">
|
||||||
|
<div class="rounded-md shadow">
|
||||||
|
<a class="w-full flex items-center justify-center px-8 py-3 text-base leading-6 font-medium rounded-md text-white bg-pink-400 hover:bg-pink-500 hover:text-white focus:ring ring-offset-2 ring-pink-400 focus:outline-none transition duration-150 ease-in-out md:py-4 md:text-lg md:px-10"
|
||||||
|
href="/signup">
|
||||||
|
Get started
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center space-x-4 mt-8">
|
||||||
|
<div class="grid gap-4 md:gap-8 lg:gap-4 items-start sm:grid-cols-2">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="w-6 h-6 flex-shrink-0 text-gray-500">
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||||
|
</svg>
|
||||||
|
<div class="text-sm font-medium leading-none md:text-base">
|
||||||
|
Secure encryption to protect your data
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="w-6 h-6 flex-shrink-0 text-gray-500">
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||||
|
</svg>
|
||||||
|
<div class="text-sm font-medium leading-none md:text-base">
|
||||||
|
Easy file sharing with customizable permissions
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="w-6 h-6 flex-shrink-0 text-gray-500">
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||||
|
</svg>
|
||||||
|
<div class="text-sm font-medium leading-none md:text-base">
|
||||||
|
Unlimited storage capacity for your files
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
class="w-6 h-6 flex-shrink-0 text-gray-500">
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||||
|
</svg>
|
||||||
|
<div class="text-sm font-medium leading-none md:text-base">
|
||||||
|
Seamless collaboration with built-in productivity tools
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<footer class="w-full border-t">
|
||||||
|
<div class="container grid-inset py-12 gap-4 text-center md:gap-8 md:py-16">
|
||||||
|
<div class="flex items-center justify-center gap-2 text-xs tracking-wide md:gap-4">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">© 2023 Acme, Inc. All rights reserved.</p>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Terms of Service</p>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Privacy Policy</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Main(title string) {
|
||||||
|
@content(title)
|
||||||
|
}
|
16
view/layout/base.templ
Normal file
16
view/layout/base.templ
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package layout
|
||||||
|
|
||||||
|
templ Base(title string){
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link href="/public/output.css" rel="stylesheet"/>
|
||||||
|
<title>{ title }</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{ children... }
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
55
view/signin/signin.templ
Normal file
55
view/signin/signin.templ
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package signinView
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/view/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ content(err types.Message, title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
|
||||||
|
<div class="mx-auto w-full max-w-md space-y-8">
|
||||||
|
<header class="text-center">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-3xl font-bold text-white">Sign In</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Enter your email or username below to login to your account</p>
|
||||||
|
switch err.Code {
|
||||||
|
case 0:
|
||||||
|
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<span class="font-medium">Danger alert!</span> {err.Message}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<form class="space-y-4" method="post" action="">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="email">Email</label>
|
||||||
|
<input type="email" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="email" name="email" placeholder="m@example.com" required="" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Password</label>
|
||||||
|
<a class="ml-auto inline-block text-sm underline text-gray-300 dark:text-gray-400" href="/forgot-password" rel="ugc">
|
||||||
|
Forgot your password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="password" name="password" required="" />
|
||||||
|
</div>
|
||||||
|
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div class="text-center text-sm text-white">
|
||||||
|
Don't have an account?
|
||||||
|
<a class="underline" href="/signup" rel="ugc">
|
||||||
|
Sign up
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Main(title string, err types.Message) {
|
||||||
|
@content(err, title)
|
||||||
|
}
|
159
view/signup/signup.templ
Normal file
159
view/signup/signup.templ
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package signup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fossyy/filekeeper/types"
|
||||||
|
"github.com/fossyy/filekeeper/view/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ form(err types.Message, title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="dark flex items-center min-h-screen p-4 sm:p-6 bg-gray-900">
|
||||||
|
<div class="mx-auto w-full max-w-md space-y-8">
|
||||||
|
<header class="text-center">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-3xl font-bold text-white">Sign Up</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Enter your email below to login to your account</p>
|
||||||
|
switch err.Code {
|
||||||
|
case 0:
|
||||||
|
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<span class="font-medium">Danger alert!</span> {err.Message}
|
||||||
|
</div>
|
||||||
|
case 1:
|
||||||
|
<div class="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400" role="alert">
|
||||||
|
<span class="font-medium">Success alert!</span> {err.Message}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<form class="space-y-4" method="post" action="">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="email">Email</label>
|
||||||
|
<input type="email" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="email" name="email" placeholder="m@example.com" required="" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Username</label>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="username" name="username" required="" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="password">Password</label>
|
||||||
|
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="password" name="password" required />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white" for="confirmPassword">Confirm Password</label>
|
||||||
|
<input type="password" class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-white" id="confirmPassword" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-start mt-3 ml-4 p-1">
|
||||||
|
<ul>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="matchSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="matchSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="matchGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="matchBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="matchStatusText" class="font-medium text-sm ml-3 text-red-700"> Passwords do not match</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="lengthSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="lengthSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="lengthGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="lengthBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="lengthStatusText" class="font-medium text-sm ml-3 text-red-700"> Password length must be at least 8 characters</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center py-1">
|
||||||
|
<div id="requirementsSvgContainer" class="rounded-full p-1 fill-current bg-red-200 text-green-700">
|
||||||
|
<svg id="requirementsSvgIcon" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path id="requirementsGoodPath" style="display: none;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
<path id="requirementsBadPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span id="requirementsStatusText" class="font-medium text-sm ml-3 text-red-700">The password must contain at least one symbol (!@#$%^&*), one uppercase letter, and three numbers</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="bg-slate-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit" id="submit" name="submit" disabled>
|
||||||
|
Sign up
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div class="text-center text-sm text-white">
|
||||||
|
Already have an account?
|
||||||
|
<a class="underline" href="/signin" rel="ugc">
|
||||||
|
Sign in
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/public/validatePassword.js" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Main(title string, err types.Message) {
|
||||||
|
@form(err, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
templ EmailSend(title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<style>h1, h2, h3, h4, h5, h6 { font-family: 'Arimo', sans-serif; --font-sans: 'Arimo'; }</style>
|
||||||
|
<style>body { font-family: 'Libre Franklin', sans-serif; --font-sans: 'Libre Franklin'; }</style>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-[80vh] gap-6">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-16 w-16 text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
|
||||||
|
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 text-center">
|
||||||
|
<h1 class="text-3xl font-bold">Email Verification Sent</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
We've sent a verification email to your inbox. Please check your email and follow the instructions to verify your account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ VerifySuccess(title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<style>h1, h2, h3, h4, h5, h6 { font-family: 'Arimo', sans-serif; --font-sans: 'Arimo'; }</style>
|
||||||
|
<style>body { font-family: 'Libre Franklin', sans-serif; --font-sans: 'Libre Franklin'; }</style>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-[80vh] gap-6">
|
||||||
|
<div class="bg-green-500 text-white rounded-full p-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-8 w-8"
|
||||||
|
>
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 text-center">
|
||||||
|
<h1 class="text-3xl font-bold">Account Verified</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
Your account has been successfully verified. You can now access all the features of our platform.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
56
view/upload/upload.templ
Normal file
56
view/upload/upload.templ
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package uploadView
|
||||||
|
|
||||||
|
import "github.com/fossyy/filekeeper/view/layout"
|
||||||
|
|
||||||
|
templ content(title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="flex items-center min-h-screen p-4 sm:p-6 bg-gray-900 text-white">
|
||||||
|
<div class="mx-auto w-full max-w-md space-y-8">
|
||||||
|
<div class="rounded-lg border bg-card text-card-foreground shadow-sm w-full max-w-md" data-v0-t="card">
|
||||||
|
<div class="flex flex-col space-y-1.5 p-4">
|
||||||
|
<div class="flex items-center justify-center w-full">
|
||||||
|
<label for="dropzone-file"
|
||||||
|
class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-bray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
|
||||||
|
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
|
<svg class="w-8 h-8 mb-4 text-gray-400" aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2" />
|
||||||
|
</svg>
|
||||||
|
<p class="mb-2 text-sm text-gray-400"><span
|
||||||
|
class="font-semibold">Click to upload</span> or drag and drop</p>
|
||||||
|
<p class="text-xs text-gray-400">SVG, PNG, JPG or GIF (MAX.
|
||||||
|
800x400px)</p>
|
||||||
|
</div>
|
||||||
|
<input id="dropzone-file" type="file" class="hidden" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div hidden>
|
||||||
|
<div class="flex items-center gap-x-3 whitespace-nowrap">
|
||||||
|
<div id="progress-fake" class="flex w-full h-2 rounded-full overflow-hidden bg-gray-700"
|
||||||
|
role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<div id="progress-fake"
|
||||||
|
class="flex flex-col justify-center rounded-full overflow-hidden bg-teal-500 text-xs text-white text-center whitespace-nowrap transition duration-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-6 text-end">
|
||||||
|
<span id="progress-fake" class="text-sm text-white">Starting...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/public/upload.js" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Main(title string) {
|
||||||
|
@content(title)
|
||||||
|
}
|
53
view/user/user.templ
Normal file
53
view/user/user.templ
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package userView
|
||||||
|
|
||||||
|
import "github.com/fossyy/filekeeper/view/layout"
|
||||||
|
|
||||||
|
templ content(email string, username string, title string) {
|
||||||
|
@layout.Base(title){
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg border">
|
||||||
|
<div class="px-4 py-5 sm:px-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
User Profile
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||||
|
This is some information about the user.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-200 px-4 py-5 sm:p-0">
|
||||||
|
<dl class="sm:divide-y sm:divide-gray-200">
|
||||||
|
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt class="text-sm font-medium text-gray-500">
|
||||||
|
Full name
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{ username }
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt class="text-sm font-medium text-gray-500">
|
||||||
|
Email address
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{ email }
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="py-3 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt class="text-sm font-medium text-gray-500">
|
||||||
|
Password
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
ntah lah
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<form action="/logout" method="get">
|
||||||
|
<button type="submit" class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Main(title string, email string, username string) {
|
||||||
|
@content(email, username, title)
|
||||||
|
}
|
Reference in New Issue
Block a user