Compare commits

...

10 Commits

Author SHA1 Message Date
Peter Cai f4be6e135a templates: Translate to en_US 2023-02-24 16:17:04 +01:00
Peter Cai 950f521002 Add "nickname" claim for better OIDC compat 2023-02-24 16:10:16 +01:00
Peter Cai 20ad51f214 feat: Add "sub" claim for the openid scope
Required by OIDC clients like Gitea
2023-02-24 16:05:47 +01:00
Peter Cai 7de5f11bcc Update .gitignore 2023-02-24 00:46:31 +01:00
Nya Candy fec344dc45
chore: merge /api/ 2023-02-17 23:24:00 +08:00
Nya Candy efe707fd16
feat: any
any is fine
2023-01-29 17:22:21 +08:00
Nya Candy 22d44480c9
feat: limit scopes max height to prevent page overflow 2023-01-29 16:08:10 +08:00
Nya Candy 369db32569
fix: page not full on firefox 2023-01-29 14:57:31 +08:00
Nya Candy a04c5b062e
feat: configurable timings 2023-01-29 14:55:56 +08:00
Nya Candy a99045a034
feat(refractor): use misskey userinfo key as scope
...and UI tweaks 🎨
2023-01-29 14:23:49 +08:00
22 changed files with 288 additions and 281 deletions

View File

@ -7,3 +7,8 @@ misskey:
secret: ""
hydra:
admin_url: "http://localhost:4445"
time:
request_valid: 3600
login_remember: 600
consent_remember: 0
userinfo_cache: 3600

3
.gitignore vendored
View File

@ -4,3 +4,6 @@
db/
redis/
config.yml
misso

View File

@ -1,8 +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_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
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_USER_ACCESS_TOKEN = "misso:user:token:%s" // Subject, access token as value
REDIS_KEY_USER_INFO = "misso:user:info:%s" // Subject, user info as value
)

View File

@ -1,12 +1,10 @@
package consts
import "time"
const (
TIME_REQUEST_VALID = 1 * time.Hour
TIME_DEFAULT_REQUEST_VALID = 3600 // 1 Hour
TIME_LOGIN_REMEMBER = 10 * time.Minute
TIME_CONSENT_REMEMBER = 0 // Forever
TIME_DEFAULT_LOGIN_REMEMBER = 600 // 10 Minute
TIME_DEFAULT_CONSENT_REMEMBER = 0 // Forever
TIME_USERINFO_CACHE = 10 * time.Minute
TIME_DEFAULT_USERINFO_CACHE = 3600 // 1 Hour
)

View File

