评论登录新增 Google OAuth 与 Telegram Login Widget
- Google:授权码流程(openid email profile),handle 优先用已验证邮箱,头像取 picture - Telegram:官方 widget 直接回传签名资料,后端按官方算法验签(secret=SHA256(bot_token),除 hash 外全字段排序比对)+ auth_date 24h 时效 - providers 列表自动按配置下发(未配置的不显示);widget 型带 bot 用户名 - 三个登录方式共用读者会话与 upsert;Telegram 走 POST 无跳转 - .env.example 补 ONE_GOOGLE_* / ONE_TELEGRAM_* 模板
This commit is contained in:
@@ -8,3 +8,13 @@ Bucket=
|
||||
# 回调地址填 http://localhost:8080/api/auth/callback/github(线上换成正式域名)
|
||||
ONE_GITHUB_CLIENT_ID=
|
||||
ONE_GITHUB_CLIENT_SECRET=
|
||||
|
||||
# Telegram 登录(Login Widget):@BotFather 建 bot 拿 token,
|
||||
# 再用 /setdomain 把域名登记给 bot(本地开发填 localhost)
|
||||
ONE_TELEGRAM_BOT=
|
||||
ONE_TELEGRAM_BOT_TOKEN=
|
||||
|
||||
# Google 登录:https://console.cloud.google.com/apis/credentials 建 OAuth 客户端,
|
||||
# 回调地址填 http://localhost:8080/api/auth/callback/google(线上换成正式域名)
|
||||
ONE_GOOGLE_CLIENT_ID=
|
||||
ONE_GOOGLE_CLIENT_SECRET=
|
||||
|
||||
@@ -32,6 +32,9 @@ type API struct {
|
||||
AdminSessions interface {
|
||||
Verify(token string) (string, error)
|
||||
}
|
||||
// 其余登录方式(main.go 装配,未配置的自动不开放)
|
||||
GG auth.Google
|
||||
TG auth.Telegram
|
||||
}
|
||||
|
||||
func (a *API) Routes() http.Handler {
|
||||
@@ -49,6 +52,9 @@ func (a *API) Routes() http.Handler {
|
||||
mux.HandleFunc("/api/auth/logout", a.authLogout)
|
||||
mux.HandleFunc("/api/auth/github/login", a.githubLogin)
|
||||
mux.HandleFunc("/api/auth/callback/github", a.githubCallback)
|
||||
mux.HandleFunc("/api/auth/google/login", a.googleLogin)
|
||||
mux.HandleFunc("/api/auth/callback/google", a.googleCallback)
|
||||
mux.HandleFunc("/api/auth/telegram", a.telegramAuth)
|
||||
mux.HandleFunc("/api/comments", a.comments)
|
||||
mux.HandleFunc("/api/comments/", a.commentSub)
|
||||
mux.HandleFunc("/api/site", a.site)
|
||||
|
||||
@@ -76,16 +76,6 @@ func (a *API) ownerReader() (model.Reader, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func (a *API) authProviders(w http.ResponseWriter, r *http.Request) {
|
||||
providers := []map[string]any{}
|
||||
if a.GH.Enabled() {
|
||||
providers = append(providers, map[string]any{
|
||||
"id": "github", "label": "GitHub", "kind": "redirect",
|
||||
})
|
||||
}
|
||||
httpx.OK(w, map[string]any{"providers": providers})
|
||||
}
|
||||
|
||||
func (a *API) authMe(w http.ResponseWriter, r *http.Request) {
|
||||
var user any // 匿名时 {user: null},前端判空即「未登录」
|
||||
if reader, ok, err := a.resolveReader(r); err == nil && ok {
|
||||
@@ -158,18 +148,7 @@ func (a *API) githubCallback(w http.ResponseWriter, r *http.Request) {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
token, _ := a.ReaderSessions.Issue(reader.ID)
|
||||
http.SetCookie(w, &http.Cookie{Name: auth.ReaderCookie, Value: token, Path: "/",
|
||||
HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: int((30 * 24 * time.Hour).Seconds())})
|
||||
// 回到发起登录的前台;没有记录(直接敲 URL 进来的)就回站点根
|
||||
back := a.Cfg.SiteURL
|
||||
if ck, err := r.Cookie(oauthBackCook); err == nil && ck.Value != "" {
|
||||
if u, err := url.Parse(ck.Value); err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" && u.Path == "" {
|
||||
back = u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: oauthBackCook, Value: "", Path: "/", MaxAge: -1})
|
||||
http.Redirect(w, r, back, http.StatusFound)
|
||||
http.Redirect(w, r, a.issueReaderCookie(w, r, reader.ID), http.StatusFound)
|
||||
}
|
||||
|
||||
func randHex(n int) string {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// Google / Telegram 登录的 HTTP 端点。GitHub 的在 comments.go——
|
||||
// 三个 Provider 共用同一套读者会话与 upsert 逻辑,只是凭据交换方式不同:
|
||||
// GitHub / Google 是授权码换 token,Telegram 是官方 widget 直接带签名资料。
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"oneblog/internal/auth"
|
||||
"oneblog/internal/httpx"
|
||||
"oneblog/internal/model"
|
||||
)
|
||||
|
||||
// authProviders 列出已配置的登录方式。
|
||||
// redirect 型前端直接跳 /api/auth/{id}/login;widget 型(Telegram)
|
||||
// 前端内联官方脚本,需要 bot 用户名。
|
||||
func (a *API) authProviders(w http.ResponseWriter, r *http.Request) {
|
||||
providers := []map[string]any{}
|
||||
if a.GH.Enabled() {
|
||||
providers = append(providers, map[string]any{
|
||||
"id": "github", "label": "GitHub", "kind": "redirect",
|
||||
})
|
||||
}
|
||||
if a.GG.Enabled() {
|
||||
providers = append(providers, map[string]any{
|
||||
"id": "google", "label": "Google", "kind": "redirect",
|
||||
})
|
||||
}
|
||||
if a.TG.Enabled() {
|
||||
providers = append(providers, map[string]any{
|
||||
"id": "telegram", "label": "Telegram", "kind": "widget", "login": a.TG.Bot,
|
||||
})
|
||||
}
|
||||
httpx.OK(w, map[string]any{"providers": providers})
|
||||
}
|
||||
|
||||
// issueReaderCookie 登录成功后的公共收尾:发读者会话 + 决定跳回去的地址
|
||||
func (a *API) issueReaderCookie(w http.ResponseWriter, r *http.Request, readerID int64) string {
|
||||
token, _ := a.ReaderSessions.Issue(readerID)
|
||||
http.SetCookie(w, &http.Cookie{Name: auth.ReaderCookie, Value: token, Path: "/",
|
||||
HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: int((30 * 24 * time.Hour).Seconds())})
|
||||
// 回到发起登录的前台;没有记录(直接敲 URL 进来的)就回站点根
|
||||
back := a.Cfg.SiteURL
|
||||
if ck, err := r.Cookie(oauthBackCook); err == nil && ck.Value != "" {
|
||||
if u, err := url.Parse(ck.Value); err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" && u.Path == "" {
|
||||
back = u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: oauthBackCook, Value: "", Path: "/", MaxAge: -1})
|
||||
return back
|
||||
}
|
||||
|
||||
// googleLogin 跳 Google 授权页(state 防 CSRF 同 GitHub)
|
||||
func (a *API) googleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.GG.Enabled() {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
state := randHex(16)
|
||||
http.SetCookie(w, &http.Cookie{Name: oauthStateCook, Value: state, Path: "/",
|
||||
HttpOnly: true, MaxAge: 600})
|
||||
if ref := r.Referer(); ref != "" {
|
||||
if u, err := url.Parse(ref); err == nil && u.Scheme != "" && u.Host != "" {
|
||||
http.SetCookie(w, &http.Cookie{Name: oauthBackCook,
|
||||
Value: u.Scheme + "://" + u.Host, Path: "/", HttpOnly: true, MaxAge: 600})
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, a.GG.LoginURL(a.Cfg.SiteURL+"/api/auth/callback/google", state), http.StatusFound)
|
||||
}
|
||||
|
||||
// googleCallback 用 code 换身份:Google 用户 → upsert 读者 → 发会话
|
||||
func (a *API) googleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.GG.Enabled() {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
ck, err := r.Cookie(oauthStateCook)
|
||||
if err != nil || ck.Value == "" || ck.Value != r.FormValue("state") {
|
||||
httpx.BadRequest(w, "state 不匹配,请重新登录")
|
||||
return
|
||||
}
|
||||
accessToken, err := a.GG.Exchange(r.Context(), r.FormValue("code"), a.Cfg.SiteURL+"/api/auth/callback/google")
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
u, err := a.GG.FetchUser(r.Context(), accessToken)
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
// handle 优先用已验证邮箱(可读),否则退回 sub(Google 的稳定唯一 id)
|
||||
handle := u.Sub
|
||||
if u.EmailVerified && u.Email != "" {
|
||||
handle = u.Email
|
||||
}
|
||||
name := u.Name
|
||||
if name == "" {
|
||||
name = handle
|
||||
}
|
||||
reader, err := a.Store.UpsertReader(model.Reader{
|
||||
Provider: "google", Handle: handle, Name: name,
|
||||
AvatarURL: u.Picture,
|
||||
})
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, a.issueReaderCookie(w, r, reader.ID), http.StatusFound)
|
||||
}
|
||||
|
||||
// telegramAuth 校验 Login Widget 回传的签名资料并登录。
|
||||
// 前端把 widget 的 user 对象原样 POST 过来(见 reader.js 的 oneTelegramAuth)。
|
||||
func (a *API) telegramAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.TG.Enabled() {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
// 原样读 body:验签必须用收到的全部字段(官方规则),
|
||||
// 身份字段再单独解一次
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpx.BadRequest(w, "invalid body")
|
||||
return
|
||||
}
|
||||
var fields map[string]any
|
||||
if err := json.Unmarshal(body, &fields); err != nil || len(fields) == 0 {
|
||||
httpx.BadRequest(w, "invalid body")
|
||||
return
|
||||
}
|
||||
if err := a.TG.VerifyMap(fields); err != nil {
|
||||
httpx.Error(w, http.StatusForbidden, "Telegram 登录校验失败,请重试")
|
||||
return
|
||||
}
|
||||
var in auth.TelegramUser
|
||||
if err := json.Unmarshal(body, &in); err != nil || in.IDInt() == 0 {
|
||||
httpx.BadRequest(w, "invalid body")
|
||||
return
|
||||
}
|
||||
handle := in.Username
|
||||
if handle == "" {
|
||||
// 没有公开 username 的用户用数字 id,保证 provider+handle 稳定唯一
|
||||
handle = strconv.FormatInt(in.IDInt(), 10)
|
||||
}
|
||||
reader, err := a.Store.UpsertReader(model.Reader{
|
||||
Provider: "telegram", Handle: handle, Name: in.DisplayName(),
|
||||
AvatarURL: in.PhotoURL,
|
||||
URL: tgProfileURL(in.Username),
|
||||
})
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
// 会话同样落 httpOnly cookie,前端 POST 完刷新 /api/auth/me 即可见
|
||||
token, _ := a.ReaderSessions.Issue(reader.ID)
|
||||
http.SetCookie(w, &http.Cookie{Name: auth.ReaderCookie, Value: token, Path: "/",
|
||||
HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: int((30 * 24 * time.Hour).Seconds())})
|
||||
httpx.OK(w, map[string]any{"user": map[string]any{
|
||||
"id": reader.ID, "name": reader.Name, "handle": reader.Handle,
|
||||
"avatar_url": reader.AvatarURL, "url": reader.URL,
|
||||
"provider": reader.Provider, "is_owner": false, "banned": reader.Banned,
|
||||
}})
|
||||
}
|
||||
|
||||
func tgProfileURL(username string) string {
|
||||
if username == "" {
|
||||
return ""
|
||||
}
|
||||
return "https://t.me/" + username
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Google OAuth2(OIDC 简化用法:openid email profile 三个 scope,
|
||||
// userinfo 接口拿资料)。配置来源 .env 的 ONE_GOOGLE_CLIENT_ID / SECRET。
|
||||
const (
|
||||
googleAuthURL = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
googleTokenURL = "https://oauth2.googleapis.com/token"
|
||||
googleUserURL = "https://openidconnect.googleapis.com/v1/userinfo"
|
||||
)
|
||||
|
||||
type Google struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
func (g Google) Enabled() bool { return g.ClientID != "" && g.ClientSecret != "" }
|
||||
|
||||
func (g Google) LoginURL(redirectURI, state string) string {
|
||||
v := url.Values{}
|
||||
v.Set("client_id", g.ClientID)
|
||||
v.Set("redirect_uri", redirectURI)
|
||||
v.Set("response_type", "code")
|
||||
v.Set("scope", "openid email profile")
|
||||
v.Set("state", state)
|
||||
return googleAuthURL + "?" + v.Encode()
|
||||
}
|
||||
|
||||
// GoogleUser 是 userinfo 接口里我们关心的字段。
|
||||
// sub 是 Google 账号的稳定唯一 id;邮箱需要已验证才当 handle 用。
|
||||
type GoogleUser struct {
|
||||
Sub string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
Picture string `json:"picture"`
|
||||
}
|
||||
|
||||
func (g Google) Exchange(ctx context.Context, code, redirectURI string) (string, error) {
|
||||
v := url.Values{}
|
||||
v.Set("client_id", g.ClientID)
|
||||
v.Set("client_secret", g.ClientSecret)
|
||||
v.Set("code", code)
|
||||
v.Set("redirect_uri", redirectURI)
|
||||
v.Set("grant_type", "authorization_code")
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, googleTokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
var out struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil || out.AccessToken == "" {
|
||||
return "", fmt.Errorf("google: token exchange failed")
|
||||
}
|
||||
return out.AccessToken, nil
|
||||
}
|
||||
|
||||
func (g Google) FetchUser(ctx context.Context, accessToken string) (GoogleUser, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, googleUserURL, nil)
|
||||
if err != nil {
|
||||
return GoogleUser{}, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return GoogleUser{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return GoogleUser{}, fmt.Errorf("google: userinfo %d", res.StatusCode)
|
||||
}
|
||||
var u GoogleUser
|
||||
if err := json.NewDecoder(res.Body).Decode(&u); err != nil {
|
||||
return GoogleUser{}, err
|
||||
}
|
||||
if u.Sub == "" {
|
||||
return GoogleUser{}, fmt.Errorf("google: userinfo missing sub")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Telegram Login Widget:没有授权码流程,官方脚本把用户资料连同
|
||||
// hash 一起交给前端,前端 POST 到 /api/auth/telegram,后端用 bot
|
||||
// token 验签。算法见官方文档:secret = SHA256(bot_token),
|
||||
// data-check-string 是除 hash 外所有收到的字段按 key 排序的 "k=v" 行。
|
||||
type Telegram struct {
|
||||
Bot string // bot 用户名(不含 @),下发给 widget
|
||||
Token string // bot token,只用于验签,不出后端
|
||||
}
|
||||
|
||||
func (t Telegram) Enabled() bool { return t.Bot != "" && t.Token != "" }
|
||||
|
||||
// telegramAuthTTL 是验签窗口:widget 回传的 auth_date 超过它视为过期
|
||||
const telegramAuthTTL = 24 * time.Hour
|
||||
|
||||
// TelegramUser 是 widget 回传的身份字段(验签通过后从中取)。
|
||||
// widget 把所有值都当字符串发(id 也不例外),但前端测试或手工
|
||||
// 调用可能发数字 —— FlexStr 两种都收。
|
||||
type TelegramUser struct {
|
||||
ID FlexStr `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Username string `json:"username"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
}
|
||||
|
||||
// FlexStr 兼容 JSON 里的字符串和数字
|
||||
type FlexStr string
|
||||
|
||||
func (f *FlexStr) UnmarshalJSON(b []byte) error {
|
||||
if len(b) > 0 && b[0] == '"' {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
*f = FlexStr(s)
|
||||
return nil
|
||||
}
|
||||
*f = FlexStr(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FlexStr) Int64() (int64, error) {
|
||||
return strconv.ParseInt(strings.Trim(string(f), `"`), 10, 64)
|
||||
}
|
||||
|
||||
func (u TelegramUser) IDInt() int64 { id, _ := u.ID.Int64(); return id }
|
||||
|
||||
func (u TelegramUser) DisplayName() string {
|
||||
name := strings.TrimSpace(u.FirstName + " " + u.LastName)
|
||||
if name == "" {
|
||||
name = u.Username
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// normalize 把 JSON 值规整成 widget 发送时的字符串形态
|
||||
// (widget 的数值字段也是字符串,但调用方可能发真数字)
|
||||
func normalize(v any) string {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return x
|
||||
case float64:
|
||||
return strconv.FormatInt(int64(x), 10)
|
||||
case json.Number:
|
||||
return x.String()
|
||||
case bool:
|
||||
return strconv.FormatBool(x)
|
||||
default:
|
||||
return fmt.Sprint(x)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyMap 校验 hash 与时效。fields 是前端原样 POST 的 JSON 对象;
|
||||
// exclude 里的 key(我们附加的非 Telegram 字段)不参与验签。
|
||||
func (t Telegram) VerifyMap(fields map[string]any, exclude ...string) error {
|
||||
hash := normalize(fields["hash"])
|
||||
if hash == "" {
|
||||
return fmt.Errorf("telegram: missing hash")
|
||||
}
|
||||
authDate, err := strconv.ParseInt(normalize(fields["auth_date"]), 10, 64)
|
||||
if err != nil || authDate == 0 || time.Since(time.Unix(authDate, 0)) > telegramAuthTTL {
|
||||
return fmt.Errorf("telegram: auth_date expired")
|
||||
}
|
||||
skip := make(map[string]bool, len(exclude)+1)
|
||||
skip["hash"] = true
|
||||
for _, k := range exclude {
|
||||
skip[k] = true
|
||||
}
|
||||
keys := make([]string, 0, len(fields))
|
||||
for k := range fields {
|
||||
if !skip[k] {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
lines := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
lines = append(lines, k+"="+normalize(fields[k]))
|
||||
}
|
||||
secret := sha256.Sum256([]byte(t.Token))
|
||||
mac := hmac.New(sha256.New, secret[:])
|
||||
mac.Write([]byte(strings.Join(lines, "\n")))
|
||||
if !hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(strings.ToLower(hash))) {
|
||||
return fmt.Errorf("telegram: hash mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -25,6 +25,15 @@ type Config struct {
|
||||
GitHubClientID string
|
||||
GitHubClientSecret string
|
||||
|
||||
// 评论区 Google 登录(OAuth 客户端凭据,Google Cloud Console 创建)
|
||||
GoogleClientID string
|
||||
GoogleClientSecret string
|
||||
|
||||
// 评论区 Telegram 登录(Login Widget):Bot 是用户名(不含 @,下发给
|
||||
// 前端 widget),Token 用于验签。两者都要,缺一该入口不开放。
|
||||
TelegramBot string
|
||||
TelegramToken string
|
||||
|
||||
// 对象存储(文件上传)。变量名与站主 .env 里的写法一致(站主已整理):
|
||||
// S3Api = R2 的 S3 API 端点(https://<账户ID>.r2.cloudflarestorage.com,
|
||||
// 控制台 R2 概览可复制),上传走它 —— 公开域名收不了上传请求
|
||||
@@ -129,6 +138,10 @@ func Load() (*Config, error) {
|
||||
// 评论区 GitHub 登录(OAuth App 凭据,站主在 GitHub 上创建后填入)
|
||||
c.GitHubClientID = getenv("ONE_GITHUB_CLIENT_ID", "")
|
||||
c.GitHubClientSecret = getenv("ONE_GITHUB_CLIENT_SECRET", "")
|
||||
c.GoogleClientID = getenv("ONE_GOOGLE_CLIENT_ID", "")
|
||||
c.GoogleClientSecret = getenv("ONE_GOOGLE_CLIENT_SECRET", "")
|
||||
c.TelegramBot = getenv("ONE_TELEGRAM_BOT", "")
|
||||
c.TelegramToken = getenv("ONE_TELEGRAM_BOT_TOKEN", "")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ func main() {
|
||||
Blobs: blobs,
|
||||
ReaderSessions: readerSessions,
|
||||
GH: auth.GitHub{ClientID: cfg.GitHubClientID, ClientSecret: cfg.GitHubClientSecret},
|
||||
GG: auth.Google{ClientID: cfg.GoogleClientID, ClientSecret: cfg.GoogleClientSecret},
|
||||
TG: auth.Telegram{Bot: cfg.TelegramBot, Token: cfg.TelegramToken},
|
||||
AdminSessions: adminSessions,
|
||||
}
|
||||
adminAPI := admin.NewAPI(st, cfg, adminSessions)
|
||||
|
||||
Reference in New Issue
Block a user