feat: latest info, logout, and better ui

This commit is contained in:
Nya Candy 2023-01-26 20:38:28 +08:00
parent 83daefe9f0
commit 24efbe8127
No known key found for this signature in database
GPG Key ID: 8B1BE5E86F2E66AE
17 changed files with 187 additions and 100 deletions

View File

@ -1,7 +1,8 @@
package consts package consts
const ( const (
REDIS_KEY_LOGIN_SESSION = "misso:login:%s" // Username, token as value REDIS_KEY_LOGIN_SESSION = "misso:login:%s" // Username, token as value
REDIS_KEY_CONSENT_CSRF = "misso:consent:%s" // Random string, consent challenge as value REDIS_KEY_CONSENT_CSRF = "misso:consent:%s" // Random string, consent challenge as value
REDIS_KEY_SHARE_CONTEXT = "misso:share:%s" // Subject, context as value REDIS_KEY_SHARE_ACCESS_TOKEN = "misso:share:at:%s" // Subject, access token as value
REDIS_KEY_SHARE_USER_INFO = "misso:share:ui:%s" // Subject, user info as value
) )

View File

@ -3,9 +3,9 @@ package consts
import "time" import "time"
const ( const (
TIME_LOGIN_REQUEST_VALID = 10 * time.Minute TIME_REQUEST_VALID = 1 * time.Hour
TIME_LOGIN_SESSION_VALID = 7 * 24 * time.Hour
TIME_CONSENT_REQUEST_VALID = 1 * time.Hour TIME_LOGIN_REMEMBER = 10 * time.Minute
TIME_CONSENT_SESSION_VALID = 30 * 24 * time.Hour
TIME_USERINFO_CACHE = 10 * time.Minute
) )

View File