@ -5,10 +5,12 @@ import (
"fmt"
"github.com/gin-gonic/gin"
client "github.com/ory/hydra-client-go/v2"
"misso/config"
"misso/consts"
"misso/global"
"misso/utils"
"net/http"
"time"
)
func ConsentCheck(ctx *gin.Context) {
@ -56,8 +58,8 @@ func ConsentCheck(ctx *gin.Context) {
// Generate CSRF token
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_REQUEST_VALID).Err()
sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, oauth2challenge)
err := global.Redis.Set(context.Background(), sessKey, csrf, time.Duration(config.Config.Time.RequestValid)*time.Second).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{
@ -69,7 +71,7 @@ func ConsentCheck(ctx *gin.Context) {
// Retrieve context
global.Logger.Debugf("Retrieving context...")
userinfoCtx, err := utils.GetUserinfo(*consentReq.Subject)
userinfo, err := utils.GetUserinfo(*consentReq.Subject)
if err != nil {
global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
@ -81,9 +83,10 @@ func ConsentCheck(ctx *gin.Context) {
// Show the consent UI
global.Logger.Debugf("Rendering consent UI...")
templateFields := gin.H{
"user": *userinfoCtx,
"user": *userinfo,
"challenge": oauth2challenge,
"csrf": csrf,
"scopes": consentReq.RequestedScope,
}
if consentReq.Client.LogoUri != nil && *consentReq.Client.LogoUri != "" {
@ -94,6 +97,12 @@ func ConsentCheck(ctx *gin.Context) {
} else {
templateFields["clientName"] = *consentReq.Client.ClientId
}
if consentReq.Client.PolicyUri != nil && *consentReq.Client.PolicyUri != "" {
templateFields["clientPolicy"] = *consentReq.Client.PolicyUri
}
if consentReq.Client.TosUri != nil && *consentReq.Client.TosUri != "" {
templateFields["clientTos"] = *consentReq.Client.TosUri
}
ctx.HTML(http.StatusOK, "consent.tmpl", templateFields)
global.Logger.Debugf("User should now see Consent UI.")

View File

@ -5,16 +5,17 @@ import (
"fmt"
"github.com/gin-gonic/gin"
client "github.com/ory/hydra-client-go/v2"
"misso/config"
"misso/consts"
"misso/global"
"net/http"
"time"
)
type ConsentConfirmRequest struct {
CSRF string `form:"_csrf"`
Remember bool `form:"remember"`
Action string `form:"action"`
CSRF string `form:"_csrf"`
Challenge string `form:"challenge"`
Remember bool `form:"remember"`
Action string `form:"action"`
}
func ConsentConfirm(ctx *gin.Context) {
@ -32,16 +33,23 @@ func ConsentConfirm(ctx *gin.Context) {
// Validate CSRF
global.Logger.Debugf("Validating CSRF...")
sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, req.CSRF)
oauth2challenge, err := global.Redis.Get(context.Background(), sessKey).Result()
sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, req.Challenge)
csrfSession, err := global.Redis.Get(context.Background(), sessKey).Result()
if err != nil {
global.Logger.Errorf("Failed to get csrf from redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to get csrf",
})
return
} else if csrfSession != req.CSRF {
ctx.HTML(http.StatusForbidden, "error.tmpl", gin.H{
"error": "CSRF not match",
})
return
}
oauth2challenge := req.Challenge
// Delete used challenge
global.Redis.Del(context.Background(), sessKey)
@ -80,9 +88,9 @@ func ConsentConfirm(ctx *gin.Context) {
global.Logger.Debugf("User accepted the request, reporting back to hydra...")
global.Logger.Debugf("Initializing ID Token...")
rememberFor := int64(consts.TIME_CONSENT_REMEMBER / time.Second) // Remember forever
rememberFor := config.Config.Time.ConsentRemember // Remember forever
acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2ConsentRequest(context.Background()).ConsentChallenge(oauth2challenge).AcceptOAuth2ConsentRequest(client.AcceptOAuth2ConsentRequest{
GrantScope: consentReq.RequestedScope, // TODO: Specify scopes
GrantScope: consentReq.RequestedScope,
GrantAccessTokenAudience: consentReq.RequestedAccessTokenAudience,
Remember: &req.Remember,
RememberFor: &rememberFor,

View File

@ -5,10 +5,12 @@ import (
"fmt"
"github.com/gin-gonic/gin"
client "github.com/ory/hydra-client-go/v2"
"misso/config"
"misso/consts"
"misso/global"
"misso/misskey"
"net/http"
"time"
)
func Login(ctx *gin.Context) {
@ -69,7 +71,7 @@ func Login(ctx *gin.Context) {
// 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_REQUEST_VALID).Err()
err = global.Redis.Set(context.Background(), sessKey, oauth2challenge, time.Duration(config.Config.Time.RequestValid)*time.Second).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{

View File

@ -9,9 +9,7 @@ import (
"misso/consts"
"misso/global"
"misso/misskey"
"misso/utils"
"net/http"
"time"
)
func MisskeyAuthCallback(ctx *gin.Context) {
@ -62,9 +60,10 @@ func MisskeyAuthCallback(ctx *gin.Context) {
return
}
userid := fmt.Sprintf("%s@%s", usermeta.User.Username, config.Config.Misskey.Instance)
// Save user key
userIdentifier := fmt.Sprintf("%s@%s", usermeta.User.Username, config.Config.Misskey.Instance)
sessAccessTokenKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_ACCESS_TOKEN, userid)
sessAccessTokenKey := fmt.Sprintf(consts.REDIS_KEY_USER_ACCESS_TOKEN, userIdentifier)
err = global.Redis.Set(context.Background(), sessAccessTokenKey, usermeta.AccessToken, 0).Err()
if err != nil {
global.Logger.Errorf("Failed to save session access token into redis with error: %v", err)
@ -74,24 +73,16 @@ func MisskeyAuthCallback(ctx *gin.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_REMEMBER / time.Second)
acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LoginRequest(context.Background()).LoginChallenge(oauth2challenge).AcceptOAuth2LoginRequest(client.AcceptOAuth2LoginRequest{
Subject: userid,
Remember: &remember,
RememberFor: &rememberFor,
}).Execute()
acceptReq := client.AcceptOAuth2LoginRequest{
Subject: userIdentifier,
}
if config.Config.Time.LoginRemember > 0 {
remember := true
acceptReq.Remember = &remember
acceptReq.RememberFor = &config.Config.Time.LoginRemember
}
acceptRes, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LoginRequest(context.Background()).LoginChallenge(oauth2challenge).AcceptOAuth2LoginRequest(acceptReq).Execute()
if err != nil {
global.Logger.Errorf("Failed to accept login request with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
@ -101,7 +92,7 @@ func MisskeyAuthCallback(ctx *gin.Context) {
}
// Redirect to target uri
ctx.Redirect(http.StatusTemporaryRedirect, acceptReq.RedirectTo)
ctx.Redirect(http.StatusTemporaryRedirect, acceptRes.RedirectTo)
global.Logger.Debugf("User should now be redirecting to target URI.")

View File

@ -4,17 +4,11 @@ import (
"context"
"github.com/gin-gonic/gin"
"misso/global"
"misso/types"
"misso/utils"
"net/http"
"strings"
)
type UserinfoResponse struct {
types.MisskeyUser
EMail string `json:"email"`
}
func UserInfo(ctx *gin.Context) {
// Get token from header
accessToken := strings.Replace(ctx.GetHeader("Authorization"), "Bearer ", "", 1)
@ -45,7 +39,7 @@ func UserInfo(ctx *gin.Context) {
// Return user info
global.Logger.Debugf("Retrieving context...")
userinfoCtx, err := utils.GetUserinfo(*tokenInfo.Sub)
userinfo, err := utils.GetUserinfo(*tokenInfo.Sub)
if err != nil {
global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
@ -54,9 +48,26 @@ func UserInfo(ctx *gin.Context) {
return
}
ctx.JSON(http.StatusOK, UserinfoResponse{
MisskeyUser: *userinfoCtx,
EMail: *tokenInfo.Sub,
})
userinfoRes := gin.H{} // map[string]interface{}
// Get scopes
if tokenInfo.Scope != nil && *tokenInfo.Scope != "" {
// Has scopes
scopes := strings.Split(*tokenInfo.Scope, " ")
for _, s := range scopes {
if value, ok := (*userinfo)[s]; ok {
userinfoRes[s] = value
}
if s == "openid" {
userinfoRes["sub"] = *tokenInfo.Sub
} else if s == "username" {
// Add "nickname" field for OIDC compatibility
userinfoRes["nickname"] = userinfoRes[s]
}
}
}
ctx.JSON(http.StatusOK, userinfoRes)
}

View File

@ -3,6 +3,7 @@ package inits
import (
"gopkg.in/yaml.v3"
"misso/config"
"misso/consts"
"os"
)
@ -23,5 +24,21 @@ func Config() error {
return err
}
// Validate time
if config.Config.Time.RequestValid <= 0 {
config.Config.Time.RequestValid = consts.TIME_DEFAULT_REQUEST_VALID
}
if config.Config.Time.LoginRemember < 0 {
// 0 means don't remember (in extreme account switch situations)
config.Config.Time.LoginRemember = consts.TIME_DEFAULT_LOGIN_REMEMBER
}
if config.Config.Time.ConsentRemember < 0 {
// 0 means remember forever (default behavior)
config.Config.Time.ConsentRemember = consts.TIME_DEFAULT_CONSENT_REMEMBER
}
if config.Config.Time.UserinfoCache <= 0 {
config.Config.Time.UserinfoCache = consts.TIME_DEFAULT_USERINFO_CACHE
}
return nil
}

View File

@ -22,7 +22,7 @@ func PostAPIRequest[T I_Response | AuthSessionGenerate_Response | AuthSessionUse
apiEndpointPath string, reqBody any,
) (*T, error) {
// Prepare request
apiEndpoint := fmt.Sprintf("https://%s%s", config.Config.Misskey.Instance, apiEndpointPath)
apiEndpoint := fmt.Sprintf("https://%s/api/%s", config.Config.Misskey.Instance, apiEndpointPath)
reqBodyBytes, err := json.Marshal(reqBody)
if err != nil {

View File

@ -13,7 +13,7 @@ type AuthSessionGenerate_Response struct {
func GenerateAuthSession() (*AuthSessionGenerate_Response, error) {
return PostAPIRequest[AuthSessionGenerate_Response]("/api/auth/session/generate", &AuthSessionGenerate_Request{
return PostAPIRequest[AuthSessionGenerate_Response]("auth/session/generate", &AuthSessionGenerate_Request{
AppSecret: config.Config.Misskey.Application.Secret,
})

View File

@ -9,7 +9,7 @@ type I_Request struct {
type I_Response = types.MisskeyUser
func GetUserinfo(accessToken string) (*I_Response, error) {
return PostAPIRequest[I_Response]("/api/i", &I_Request{
return PostAPIRequest[I_Response]("i", &I_Request{
I: accessToken,
})
}

View File

@ -11,13 +11,13 @@ type AuthSessionUserkey_Request struct {
}
type AuthSessionUserkey_Response struct {
AccessToken string `json:"accessToken"`
User types.MisskeyUser `json:"user"`
AccessToken string `json:"accessToken"`
User types.MisskeyUserBase `json:"user"`
}
func GetUserkey(token string) (*AuthSessionUserkey_Response, error) {
return PostAPIRequest[AuthSessionUserkey_Response]("/api/auth/session/userkey", &AuthSessionUserkey_Request{
return PostAPIRequest[AuthSessionUserkey_Response]("auth/session/userkey", &AuthSessionUserkey_Request{
AppSecret: config.Config.Misskey.Application.Secret,
Token: token,
})

View File

@ -1,11 +1,60 @@
{{ define "consent.tmpl" }}
<html>
<html lang="zh">
<head>
<title>授权确认</title>
<title>Confirm authorization</title>
{{ template "head.tmpl" }}
<style>
ul.scopes,
ul.app-terms {
list-style: none;
padding-left: 0;
}
ul.scopes {
text-align: left;
width: 100%;
max-height: 30vh;
overflow-y: auto;
}
ul.scopes > li {
padding: 6px 12px;
margin: 6px 0;
border-radius: 6px;
background-color: #282c34;
}
ul.scopes > li > * {
cursor: pointer;
}
ul.app-terms {
display: inline-flex;
gap: 0;
}
ul.app-terms > li {
padding: 0 6px;
margin: 0;
}
ul.app-terms > li:not(:last-child) {
border-right: 1px solid white;
}
ul.app-terms > li > a {
color: #62b6e7;
}
.consent-notice {
width: 100%;
border-radius: 6px;
background-color: #282c34;
}
p.remember {
display: flex;
justify-content: center;
}
p.remember, p.remember > * {
cursor: pointer;
@ -59,7 +108,7 @@
background: #15803d;
}
#app-name, #user-name {
.app-name, .user-name {
color: #62b6e7;
}
</style>
@ -67,29 +116,69 @@
<body>
<form id="main" action="/consent" method="POST">
<input type="hidden" name="_csrf" value="{{ .csrf }}" />
<input type="hidden" name="challenge" value="{{ .challenge }}" />
{{ if .logo }}
<img src="{{ .logo }}" alt="{{ .clientName }}" width="120" height="120" />
{{ else }}
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48"><title>c-warning</title><g><path fill="#EFD358" d="M24,1C11.31787,1,1,11.31787,1,24s10.31787,23,23,23s23-10.31787,23-23S36.68213,1,24,1z"></path> <path fill="#FFFFFF" d="M24,28c0.55225,0,1-0.44775,1-1V14c0-0.55225-0.44775-1-1-1s-1,0.44775-1,1v13 C23,27.55225,23.44775,28,24,28z"></path> <circle fill="#FFFFFF" cx="24" cy="33" r="2"></circle></g></svg>
{{ end }}
<div class="logo">
{{ if .logo }}
<img src="{{ .logo }}" alt="{{ .clientName }}" width="120" height="120" />
{{ else }}
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48">
<g>
<circle fill="#EFD358" cx="24" cy="24" r="24"></circle>
<path fill="#FFFFFF" d="M24,28c0.55225,0,1-0.44775,1-1V14c0-0.55225-0.44775-1-1-1s-1,0.44775-1,1v13 C23,27.55225,23.44775,28,24,28z"></path>
<circle fill="#FFFFFF" cx="24" cy="33" r="2"></circle>
</g>
</svg>
{{ end }}
</div>
<p>
应用程序
<span id="app-name">{{ .clientName }}</span>
正请求读取
<span id="user-name">{{ .user.Name }}</span>
的信息
</p>
<div>
<p>
Application
<span class="app-name">{{ .clientName }}</span>
is requesting
<br />
access to account
<span class="user-name">{{ .user.name }}</span>
with the following scopes:
</p>
<p class="remember">
<input type="checkbox" id="remember" name="remember" value="true" />
<label for="remember">记住我的选择</label>
</p>
<ul class="scopes">
{{ $user := .user }}
{{ range $scope := .scopes }}
<li>
<details>
<summary>{{ $scope }}</summary>
<pre>{{ index $user $scope }}</pre>
</details>
</li>
{{ end }}
</ul>
{{ if or .clientPolicy .clientTos }}
<ul class="app-terms">
{{ if .clientPolicy }}
<li><a href="{{ .clientPolicy }}" target="_blank" referrerpolicy="no-referrer">Client Usage Policy</a></li>
{{ end }}
{{ if .clientTos }}
<li><a href="{{ .clientTos }}" target="_blank" referrerpolicy="no-referrer">Terms of Service</a></li>
{{ end }}
</ul>
{{ end }}
</div>
<div class="consent-notice">
<p>Accept the request?</p>
<p class="remember">
<input type="checkbox" id="remember" name="remember" value="true" />
<label for="remember">Remember my choice</label>
</p>
</div>
<p class="buttons">
<button type="submit" name="action" value="reject" class="reject">拒绝</button>
<button type="submit" name="action" value="accept" class="accept">接受</button>
<button type="submit" name="action" value="reject" class="reject">Reject</button>
<button type="submit" name="action" value="accept" class="accept">Accept</button>
</p>
</form>
</body>

View File

@ -1,14 +1,30 @@
{{ define "error.tmpl" }}
<html>
<html lang="zh">
<head>
<title>出错了</title>
<title>Error</title>
{{ template "head.tmpl" }}
<style>
.error {
width: 100%;
padding: 20px 0;
border-radius: 6px;
background-color: #282c34;
user-select: all;
}
</style>
</head>
<body>
<div id="main">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48"><title>c-remove</title><g><path fill="#E86C60" d="M24,47C11.31787,47,1,36.68262,1,24S11.31787,1,24,1s23,10.31738,23,23S36.68213,47,24,47z"></path> <path fill="#FFFFFF" d="M25.41406,24l7.29297-7.29297c0.39062-0.39062,0.39062-1.02344,0-1.41406s-1.02344-0.39062-1.41406,0 L24,22.58594l-7.29297-7.29297c-0.39062-0.39062-1.02344-0.39062-1.41406,0s-0.39062,1.02344,0,1.41406L22.58594,24 l-7.29297,7.29297c-0.39062,0.39062-0.39062,1.02344,0,1.41406C15.48828,32.90234,15.74414,33,16,33 s0.51172-0.09766,0.70703-0.29297L24,25.41406l7.29297,7.29297C31.48828,32.90234,31.74414,33,32,33 s0.51172-0.09766,0.70703-0.29297c0.39062-0.39062,0.39062-1.02344,0-1.41406L25.41406,24z"></path></g></svg>
<h1>发生了一些错误</h1>
<p>{{ .error }}</p>
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48">
<g>
<circle fill="#E86C60" cx="24" cy="24" r="24"></circle>
<path fill="#FFFFFF" d="M25.41406,24l7.29297-7.29297c0.39062-0.39062,0.39062-1.02344,0-1.41406s-1.02344-0.39062-1.41406,0 L24,22.58594l-7.29297-7.29297c-0.39062-0.39062-1.02344-0.39062-1.41406,0s-0.39062,1.02344,0,1.41406L22.58594,24 l-7.29297,7.29297c-0.39062,0.39062-0.39062,1.02344,0,1.41406C15.48828,32.90234,15.74414,33,16,33 s0.51172-0.09766,0.70703-0.29297L24,25.41406l7.29297,7.29297C31.48828,32.90234,31.74414,33,32,33 s0.51172-0.09766,0.70703-0.29297c0.39062-0.39062,0.39062-1.02344,0-1.41406L25.41406,24z"></path>
</g>
</svg>
</div>
<h1>An error happened</h1>
<p class="error">{{ .error }}</p>
</div>
</body>
</html>

View File

@ -1,10 +1,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<style>
html, body {
height: 100%;
width: 100%;
margin: 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
background: #282c34;
background-color: #282c34;
}
#main {
@ -13,12 +19,23 @@
text-align: center;
align-items: center;
color: white;
padding: 40px;
padding: 80px 40px 40px 40px;
background-color: #21252b;
border-radius: 12px;
margin: 0 40px;
width: 100%;
max-width: 480px;
word-break: break-word;
position: relative;
}
.logo {
position: absolute;
border: 10px solid #282c34;
top: -70px;
}
.logo, .logo > * {
border-radius: 120px;
}
</style>

View File

@ -1,5 +1,5 @@
{{ define "index.tmpl" }}
<html>
<html lang="zh">
<head>
<title>MiSSO</title>
{{ template "head.tmpl" }}
@ -24,13 +24,21 @@
</head>
<body>
<div id="main">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48"><title>access-key</title><rect data-element="frame" x="0" y="0" width="48" height="48" rx="48" ry="48" stroke="none" fill="#62b6e7"></rect><g transform="translate(9.600000000000001 9.600000000000001) scale(0.6)"><path d="M38,23a1,1,0,0,1-.707-.293l-6-6a1,1,0,0,1,0-1.414l8-8a1,1,0,0,1,1.414,0l6,6a1,1,0,0,1,0,1.414l-2,2a1,1,0,0,1-1.414,0L41,14.414,38.414,17l2.293,2.293a1,1,0,0,1,0,1.414l-2,2A1,1,0,0,1,38,23Z" fill="#eba40a"></path><path d="M44.061,3.939a1.5,1.5,0,0,0-2.122,0L17.923,27.956a10.027,10.027,0,1,0,2.121,2.121L44.061,6.061A1.5,1.5,0,0,0,44.061,3.939ZM12,43a7,7,0,1,1,4.914-11.978c.011.012.014.027.025.039s.027.014.039.025A6.995,6.995,0,0,1,12,43Z" fill="#ffd764"></path></g></svg>
<h1>欢迎来到 MiSSO</h1>
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48">
<circle fill="#62b6e7" cx="24" cy="24" r="24"></circle>
<g transform="translate(9.600000000000001 9.600000000000001) scale(0.6)">
<path d="M38,23a1,1,0,0,1-.707-.293l-6-6a1,1,0,0,1,0-1.414l8-8a1,1,0,0,1,1.414,0l6,6a1,1,0,0,1,0,1.414l-2,2a1,1,0,0,1-1.414,0L41,14.414,38.414,17l2.293,2.293a1,1,0,0,1,0,1.414l-2,2A1,1,0,0,1,38,23Z" fill="#eba40a"></path>
<path d="M44.061,3.939a1.5,1.5,0,0,0-2.122,0L17.923,27.956a10.027,10.027,0,1,0,2.121,2.121L44.061,6.061A1.5,1.5,0,0,0,44.061,3.939ZM12,43a7,7,0,1,1,4.914-11.978c.011.012.014.027.025.039s.027.014.039.025A6.995,6.995,0,0,1,12,43Z" fill="#ffd764"></path>
</g>
</svg>
</div>
<h1>Welcome to MiSSO</h1>
<p>
直接访问这里好像不太对哦
You should not access this page directly.
</p>
<p>
<a class="primary" href="https://docs.nya.one/peripheral/misso/">查看文档</a>
<a class="primary" href="https://docs.nya.one/peripheral/misso/">Documentation</a>
</p>
</div>
</body>

View File

@ -14,4 +14,10 @@ type Config struct {
Hydra struct {
AdminUrl string `yaml:"admin_url"`
} `yaml:"hydra"`
Time struct {
RequestValid int64 `yaml:"request_valid"`
LoginRemember int64 `yaml:"login_remember"`
ConsentRemember int64 `yaml:"consent_remember"`
UserinfoCache int64 `yaml:"userinfo_cache"`
} `yaml:"time"`
}

View File

@ -1,186 +1,8 @@
package types
import "time"
type MisskeyUser struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Host interface{} `json:"host"`
AvatarUrl string `json:"avatarUrl"`
AvatarBlurhash string `json:"avatarBlurhash"`
IsBot bool `json:"isBot"`
IsCat bool `json:"isCat"`
OnlineStatus string `json:"onlineStatus"`
Url interface{} `json:"url"`
Uri interface{} `json:"uri"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastFetchedAt interface{} `json:"lastFetchedAt"`
BannerUrl string `json:"bannerUrl"`
BannerBlurhash string `json:"bannerBlurhash"`
IsLocked bool `json:"isLocked"`
IsSilenced bool `json:"isSilenced"`
IsSuspended bool `json:"isSuspended"`
Description string `json:"description"`
Location string `json:"location"`
Birthday string `json:"birthday"`
Lang string `json:"lang"`
Fields []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"fields"`
FollowersCount int `json:"followersCount"`
FollowingCount int `json:"followingCount"`
NotesCount int `json:"notesCount"`
PinnedNoteIds []string `json:"pinnedNoteIds"`
PinnedNotes []struct {
Id string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UserId string `json:"userId"`
User struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Host interface{} `json:"host"`
AvatarUrl string `json:"avatarUrl"`
AvatarBlurhash string `json:"avatarBlurhash"`
IsBot bool `json:"isBot"`
IsCat bool `json:"isCat"`
OnlineStatus string `json:"onlineStatus"`
} `json:"user"`
Text string `json:"text"`
Cw *string `json:"cw"`
Visibility string `json:"visibility"`
LocalOnly bool `json:"localOnly"`
RenoteCount int `json:"renoteCount"`
RepliesCount int `json:"repliesCount"`
Reactions map[string]int `json:"reactions"`
FileIds []string `json:"fileIds"`
Files []struct {
Id string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Type string `json:"type"`
Md5 string `json:"md5"`
Size int `json:"size"`
IsSensitive bool `json:"isSensitive"`
Blurhash string `json:"blurhash"`
Properties struct {
Width int `json:"width"`
Height int `json:"height"`
} `json:"properties"`
Url string `json:"url"`
ThumbnailUrl string `json:"thumbnailUrl"`
Comment interface{} `json:"comment"`
FolderId interface{} `json:"folderId"`
Folder interface{} `json:"folder"`
UserId interface{} `json:"userId"`
User interface{} `json:"user"`
} `json:"files"`
ReplyId interface{} `json:"replyId"`
RenoteId interface{} `json:"renoteId"`
MyReaction string `json:"myReaction,omitempty"`
} `json:"pinnedNotes"`
PinnedPageId string `json:"pinnedPageId"`
PinnedPage struct {
Id string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
UserId string `json:"userId"`
User struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Host interface{} `json:"host"`
AvatarUrl string `json:"avatarUrl"`
AvatarBlurhash string `json:"avatarBlurhash"`
IsBot bool `json:"isBot"`
IsCat bool `json:"isCat"`
OnlineStatus string `json:"onlineStatus"`
} `json:"user"`
Content []struct {
Id string `json:"id"`
Text string `json:"text,omitempty"`
Type string `json:"type"`
Title string `json:"title,omitempty"`
} `json:"content"`
Variables []interface{} `json:"variables"`
Title string `json:"title"`
Name string `json:"name"`
Summary string `json:"summary"`
HideTitleWhenPinned bool `json:"hideTitleWhenPinned"`
AlignCenter bool `json:"alignCenter"`
Font string `json:"font"`
Script string `json:"script"`
EyeCatchingImageId string `json:"eyeCatchingImageId"`
EyeCatchingImage struct {
Id string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Type string `json:"type"`
Md5 string `json:"md5"`
Size int `json:"size"`
IsSensitive bool `json:"isSensitive"`
Blurhash string `json:"blurhash"`
Properties struct {
Width int `json:"width"`
Height int `json:"height"`
} `json:"properties"`
Url string `json:"url"`
ThumbnailUrl string `json:"thumbnailUrl"`
Comment interface{} `json:"comment"`
FolderId interface{} `json:"folderId"`
Folder interface{} `json:"folder"`
UserId interface{} `json:"userId"`
User interface{} `json:"user"`
} `json:"eyeCatchingImage"`
AttachedFiles []interface{} `json:"attachedFiles"`
LikedCount int `json:"likedCount"`
IsLiked bool `json:"isLiked"`
} `json:"pinnedPage"`
PublicReactions bool `json:"publicReactions"`
FfVisibility string `json:"ffVisibility"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
UsePasswordLessLogin bool `json:"usePasswordLessLogin"`
SecurityKeys bool `json:"securityKeys"`
Roles []struct {
Id string `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
IsModerator bool `json:"isModerator"`
IsAdministrator bool `json:"isAdministrator"`
} `json:"roles"`
AvatarId string `json:"avatarId"`
BannerId string `json:"bannerId"`
IsModerator bool `json:"isModerator"`
IsAdmin bool `json:"isAdmin"`
InjectFeaturedNote bool `json:"injectFeaturedNote"`
ReceiveAnnouncementEmail bool `json:"receiveAnnouncementEmail"`
AlwaysMarkNsfw bool `json:"alwaysMarkNsfw"`
AutoSensitive bool `json:"autoSensitive"`
CarefulBot bool `json:"carefulBot"`
AutoAcceptFollowed bool `json:"autoAcceptFollowed"`
NoCrawle bool `json:"noCrawle"`
IsExplorable bool `json:"isExplorable"`
IsDeleted bool `json:"isDeleted"`
HideOnlineStatus bool `json:"hideOnlineStatus"`
HasUnreadSpecifiedNotes bool `json:"hasUnreadSpecifiedNotes"`
HasUnreadMentions bool `json:"hasUnreadMentions"`
HasUnreadAnnouncement bool `json:"hasUnreadAnnouncement"`
HasUnreadAntenna bool `json:"hasUnreadAntenna"`
HasUnreadChannel bool `json:"hasUnreadChannel"`
HasUnreadMessagingMessage bool `json:"hasUnreadMessagingMessage"`
HasUnreadNotification bool `json:"hasUnreadNotification"`
HasPendingReceivedFollowRequest bool `json:"hasPendingReceivedFollowRequest"`
Integrations struct {
} `json:"integrations"`
MutedWords [][]string `json:"mutedWords"`
MutedInstances []string `json:"mutedInstances"`
MutingNotificationTypes []string `json:"mutingNotificationTypes"`
EmailNotificationTypes []string `json:"emailNotificationTypes"`
ShowTimelineReplies bool `json:"showTimelineReplies"`
type MisskeyUserBase struct {
Username string `json:"username"`
// Ignore other fields
}
// TODO: Find a better way to split necessary fields and additional fields
type MisskeyUser = map[string]any // Just raw json map

View File

@ -13,7 +13,7 @@ import (
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)
userinfoCacheKey := fmt.Sprintf(consts.REDIS_KEY_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)
@ -38,7 +38,7 @@ func GetUserinfo(subject string) (*types.MisskeyUser, error) {
// 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)
accessTokenCacheKey := fmt.Sprintf(consts.REDIS_KEY_USER_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)
@ -52,6 +52,9 @@ func GetUserinfo(subject string) (*types.MisskeyUser, error) {
return nil, err
}
// Append subject as email to userinfo
(*userinfo)["email"] = subject
// Save userinfo into redis
_ = SaveUserinfo(subject, userinfo) // Ignore errors

View File

@ -4,9 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"misso/config"
"misso/consts"
"misso/global"
"misso/types"
"time"
)
func SaveUserinfo(subject string, userinfo *types.MisskeyUser) error {
@ -15,8 +17,8 @@ func SaveUserinfo(subject string, userinfo *types.MisskeyUser) error {
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()
sessUserInfoKey := fmt.Sprintf(consts.REDIS_KEY_USER_INFO, subject)
err = global.Redis.Set(context.Background(), sessUserInfoKey, userinfoBytes, time.Duration(config.Config.Time.UserinfoCache)*time.Second).Err()
if err != nil {
global.Logger.Errorf("Failed to save session user info into redis with error: %v", err)
return err