feat: latest info, logout, and better ui
parent
83daefe9f0
commit
24efbe8127
|
@ -1,7 +1,8 @@
|
|||
package consts
|
||||
|
||||
const (
|
||||
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_SHARE_CONTEXT = "misso:share:%s" // Subject, context 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_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
|
||||
)
|
||||
|
|
|
@ -3,9 +3,9 @@ package consts
|
|||
import "time"
|
||||
|
||||
const (
|
||||
TIME_LOGIN_REQUEST_VALID = 10 * time.Minute
|
||||
TIME_LOGIN_SESSION_VALID = 7 * 24 * time.Hour
|
||||
TIME_REQUEST_VALID = 1 * time.Hour
|
||||
|
||||
TIME_CONSENT_REQUEST_VALID = 1 * time.Hour
|
||||
TIME_CONSENT_SESSION_VALID = 30 * 24 * time.Hour
|
||||
TIME_LOGIN_REMEMBER = 10 * time.Minute
|
||||
|
||||
TIME_USERINFO_CACHE = 10 * time.Minute
|
||||
)
|
||||
|
|
|
@ -2,13 +2,11 @@ package consent
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
client "github.com/ory/hydra-client-go/v2"
|
||||
"misso/consts"
|
||||
"misso/global"
|
||||
"misso/types"
|
||||
"misso/utils"
|
||||
"net/http"
|
||||
)
|
||||
|
@ -59,7 +57,7 @@ func ConsentCheck(ctx *gin.Context) {
|
|||
global.Logger.Debugf("Generating CSRF token...")
|
||||
csrf := utils.RandString(32)
|
||||
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 {
|
||||
global.Logger.Errorf("Failed to save csrf into redis with error: %v", err)
|
||||
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
||||
|
@ -70,23 +68,12 @@ func ConsentCheck(ctx *gin.Context) {
|
|||
|
||||
// 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)
|
||||
userinfoCtx, err := utils.GetUserinfo(*consentReq.Subject)
|
||||
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{
|
||||
"error": "Failed to parse context",
|
||||
"error": "Failed to get userinfo",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -94,7 +81,7 @@ func ConsentCheck(ctx *gin.Context) {
|
|||
// Show the consent UI
|
||||
global.Logger.Debugf("Rendering consent UI...")
|
||||
templateFields := gin.H{
|
||||
"user": userinfoCtx.User,
|
||||
"user": *userinfoCtx,
|
||||
"challenge": oauth2challenge,
|
||||
"csrf": csrf,
|
||||
}
|
||||
|
|
|
@ -2,15 +2,12 @@ package consent
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
client "github.com/ory/hydra-client-go/v2"
|
||||
"misso/consts"
|
||||
"misso/global"
|
||||
"misso/types"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConsentConfirmRequest struct {
|
||||
|
@ -80,31 +77,9 @@ func ConsentConfirm(ctx *gin.Context) {
|
|||
global.Logger.Debugf("User should now be redirecting to target URI.")
|
||||
} else if req.Action == "accept" {
|
||||
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...")
|
||||
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{
|
||||
GrantScope: consentReq.RequestedScope, // TODO: Specify scopes
|
||||
GrantAccessTokenAudience: consentReq.RequestedAccessTokenAudience,
|
||||
|
|
|
@ -65,9 +65,11 @@ func Login(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
global.Logger.Debugf("Grabbed auth session: %s", authSess.Token)
|
||||
|
||||
// Save login challenge state into redis (misskey cannot keep state info)
|
||||
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 {
|
||||
global.Logger.Errorf("Failed to save session into redis with error: %v", err)
|
||||
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
||||
|
|
|
@ -2,7 +2,6 @@ package login
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
client "github.com/ory/hydra-client-go/v2"
|
||||
|
@ -10,7 +9,7 @@ import (
|
|||
"misso/consts"
|
||||
"misso/global"
|
||||
"misso/misskey"
|
||||
"misso/types"
|
||||
"misso/utils"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
@ -65,31 +64,29 @@ func MisskeyAuthCallback(ctx *gin.Context) {
|
|||
|
||||
userid := fmt.Sprintf("%s@%s", usermeta.User.Username, config.Config.Misskey.Instance)
|
||||
|
||||
// Save context into redis
|
||||
userinfoCtxBytes, err := json.Marshal(&types.SessionContext{
|
||||
MisskeyToken: usermeta.AccessToken,
|
||||
User: usermeta.User,
|
||||
})
|
||||
sessAccessTokenKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_ACCESS_TOKEN, userid)
|
||||
err = global.Redis.Set(context.Background(), sessAccessTokenKey, usermeta.AccessToken, 0).Err()
|
||||
if err != nil {
|
||||
global.Logger.Errorf("Failed to parse accept context 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)
|
||||
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 save context",
|
||||
})
|
||||
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...")
|
||||
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{
|
||||
Subject: userid,
|
||||
Remember: &remember,
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -2,12 +2,10 @@ package user
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"misso/consts"
|
||||
"misso/global"
|
||||
"misso/types"
|
||||
"misso/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
@ -46,32 +44,18 @@ func UserInfo(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
// Return user info
|
||||
|
||||
// Retrieve context
|
||||
global.Logger.Debugf("Retrieving context...")
|
||||
var userinfoCtx types.SessionContext
|
||||
sessKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_CONTEXT, *tokenInfo.Sub)
|
||||
userinfoCtxBytes, err := global.Redis.Get(context.Background(), sessKey).Bytes()
|
||||
userinfoCtx, err := utils.GetUserinfo(*tokenInfo.Sub)
|
||||
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{
|
||||
"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",
|
||||
"error": "Failed to get userinfo",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, UserinfoResponse{
|
||||
MisskeyUser: userinfoCtx.User,
|
||||
MisskeyUser: *userinfoCtx,
|
||||
EMail: *tokenInfo.Sub,
|
||||
})
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ type Error_Response struct {
|
|||
} `json:"error"`
|
||||
}
|
||||
|
||||
func PostAPIRequest[T AuthSessionGenerate_Response | AuthSessionUserkey_Response](
|
||||
func PostAPIRequest[T I_Response | AuthSessionGenerate_Response | AuthSessionUserkey_Response](
|
||||
apiEndpointPath string, reqBody any,
|
||||
) (*T, error) {
|
||||
// Prepare request
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -18,6 +18,9 @@ func R(e *gin.Engine) {
|
|||
// Consent
|
||||
Consent(rootGroup)
|
||||
|
||||
// Logout
|
||||
Logout(rootGroup)
|
||||
|
||||
// Userinfo
|
||||
User(rootGroup)
|
||||
}
|
||||
|
|
|
@ -10,13 +10,14 @@
|
|||
#main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
padding: 32px 40px;
|
||||
padding: 40px;
|
||||
background-color: #21252b;
|
||||
border-radius: 12px;
|
||||
margin: 0 40px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
|
@ -182,3 +182,5 @@ type MisskeyUser struct {
|
|||
EmailNotificationTypes []string `json:"emailNotificationTypes"`
|
||||
ShowTimelineReplies bool `json:"showTimelineReplies"`
|
||||
}
|
||||
|
||||
// TODO: Find a better way to split necessary fields and additional fields
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package types
|
||||
|
||||
type SessionContext struct {
|
||||
MisskeyToken string `json:"token"`
|
||||
User MisskeyUser `json:"user"`
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue