Facebook login

This commit is contained in:
Eduard Urbach 2017-07-02 23:42:46 +02:00
parent 8f3c03a5c5
commit 6c7fc902c0
12 changed files with 267 additions and 78 deletions

View File

@ -8,6 +8,9 @@ func Install(app *aero.Application) {
// Google // Google
InstallGoogleAuth(app) InstallGoogleAuth(app)
// Facebook
InstallFacebookAuth(app)
// Logout // Logout
app.Get("/logout", func(ctx *aero.Context) string { app.Get("/logout", func(ctx *aero.Context) string {
if ctx.HasSession() { if ctx.HasSession() {

172
auth/facebook.go Normal file
View File

@ -0,0 +1,172 @@
package auth
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"strings"
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/utils"
"golang.org/x/oauth2"
"golang.org/x/oauth2/facebook"
)
// FacebookUser is the user data we receive from Facebook
type FacebookUser struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Gender string `json:"gender"`
}
// InstallFacebookAuth enables Facebook login for the app.
func InstallFacebookAuth(app *aero.Application) {
config := &oauth2.Config{
ClientID: arn.APIKeys.Facebook.ID,
ClientSecret: arn.APIKeys.Facebook.Secret,
RedirectURL: "https://" + app.Config.Domain + "/auth/facebook/callback",
Scopes: []string{
"public_profile",
"email",
},
Endpoint: facebook.Endpoint,
}
// Auth
app.Get("/auth/facebook", func(ctx *aero.Context) string {
state := ctx.Session().ID()
url := config.AuthCodeURL(state)
ctx.Redirect(url)
return ""
})
// Auth Callback
app.Get("/auth/facebook/callback", func(ctx *aero.Context) string {
if !ctx.HasSession() {
return ctx.Error(http.StatusUnauthorized, "Facebook login failed", errors.New("Session does not exist"))
}
session := ctx.Session()
if session.ID() != ctx.Query("state") {
return ctx.Error(http.StatusUnauthorized, "Facebook login failed", errors.New("Incorrect state"))
}
// Handle the exchange code to initiate a transport
token, err := config.Exchange(oauth2.NoContext, ctx.Query("code"))
if err != nil {
return ctx.Error(http.StatusBadRequest, "Could not obtain OAuth token", err)
}
// Construct the OAuth client
client := config.Client(oauth2.NoContext, token)
// Fetch user data from Facebook
resp, err := client.Get("https://graph.facebook.com/me?fields=email,first_name,last_name,gender")
if err != nil {
return ctx.Error(http.StatusBadRequest, "Failed requesting user data from Facebook", err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
// Construct a FacebookUser object
fbUser := FacebookUser{}
err = json.Unmarshal(body, &fbUser)
if err != nil {
return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err)
}
// Change googlemail.com to gmail.com
fbUser.Email = strings.Replace(fbUser.Email, "googlemail.com", "gmail.com", 1)
// Is this an existing user connecting another social account?
user := utils.GetUser(ctx)
if user != nil {
// Add FacebookToUser reference
err = user.ConnectFacebook(fbUser.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "Could not connect account to Facebook account", err)
}
authLog.Info("Added Facebook ID to existing account", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
return ctx.Redirect("/")
}
var getErr error
// Try to find an existing user via the Facebook user ID
user, getErr = arn.GetUserFromTable("FacebookToUser", fbUser.ID)
if getErr == nil && user != nil {
authLog.Info("User logged in via Facebook ID", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
user.LastLogin = arn.DateTimeUTC()
user.Save()
session.Set("userId", user.ID)
return ctx.Redirect("/")
}
// Try to find an existing user via the associated e-mail address
user, getErr = arn.GetUserByEmail(fbUser.Email)
if getErr == nil && user != nil {
authLog.Info("User logged in via Email", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
user.LastLogin = arn.DateTimeUTC()
user.Save()
session.Set("userId", user.ID)
return ctx.Redirect("/")
}
// Register new user
user = arn.NewUser()
user.Nick = "fb" + fbUser.ID
user.Email = fbUser.Email
user.FirstName = fbUser.FirstName
user.LastName = fbUser.LastName
user.Gender = fbUser.Gender
user.LastLogin = arn.DateTimeUTC()
// Save basic user info already to avoid data inconsistency problems
user.Save()
// Register user
err = arn.RegisterUser(user)
if err != nil {
ctx.Error(http.StatusInternalServerError, "Could not register a new user", err)
}
// Connect account to a Facebook account
err = user.ConnectFacebook(fbUser.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "Could not connect account to Facebook account", err)
}
// Save user object again with updated data
user.Save()
// Login
session.Set("userId", user.ID)
// Log
authLog.Info("Registered new user via Facebook", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
// Redirect to frontpage
return ctx.Redirect("/")
})
}

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings"
"github.com/aerogo/aero" "github.com/aerogo/aero"
"github.com/animenotifier/arn" "github.com/animenotifier/arn"
@ -43,8 +44,8 @@ func InstallGoogleAuth(app *aero.Application) {
// Auth // Auth
app.Get("/auth/google", func(ctx *aero.Context) string { app.Get("/auth/google", func(ctx *aero.Context) string {
sessionID := ctx.Session().ID() state := ctx.Session().ID()
url := config.AuthCodeURL(sessionID) url := config.AuthCodeURL(state)
ctx.Redirect(url) ctx.Redirect(url)
return "" return ""
}) })
@ -89,12 +90,13 @@ func InstallGoogleAuth(app *aero.Application) {
return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err) return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err)
} }
// Change googlemail.com to gmail.com
googleUser.Email = strings.Replace(googleUser.Email, "googlemail.com", "gmail.com", 1)
// Is this an existing user connecting another social account? // Is this an existing user connecting another social account?
user := utils.GetUser(ctx) user := utils.GetUser(ctx)
if user != nil { if user != nil {
println("Connected")
// Add GoogleToUser reference // Add GoogleToUser reference
err = user.ConnectGoogle(googleUser.Sub) err = user.ConnectGoogle(googleUser.Sub)
@ -102,6 +104,8 @@ func InstallGoogleAuth(app *aero.Application) {
ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err) ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err)
} }
authLog.Info("Added Google ID to existing account", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
return ctx.Redirect("/") return ctx.Redirect("/")
} }
@ -166,7 +170,7 @@ func InstallGoogleAuth(app *aero.Application) {
session.Set("userId", user.ID) session.Set("userId", user.ID)
// Log // Log
authLog.Info("Registered new user", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) authLog.Info("Registered new user via Google", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
// Redirect to frontpage // Redirect to frontpage
return ctx.Redirect("/") return ctx.Redirect("/")

View File

@ -5,13 +5,14 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn.
InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1") InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1")
label(for="Status") Status: .widget-input
select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change") label(for="Status") Status:
option(value=arn.AnimeListStatusWatching) Watching select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change")
option(value=arn.AnimeListStatusCompleted) Completed option(value=arn.AnimeListStatusWatching) Watching
option(value=arn.AnimeListStatusPlanned) Plan to watch option(value=arn.AnimeListStatusCompleted) Completed
option(value=arn.AnimeListStatusHold) On hold option(value=arn.AnimeListStatusPlanned) Plan to watch
option(value=arn.AnimeListStatusDropped) Dropped option(value=arn.AnimeListStatusHold) On hold
option(value=arn.AnimeListStatusDropped) Dropped
.anime-list-item-rating-edit .anime-list-item-rating-edit
InputNumber("Rating.Overall", item.Rating.Overall, arn.OverallRatingName(item.Episodes), "Overall rating on a scale of 0 to 10", "0", "10", "0.1") InputNumber("Rating.Overall", item.Rating.Overall, arn.OverallRatingName(item.Episodes), "Overall rating on a scale of 0 to 10", "0", "10", "0.1")

View File

@ -1,4 +1,7 @@
component Login component Login
.login-buttons .login-buttons
a.login-button(href="/auth/google") a.login-button(href="/auth/google")
img.login-button-image(src="/images/login/google", alt="Google Login", title="Login with your Google account") img.login-button-image(src="/images/login/google", alt="Google Login", title="Login with your Google account")
a.login-button(href="/auth/facebook")
img.login-button-image(src="/images/login/facebook", alt="Facebook Login", title="Login with your Facebook account")

View File

@ -4,7 +4,7 @@
justify-content center justify-content center
.login-button .login-button
// padding 0.5rem
.login-button-image .login-button-image
max-width 236px max-width 236px

View File

@ -2,17 +2,22 @@ component NewSoundTrack(user *arn.User)
.widgets .widgets
.widget .widget
h3 New soundtrack h3 New soundtrack
label(for="soundcloud-link") Soundcloud link:
input#soundcloud-link.widget-element(type="text", placeholder="https://soundcloud.com/abc/123")
label(for="youtube-link") Youtube link: .widget-input
input#youtube-link.widget-element(type="text", placeholder="https://www.youtube.com/watch?v=123") label(for="soundcloud-link") Soundcloud link:
input#soundcloud-link.widget-element(type="text", placeholder="https://soundcloud.com/abc/123")
label(for="anime-link") Anime link: .widget-input
input#anime-link.widget-element(type="text", placeholder="https://notify.moe/anime/123") label(for="youtube-link") Youtube link:
input#youtube-link.widget-element(type="text", placeholder="https://www.youtube.com/watch?v=123")
label(for="osu-link") Osu beatmap (optional): .widget-input
input#osu-link.widget-element(type="text", placeholder="https://osu.ppy.sh/s/123") label(for="anime-link") Anime link:
input#anime-link.widget-element(type="text", placeholder="https://notify.moe/anime/123")
.widget-input
label(for="osu-link") Osu beatmap (optional):
input#osu-link.widget-element(type="text", placeholder="https://osu.ppy.sh/s/123")
.buttons .buttons
button.action(data-action="createSoundTrack", data-trigger="click") button.action(data-action="createSoundTrack", data-trigger="click")

View File

@ -21,6 +21,34 @@ component Settings(user *arn.User)
InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com")
InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh") InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh")
.widget.mountable
h3.widget-title
Icon("user-plus")
span Connect
.widget-input.social-account
label(for="google") Google:
a#google.button.social-account-button(href="/auth/google")
if user.Accounts.Google.ID != ""
Icon("check")
span Connected
else
Icon("circle-o")
span Not connected
.widget-input.social-account
label(for="facebook") Facebook:
a#facebook.button.social-account-button(href="/auth/facebook")
if user.Accounts.Facebook.ID != ""
Icon("check")
span Connected
else
Icon("circle-o")
span Not connected
//- .widget.mountable(data-api="/api/settings/" + user.ID) //- .widget.mountable(data-api="/api/settings/" + user.ID)
//- h3.widget-title //- h3.widget-title
//- Icon("cogs") //- Icon("cogs")

View File

@ -0,0 +1,2 @@
.social-account-button
margin-bottom 1rem

View File

@ -11,6 +11,7 @@ func main() {
arn.DB.DeleteTable("NickToUser") arn.DB.DeleteTable("NickToUser")
arn.DB.DeleteTable("EmailToUser") arn.DB.DeleteTable("EmailToUser")
arn.DB.DeleteTable("GoogleToUser") arn.DB.DeleteTable("GoogleToUser")
arn.DB.DeleteTable("FacebookToUser")
// Get a stream of all users // Get a stream of all users
allUsers, err := arn.StreamUsers() allUsers, err := arn.StreamUsers()
@ -32,10 +33,11 @@ func main() {
} }
if user.Accounts.Google.ID != "" { if user.Accounts.Google.ID != "" {
arn.DB.Set("GoogleToUser", user.Accounts.Google.ID, &arn.GoogleToUser{ user.ConnectGoogle(user.Accounts.Google.ID)
ID: user.Accounts.Google.ID, }
UserID: user.ID,
}) if user.Accounts.Facebook.ID != "" {
user.ConnectFacebook(user.Accounts.Facebook.ID)
} }
} }

View File

@ -5,44 +5,29 @@ mixin input-focus
// TODO: Replace with alpha(main-color, 20%) function // TODO: Replace with alpha(main-color, 20%) function
box-shadow 0 0 6px rgba(248, 165, 130, 0.2) box-shadow 0 0 6px rgba(248, 165, 130, 0.2)
input, textarea, button, select input, textarea, button, .button, select
ui-element
font-family inherit font-family inherit
font-size 1em font-size 1em
padding 0.4em 0.8em line-height 1.25em
border-radius 3px
color text-color color text-color
input, textarea input, textarea, select
border ui-border
background white
box-shadow none
width 100%
input-focus input-focus
:disabled :disabled
ui-disabled ui-disabled
input input
default-transition
:active :active
transform translateY(3px) transform translateY(3px)
// We need this to have a selector with a higher priority than .widget-element:focus
input.widget-element,
textarea.widget-element
input-focus
textarea
height 10rem
button, .button button, .button
ui-element
horizontal horizontal
font-size 1rem line-height 1.5em
line-height 1rem padding 0.5rem 1rem
padding 0.75rem 1rem
color link-color color link-color
align-items center
:hover, :hover,
&.active &.active
@ -52,35 +37,17 @@ button, .button
:active :active
transform translateY(3px) transform translateY(3px)
// box-shadow 0 0 2px white, 0 -2px 5px rgba(0, 0, 0, 0.08) inset
// :active
// background-color black
// color white
// :focus
// color rgb(0, 0, 0)
// // box-shadow 0 0 6px alpha(mainColor, 20%)
// border 1px solid main-color
select select
ui-element
appearance none appearance none
-webkit-appearance none -webkit-appearance none
-moz-appearance none -moz-appearance none
font-size 1rem
// padding 0.5em 1em
label label
width 100% width 100%
padding 0.5rem 0 padding 0.5rem 0
text-align left text-align left
// input[type="submit"]:hover, textarea
// button:hover line-height 1.5em
// cursor pointer height 10rem
// text-decoration none
// button[disabled]
// opacity 0.5
// :hover
// cursor not-allowed

View File

@ -150,14 +150,16 @@ var routeTests = map[string][]string{
}, },
// Disable these tests because they require authorization // Disable these tests because they require authorization
"/auth/google": nil, "/auth/google": nil,
"/auth/google/callback": nil, "/auth/google/callback": nil,
"/anime/:id/edit": nil, "/auth/facebook": nil,
"/new/thread": nil, "/auth/facebook/callback": nil,
"/new/soundtrack": nil, "/anime/:id/edit": nil,
"/user": nil, "/new/thread": nil,
"/settings": nil, "/new/soundtrack": nil,
"/extension/embed": nil, "/user": nil,
"/settings": nil,
"/extension/embed": nil,
} }
// API interfaces // API interfaces