Skip to content

Commit

Permalink
Merge pull request #1 from mbaraa/feat/impl-login
Browse files Browse the repository at this point in the history
Feat: Implement login/signup
  • Loading branch information
mbaraa authored May 5, 2024
2 parents 040a5d8 + 62491f7 commit af842b5
Show file tree
Hide file tree
Showing 27 changed files with 947 additions and 125 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ DB_NAME=""
DB_HOST=""
DB_USERNAME=""
DB_PASSWORD=""

SMTP_HOST=""
SMTP_PORT=""
SMTP_USER=""
SMTP_PASSWORD=""
1 change: 1 addition & 0 deletions cmd/migrator/migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ func Migrate() error {
return dbConn.Debug().AutoMigrate(
new(models.Account),
new(models.Profile),
new(models.EmailVerificationCode),
)
}
39 changes: 31 additions & 8 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,56 @@ package server

import (
"dankmuzikk/config"
"dankmuzikk/db"
"dankmuzikk/handlers/apis"
"dankmuzikk/handlers/pages"
"dankmuzikk/log"
"dankmuzikk/models"
"dankmuzikk/services/jwt"
"dankmuzikk/services/login"
"dankmuzikk/services/youtube"
"embed"
"net/http"
)

func StartServer(staticFS embed.FS) error {
///////////// Pages and files /////////////
pagesHandler := http.NewServeMux()
pagesHandler.Handle("/static/", http.FileServer(http.FS(staticFS)))
pagesHandler.Handle("/music/", http.StripPrefix("/music", http.FileServer(http.Dir(config.Env().YouTube.MusicDir))))

jwtUtil := jwt.NewJWTImpl()

pagesHandler.HandleFunc("/", pages.Handler(pages.HandleHomePage))
pagesHandler.HandleFunc("/signup", pages.AuthHandler(pages.HandleSignupPage))
pagesHandler.HandleFunc("/login", pages.AuthHandler(pages.HandleLoginPage))
pagesHandler.HandleFunc("/profile", pages.AuthHandler(pages.HandleProfilePage))
pagesHandler.HandleFunc("/signup", pages.AuthHandler(pages.HandleSignupPage, jwtUtil))
pagesHandler.HandleFunc("/login", pages.AuthHandler(pages.HandleLoginPage, jwtUtil))
pagesHandler.HandleFunc("/profile", pages.AuthHandler(pages.HandleProfilePage, jwtUtil))
pagesHandler.HandleFunc("/about", pages.Handler(pages.HandleAboutPage))
pagesHandler.HandleFunc("/playlists", pages.AuthHandler(pages.HandlePlaylistsPage))
pagesHandler.HandleFunc("/playlists", pages.AuthHandler(pages.HandlePlaylistsPage, jwtUtil))
pagesHandler.HandleFunc("/privacy", pages.Handler(pages.HandlePrivacyPage))
pagesHandler.HandleFunc("/search", pages.Handler(pages.HandleSearchResultsPage(&youtube.YouTubeScraperSearch{})))

///////////// APIs /////////////
dbConn, err := db.Connector()
if err != nil {
log.Fatalln(log.ErrorLevel, err)
}

accountRepo := db.NewBaseDB[models.Account](dbConn)
profileRepo := db.NewBaseDB[models.Profile](dbConn)
otpRepo := db.NewBaseDB[models.EmailVerificationCode](dbConn)

emailLoginApi := apis.NewEmailLoginApi(login.NewEmailLoginService(accountRepo, profileRepo, otpRepo, jwtUtil))
googleLoginApi := apis.NewGoogleLoginApi(login.NewGoogleLoginService(accountRepo, profileRepo, otpRepo, jwtUtil))

apisHandler := http.NewServeMux()
apisHandler.HandleFunc("/login/google", apis.HandleGoogleOAuthLogin)
apisHandler.HandleFunc("/login/google/callback", apis.HandleGoogleOAuthLoginCallback)
apisHandler.HandleFunc("/search-suggession", apis.HandleSearchSugessions)
apisHandler.HandleFunc("/song/download/{youtube_video_id}", apis.HandleDownloadSong)
apisHandler.HandleFunc("POST /login/email", emailLoginApi.HandleEmailLogin)
apisHandler.HandleFunc("POST /signup/email", emailLoginApi.HandleEmailSignup)
apisHandler.HandleFunc("POST /verify-otp", emailLoginApi.HandleEmailOTPVerification)
apisHandler.HandleFunc("GET /login/google", googleLoginApi.HandleGoogleOAuthLogin)
apisHandler.HandleFunc("/login/google/callback", googleLoginApi.HandleGoogleOAuthLoginCallback)
apisHandler.HandleFunc("GET /search-suggession", apis.HandleSearchSugessions)
apisHandler.HandleFunc("GET /song/download/{youtube_video_id}", apis.HandleDownloadSong)

applicationHandler := http.NewServeMux()
applicationHandler.Handle("/", pagesHandler)
Expand Down
17 changes: 17 additions & 0 deletions config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ func initEnvVars() {
Username: getEnv("DB_USERNAME"),
Password: getEnv("DB_PASSWORD"),
},
Smtp: struct {
Host string
Port string
Username string
Password string
}{
Host: getEnv("SMTP_HOST"),
Port: getEnv("SMTP_PORT"),
Username: getEnv("SMTP_USER"),
Password: getEnv("SMTP_PASSWORD"),
},
}
}

