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
InstallGoogleAuth(app)
// Facebook
InstallFacebookAuth(app)
// Logout
app.Get("/logout", func(ctx *aero.Context) string {
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"
"io/ioutil"
"net/http"
"strings"
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
@ -43,8 +44,8 @@ func InstallGoogleAuth(app *aero.Application) {
// Auth
app.Get("/auth/google", func(ctx *aero.Context) string {
sessionID := ctx.Session().ID()
url := config.AuthCodeURL(sessionID)
state := ctx.Session().ID()
url := config.AuthCodeURL(state)
ctx.Redirect(url)
return ""
})
@ -89,12 +90,13 @@ func InstallGoogleAuth(app *aero.Application) {
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?
user := utils.GetUser(ctx)
if user != nil {
println("Connected")
// Add GoogleToUser reference
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)
}
authLog.Info("Added Google ID to existing account", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
return ctx.Redirect("/")
}
@ -166,7 +170,7 @@ func InstallGoogleAuth(app *aero.Application) {
session.Set("userId", user.ID)
// 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
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")
label(for="Status") Status:
select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change")
option(value=arn.AnimeListStatusWatching) Watching
option(value=arn.AnimeListStatusCompleted) Completed
option(value=arn.AnimeListStatusPlanned) Plan to watch
option(value=arn.AnimeListStatusHold) On hold
option(value=arn.AnimeListStatusDropped) Dropped
.widget-input
label(for="Status") Status:
select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change")
option(value=arn.AnimeListStatusWatching) Watching
option(value=arn.AnimeListStatusCompleted) Completed
option(value=arn.AnimeListStatusPlanned) Plan to watch
option(value=arn.AnimeListStatusHold) On hold
option(value=arn.AnimeListStatusDropped) Dropped
.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")

View File

@ -2,3 +2,6 @@ component Login
.login-buttons
a.login-button(href="/auth/google")
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
.login-button
//
padding 0.5rem
.login-button-image
max-width 236px

View File

@ -2,17 +2,22 @@ component NewSoundTrack(user *arn.User)
.widgets
.widget
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:
input#youtube-link.widget-element(type="text", placeholder="https://www.youtube.com/watch?v=123")
.widget-input
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:
input#anime-link.widget-element(type="text", placeholder="https://notify.moe/anime/123")
.widget-input
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):
input#osu-link.widget-element(type="text", placeholder="https://osu.ppy.sh/s/123")
.widget-input
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
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.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)
//- h3.widget-title
//- 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("EmailToUser")
arn.DB.DeleteTable("GoogleToUser")
arn.DB.DeleteTable("FacebookToUser")
// Get a stream of all users
allUsers, err := arn.StreamUsers()
@ -32,10 +33,11 @@ func main() {
}
if user.Accounts.Google.ID != "" {
arn.DB.Set("GoogleToUser", user.Accounts.Google.ID, &arn.GoogleToUser{
ID: user.Accounts.Google.ID,
UserID: user.ID,
})
user.ConnectGoogle(user.Accounts.Google.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
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-size 1em
padding 0.4em 0.8em
border-radius 3px
line-height 1.25em
color text-color
input, textarea
border ui-border
background white
box-shadow none
width 100%
input, textarea, select
input-focus
:disabled
ui-disabled
input
default-transition
:active
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
ui-element
horizontal
font-size 1rem
line-height 1rem
padding 0.75rem 1rem
line-height 1.5em
padding 0.5rem 1rem
color link-color
align-items center
:hover,
&.active
@ -53,34 +38,16 @@ button, .button
:active
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
ui-element
appearance none
-webkit-appearance none
-moz-appearance none
font-size 1rem
// padding 0.5em 1em
label
width 100%
padding 0.5rem 0
text-align left
// input[type="submit"]:hover,
// button:hover
// cursor pointer
// text-decoration none
// button[disabled]
// opacity 0.5
// :hover
// cursor not-allowed
textarea
line-height 1.5em
height 10rem

View File

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