@ -2,13 +2,11 @@ package consent
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
client "github.com/ory/hydra-client-go/v2" client "github.com/ory/hydra-client-go/v2"
"misso/consts" "misso/consts"
"misso/global" "misso/global"
"misso/types"
"misso/utils" "misso/utils"
"net/http" "net/http"
) )
@ -59,7 +57,7 @@ func ConsentCheck(ctx *gin.Context) {
global.Logger.Debugf("Generating CSRF token...") global.Logger.Debugf("Generating CSRF token...")
csrf := utils.RandString(32) csrf := utils.RandString(32)
sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, csrf) sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, csrf)
err := global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_CONSENT_REQUEST_VALID).Err() err := global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_REQUEST_VALID).Err()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to save csrf into redis with error: %v", err) global.Logger.Errorf("Failed to save csrf into redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
@ -70,23 +68,12 @@ func ConsentCheck(ctx *gin.Context) {
// Retrieve context // Retrieve context
global.Logger.Debugf("Retrieving context...") global.Logger.Debugf("Retrieving context...")
var userinfoCtx types.SessionContext
sessKey = fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, *consentReq.Subject)
userinfoCtxBytes, err := global.Redis.Get(context.Background(), sessKey).Bytes()
if err != nil {
global.Logger.Errorf("Failed to retrieve context with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to retrieve context",
})
return
}
global.Logger.Debugf("Decoding context...") userinfoCtx, err := utils.GetUserinfo(*consentReq.Subject)
err = json.Unmarshal(userinfoCtxBytes, &userinfoCtx)
if err != nil { if err != nil {
global.Logger.Errorf("Failed to parse context with error: %v", err) global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to parse context", "error": "Failed to get userinfo",
}) })
return return
} }
@ -94,7 +81,7 @@ func ConsentCheck(ctx *gin.Context) {
// Show the consent UI // Show the consent UI
global.Logger.Debugf("Rendering consent UI...") global.Logger.Debugf("Rendering consent UI...")
templateFields := gin.H{ templateFields := gin.H{
"user": userinfoCtx.User, "user": *userinfoCtx,
"challenge": oauth2challenge, "challenge": oauth2challenge,
"csrf": csrf, "csrf": csrf,
} }

View File

@ -2,15 +2,12 @@ package consent
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
client "github.com/ory/hydra-client-go/v2" client "github.com/ory/hydra-client-go/v2"
"misso/consts" "misso/consts"
"misso/global" "misso/global"
"misso/types"
"net/http" "net/http"
"time"
) )
type ConsentConfirmRequest struct { type ConsentConfirmRequest struct {
@ -80,31 +77,9 @@ func ConsentConfirm(ctx *gin.Context) {
global.Logger.Debugf("User should now be redirecting to target URI.") global.Logger.Debugf("User should now be redirecting to target URI.")
} else if req.Action == "accept" { } else if req.Action == "accept" {
global.Logger.Debugf("User accepted the request, reporting back to hydra...") global.Logger.Debugf("User accepted the request, reporting back to hydra...")
// Retrieve context
global.Logger.Debugf("Retrieving context...")
var userinfoCtx types.SessionContext
sessKey = fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, *consentReq.Subject)
userinfoCtxBytes, err := global.Redis.Get(context.Background(), sessKey).Bytes()
if err != nil {
global.Logger.Errorf("Failed to retrieve context with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to retrieve context",
})
return
}
global.Logger.Debugf("Decoding context...")
err = json.Unmarshal(userinfoCtxBytes, &userinfoCtx)
if err != nil {
global.Logger.Errorf("Failed to parse context with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to parse context",
})
return
}
global.Logger.Debugf("Initializing ID Token...") global.Logger.Debugf("Initializing ID Token...")
rememberFor := int64(consts.TIME_CONSENT_SESSION_VALID / time.Second) rememberFor := int64(0) // Remember forever
acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2ConsentRequest(context.Background()).ConsentChallenge(oauth2challenge).AcceptOAuth2ConsentRequest(client.AcceptOAuth2ConsentRequest{ acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2ConsentRequest(context.Background()).ConsentChallenge(oauth2challenge).AcceptOAuth2ConsentRequest(client.AcceptOAuth2ConsentRequest{
GrantScope: consentReq.RequestedScope, // TODO: Specify scopes GrantScope: consentReq.RequestedScope, // TODO: Specify scopes
GrantAccessTokenAudience: consentReq.RequestedAccessTokenAudience, GrantAccessTokenAudience: consentReq.RequestedAccessTokenAudience,

View File

@ -65,9 +65,11 @@ func Login(ctx *gin.Context) {
return return
} }
global.Logger.Debugf("Grabbed auth session: %s", authSess.Token)
// Save login challenge state into redis (misskey cannot keep state info) // Save login challenge state into redis (misskey cannot keep state info)
sessKey := fmt.Sprintf(consts.REDIS_KEY_LOGIN_SESSION, authSess.Token) sessKey := fmt.Sprintf(consts.REDIS_KEY_LOGIN_SESSION, authSess.Token)
err = global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_LOGIN_REQUEST_VALID).Err() err = global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_REQUEST_VALID).Err()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to save session into redis with error: %v", err) global.Logger.Errorf("Failed to save session into redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{

View File

@ -2,7 +2,6 @@ package login
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
client "github.com/ory/hydra-client-go/v2" client "github.com/ory/hydra-client-go/v2"
@ -10,7 +9,7 @@ import (
"misso/consts" "misso/consts"
"misso/global" "misso/global"
"misso/misskey" "misso/misskey"
"misso/types" "misso/utils"
"net/http" "net/http"
"time" "time"
) )
@ -65,31 +64,29 @@ func MisskeyAuthCallback(ctx *gin.Context) {
userid := fmt.Sprintf("%s@%s", usermeta.User.Username, config.Config.Misskey.Instance) userid := fmt.Sprintf("%s@%s", usermeta.User.Username, config.Config.Misskey.Instance)
// Save context into redis sessAccessTokenKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_ACCESS_TOKEN, userid)
userinfoCtxBytes, err := json.Marshal(&types.SessionContext{ err = global.Redis.Set(context.Background(), sessAccessTokenKey, usermeta.AccessToken, 0).Err()
MisskeyToken: usermeta.AccessToken,
User: usermeta.User,
})
if err != nil { if err != nil {
global.Logger.Errorf("Failed to parse accept context with error: %v", err) global.Logger.Errorf("Failed to save session access token into redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to parse accept context",
})
return
}
sessKey = fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, userid)
err = global.Redis.Set(context.Background(), sessKey, userinfoCtxBytes, consts.TIME_LOGIN_SESSION_VALID).Err()
if err != nil {
global.Logger.Errorf("Failed to save session into redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to save context", "error": "Failed to save context",
}) })
return return
} }
// Save context into redis
err = utils.SaveUserinfo(userid, &usermeta.User)
if err != nil {
global.Logger.Errorf("Failed to save session user info into redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to save userinfo",
})
return
}
global.Logger.Debugf("User accepted the request, reporting back to hydra...") global.Logger.Debugf("User accepted the request, reporting back to hydra...")
remember := true remember := true
rememberFor := int64(consts.TIME_LOGIN_SESSION_VALID / time.Second) rememberFor := int64(consts.TIME_LOGIN_REMEMBER / time.Second)
acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LoginRequest(context.Background()).LoginChallenge(oauth2challenge).AcceptOAuth2LoginRequest(client.AcceptOAuth2LoginRequest{ acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LoginRequest(context.Background()).LoginChallenge(oauth2challenge).AcceptOAuth2LoginRequest(client.AcceptOAuth2LoginRequest{
Subject: userid, Subject: userid,
Remember: &remember, Remember: &remember,

30
handlers/logout/logout.go Normal file
View File

@ -0,0 +1,30 @@
package logout
import (
"context"
"github.com/gin-gonic/gin"
"misso/global"
"net/http"
)
func Logout(ctx *gin.Context) {
oauth2challenge := ctx.Query("logout_challenge") // OAuth2 login
if oauth2challenge == "" {
ctx.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
"error": "Necessary challenge not provided",
})
return
}
acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LogoutRequest(context.Background()).LogoutChallenge(oauth2challenge).Execute()
if err != nil {
global.Logger.Errorf("Failed to accept logout request with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to accept logout request",
})
return
}
ctx.Redirect(http.StatusTemporaryRedirect, acceptReq.RedirectTo)
}

View File

@ -2,12 +2,10 @@ package user
import ( import (
"context" "context"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"misso/consts"
"misso/global" "misso/global"
"misso/types" "misso/types"
"misso/utils"
"net/http" "net/http"
"strings" "strings"
) )
@ -46,32 +44,18 @@ func UserInfo(ctx *gin.Context) {
} }
// Return user info // Return user info
// Retrieve context
global.Logger.Debugf("Retrieving context...") global.Logger.Debugf("Retrieving context...")
var userinfoCtx types.SessionContext userinfoCtx, err := utils.GetUserinfo(*tokenInfo.Sub)
sessKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, *tokenInfo.Sub)
userinfoCtxBytes, err := global.Redis.Get(context.Background(), sessKey).Bytes()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to retrieve context with error: %v", err) global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to retrieve context", "error": "Failed to get userinfo",
})
return
}
global.Logger.Debugf("Decoding context...")
err = json.Unmarshal(userinfoCtxBytes, &userinfoCtx)
if err != nil {
global.Logger.Errorf("Failed to parse context with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to parse context",
}) })
return return
} }
ctx.JSON(http.StatusOK, UserinfoResponse{ ctx.JSON(http.StatusOK, UserinfoResponse{
MisskeyUser: userinfoCtx.User, MisskeyUser: *userinfoCtx,
EMail: *tokenInfo.Sub, EMail: *tokenInfo.Sub,
}) })