Expand All @@ -63,6 +74,12 @@ type config struct {
Username string
Password string
}
Smtp struct {
Host string
Port string
Username string
Password string
}
}

// Env returns the thing's config values :)
Expand Down
2 changes: 1 addition & 1 deletion db/allowed_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package db
import "dankmuzikk/models"

type AllowedModels interface {
models.Account | models.Profile
models.Account | models.Profile | models.EmailVerificationCode
GetId() uint
}
14 changes: 14 additions & 0 deletions entities/email_login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package entities

type LoginRequest struct {
Email string `json:"email"`
}

type SignupRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}

type OtpRequest struct {
Code string `json:"code"`
}
113 changes: 113 additions & 0 deletions handlers/apis/email_login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package apis

import (
"context"
"dankmuzikk/config"
"dankmuzikk/entities"
"dankmuzikk/handlers"
"dankmuzikk/log"
"dankmuzikk/services/login"
"dankmuzikk/views/components/otp"
"encoding/json"
"net/http"
"time"
)

type emailLoginApi struct {
service *login.EmailLoginService
}

func NewEmailLoginApi(service *login.EmailLoginService) *emailLoginApi {
return &emailLoginApi{service}
}

func (e *emailLoginApi) HandleEmailLogin(w http.ResponseWriter, r *http.Request) {
var reqBody entities.LoginRequest
err := json.NewDecoder(r.Body).Decode(&reqBody)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

verificationToken, err := e.service.Login(reqBody)
if err != nil {
log.Errorf("[EMAIL LOGIN API]: Failed to login user: %+v, error: %s\n", reqBody, err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}

http.SetCookie(w, &http.Cookie{
Name: handlers.VerificationTokenKey,
Value: verificationToken,
HttpOnly: true,
Path: "/api/verify-otp",
Domain: config.Env().Hostname,
Expires: time.Now().UTC().Add(time.Hour / 2),
})
otp.VerifyOtp().Render(context.Background(), w)
}

func (e *emailLoginApi) HandleEmailSignup(w http.ResponseWriter, r *http.Request) {
var reqBody entities.SignupRequest
err := json.NewDecoder(r.Body).Decode(&reqBody)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

verificationToken, err := e.service.Signup(reqBody)
if err != nil {
log.Errorf("[EMAIL LOGIN API]: Failed to sign up a new user: %+v, error: %s\n", reqBody, err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}

http.SetCookie(w, &http.Cookie{
Name: handlers.VerificationTokenKey,
Value: verificationToken,
HttpOnly: true,
Path: "/api/verify-otp",
Domain: config.Env().Hostname,
Expires: time.Now().UTC().Add(time.Hour / 2),
})
otp.VerifyOtp().Render(context.Background(), w)
}

func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *http.Request) {
verificationToken, err := r.Cookie(handlers.VerificationTokenKey)
if err != nil {
w.Write([]byte("Invalid verification token"))
return
}
if verificationToken.Expires.After(time.Now().UTC()) {
w.Write([]byte("Expired verification token"))
return
}

var reqBody entities.OtpRequest
err = json.NewDecoder(r.Body).Decode(&reqBody)
if err != nil {
log.Error(err)
w.WriteHeader(http.StatusBadRequest)
return
}

sessionToken, err := e.service.VerifyOtp(verificationToken.Value, reqBody)
// TODO: specify errors further suka
if err != nil {
log.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

http.SetCookie(w, &http.Cookie{
Name: handlers.SessionTokenKey,
Value: sessionToken,
HttpOnly: true,
Path: "/",
Domain: config.Env().Hostname,
Expires: time.Now().UTC().Add(time.Hour * 24 * 30),
})

w.Header().Set("HX-Redirect", "/")
}
23 changes: 17 additions & 6 deletions handlers/apis/google_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ package apis

import (
"dankmuzikk/config"
"dankmuzikk/handlers"
"dankmuzikk/log"
"dankmuzikk/services/google"
"dankmuzikk/services/login"
"net/http"
"time"
)

func HandleGoogleOAuthLogin(w http.ResponseWriter, r *http.Request) {
url := config.GoogleOAuthConfig().AuthCodeURL(google.CurrentRandomState())
type googleLoginApi struct {
service *login.GoogleLoginService
}

func NewGoogleLoginApi(service *login.GoogleLoginService) *googleLoginApi {
return &googleLoginApi{service}
}

func (g *googleLoginApi) HandleGoogleOAuthLogin(w http.ResponseWriter, r *http.Request) {
url := config.GoogleOAuthConfig().AuthCodeURL(g.service.CurrentRandomState())
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func HandleGoogleOAuthLoginCallback(w http.ResponseWriter, r *http.Request) {
func (g *googleLoginApi) HandleGoogleOAuthLoginCallback(w http.ResponseWriter, r *http.Request) {
state := r.FormValue("state")
if state == "" {
w.WriteHeader(http.StatusBadRequest)
Expand All @@ -27,17 +36,19 @@ func HandleGoogleOAuthLoginCallback(w http.ResponseWriter, r *http.Request) {
return
}

sessionToken, err := google.CompleteLoginWithGoogle(state, code)
sessionToken, err := g.service.Login(state, code)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
log.Errorln("[GOOGLE LOGIN API]: ", err)
return
}

http.SetCookie(w, &http.Cookie{
Name: "token",
Name: handlers.SessionTokenKey,
Value: sessionToken,
HttpOnly: true,
Path: "/",
Domain: config.Env().Hostname,
Expires: time.Now().UTC().Add(time.Hour * 24 * 30),
})
http.Redirect(w, r, config.Env().Hostname, http.StatusTemporaryRedirect)
Expand Down
6 changes: 6 additions & 0 deletions handlers/cookie_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package handlers

const (
VerificationTokenKey = "verification-token"
SessionTokenKey = "token"
)
36 changes: 34 additions & 2 deletions handlers/pages/pages.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,55 @@
package pages

import (
"dankmuzikk/config"
"dankmuzikk/handlers"
"dankmuzikk/log"
"dankmuzikk/services/jwt"
"net/http"
"slices"
"strings"

_ "github.com/a-h/templ"
)

var noAuthPaths = []string{"/login", "/signup"}

func Handler(hand http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
hand(w, r)
}
}

func AuthHandler(hand http.HandlerFunc) http.HandlerFunc {
func AuthHandler(hand http.HandlerFunc, jwtUtil jwt.Manager[any]) http.HandlerFunc {
return Handler(func(w http.ResponseWriter, r *http.Request) {
log.Info(r.Cookie("token"))
sessionToken, err := r.Cookie(handlers.SessionTokenKey)
if err != nil {
log.Errorln("[AUTH]:", err)
if slices.Contains(noAuthPaths, r.URL.Path) {
hand(w, r)
return
}
http.Redirect(w, r, config.Env().Hostname+"/login", http.StatusTemporaryRedirect)
return
}

err = jwtUtil.Validate(sessionToken.Value, jwt.SessionToken)
if err != nil {
log.Errorln("[AUTH]:", err)
if slices.Contains(noAuthPaths, r.URL.Path) {
hand(w, r)
return
}
http.Redirect(w, r, config.Env().Hostname+"/login", http.StatusTemporaryRedirect)
return
}

if slices.Contains(noAuthPaths, r.URL.Path) {
http.Redirect(w, r, config.Env().Hostname, http.StatusTemporaryRedirect)
return
}

hand(w, r)
})
}
Expand Down
1 change: 1 addition & 0 deletions models/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "time"
type Account struct {
Id uint `gorm:"primaryKey;autoIncrement"`
Email string `gorm:"unique;not null"`
IsOAuth bool
CreatedAt time.Time
UpdatedAt time.Time
}
Expand Down
16 changes: 16 additions & 0 deletions models/verification_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package models

import "time"

type EmailVerificationCode struct {
Id uint `gorm:"primaryKey;autoIncrement"`
AccountId uint
Account Account
Code string `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}

func (e EmailVerificationCode) GetId() uint {
return e.Id
}
Loading

0 comments on commit af842b5

Please sign in to comment.