View File

@ -18,7 +18,7 @@ type Error_Response struct {
} `json:"error"` } `json:"error"`
} }
func PostAPIRequest[T AuthSessionGenerate_Response | AuthSessionUserkey_Response]( func PostAPIRequest[T I_Response | AuthSessionGenerate_Response | AuthSessionUserkey_Response](
apiEndpointPath string, reqBody any, apiEndpointPath string, reqBody any,
) (*T, error) { ) (*T, error) {
// Prepare request // Prepare request

15
misskey/get_userinfo.go Normal file
View File

@ -0,0 +1,15 @@
package misskey
import "misso/types"
type I_Request struct {
I string `json:"i"`
}
type I_Response = types.MisskeyUser
func GetUserinfo(accessToken string) (*I_Response, error) {
return PostAPIRequest[I_Response]("/api/i", &I_Request{
I: accessToken,
})
}

10
routers/logout.go Normal file
View File

@ -0,0 +1,10 @@
package routers
import (
"github.com/gin-gonic/gin"
"misso/handlers/logout"
)
func Logout(rg *gin.RouterGroup) {
rg.GET("/logout", logout.Logout)
}

View File

@ -18,6 +18,9 @@ func R(e *gin.Engine) {
// Consent // Consent
Consent(rootGroup) Consent(rootGroup)
// Logout
Logout(rootGroup)
// Userinfo // Userinfo
User(rootGroup) User(rootGroup)
} }

View File

@ -10,13 +10,14 @@
#main { #main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: fit-content;
text-align: center; text-align: center;
align-items: center; align-items: center;
color: white; color: white;
padding: 32px 40px; padding: 40px;
background-color: #21252b; background-color: #21252b;
border-radius: 12px; border-radius: 12px;
margin: 0 40px;
width: 100%;
max-width: 480px; max-width: 480px;
word-break: break-word; word-break: break-word;
} }

View File

@ -182,3 +182,5 @@ type MisskeyUser struct {
EmailNotificationTypes []string `json:"emailNotificationTypes"` EmailNotificationTypes []string `json:"emailNotificationTypes"`
ShowTimelineReplies bool `json:"showTimelineReplies"` ShowTimelineReplies bool `json:"showTimelineReplies"`
} }
// TODO: Find a better way to split necessary fields and additional fields

View File

@ -1,6 +0,0 @@
package types
type SessionContext struct {
MisskeyToken string `json:"token"`
User MisskeyUser `json:"user"`
}

60
utils/get_userinfo.go Normal file
View File

@ -0,0 +1,60 @@
package utils
import (
"context"
"encoding/json"
"fmt"
"misso/consts"
"misso/global"
"misso/misskey"
"misso/types"
)
func GetUserinfo(subject string) (*types.MisskeyUser, error) {
// Check cache key
global.Logger.Debugf("Checking userinfo cache...")
userinfoCacheKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_USER_INFO, subject)
exist, err := global.Redis.Exists(context.Background(), userinfoCacheKey).Result()
if err != nil {
global.Logger.Errorf("Failed to check userinfo exist status with error: %v", err)
} else if exist > 0 {
// Exist, get from cache
userinfoCacheBytes, err := global.Redis.Get(context.Background(), userinfoCacheKey).Bytes()
if err != nil {
global.Logger.Errorf("Userinfo cache exists, but failed to retrieve with error: %v", err)
} else {
// Parse into user
var userinfo types.MisskeyUser
err = json.Unmarshal(userinfoCacheBytes, &userinfo)
if err != nil {
global.Logger.Errorf("Failed to parse userinfo cache into json with error: %v", err)
} else {
// Works!
global.Logger.Debugf("Get cached userinfo successfully.")
return &userinfo, nil
}
}
}
// Fallback to get info directly, we need user's access token.
global.Logger.Debugf("No cached userinfo found (or valid), trying to get latest response.")
accessTokenCacheKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_ACCESS_TOKEN, subject)
accessToken, err := global.Redis.Get(context.Background(), accessTokenCacheKey).Result()
if err != nil {
global.Logger.Errorf("Failed to get user access token with error: %v", err)
return nil, err
}
// Get userinfo with access token
userinfo, err := misskey.GetUserinfo(accessToken)
if err != nil {
global.Logger.Errorf("Failed to get user info with saved access token with error: %v", err)
return nil, err
}
// Save userinfo into redis
_ = SaveUserinfo(subject, userinfo) // Ignore errors
return userinfo, nil
}

26
utils/save_userinfo.go Normal file
View File

@ -0,0 +1,26 @@
package utils
import (
"context"
"encoding/json"
"fmt"
"misso/consts"
"misso/global"
"misso/types"
)
func SaveUserinfo(subject string, userinfo *types.MisskeyUser) error {
userinfoBytes, err := json.Marshal(userinfo)
if err != nil {
global.Logger.Errorf("Failed to parse accept context with error: %v", err)
return err
}
sessUserInfoKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_USER_INFO, subject)
err = global.Redis.Set(context.Background(), sessUserInfoKey, userinfoBytes, consts.TIME_USERINFO_CACHE).Err()
if err != nil {
global.Logger.Errorf("Failed to save session user info into redis with error: %v", err)
return err
}
return nil
}