Merge branch 'dev' of ssh://meowgit.nekoea.red:22222/nihonium/nyanimedb into dev
All checks were successful
Build and Deploy Go App / build (push) Successful in 5m55s
Build and Deploy Go App / deploy (push) Successful in 41s

This commit is contained in:
Iron_Felix 2025-12-06 05:18:33 +03:00
commit e67c9a77ce
7 changed files with 293 additions and 134 deletions

View file

@ -13,6 +13,23 @@ import (
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
) )
const (
BearerAuthScopes = "bearerAuth.Scopes"
)
// GetImpersonationTokenJSONBody defines parameters for GetImpersonationToken.
type GetImpersonationTokenJSONBody struct {
ExternalId *int64 `json:"external_id,omitempty"`
UserId *int64 `json:"user_id,omitempty"`
union json.RawMessage
}
// GetImpersonationTokenJSONBody0 defines parameters for GetImpersonationToken.
type GetImpersonationTokenJSONBody0 = interface{}
// GetImpersonationTokenJSONBody1 defines parameters for GetImpersonationToken.
type GetImpersonationTokenJSONBody1 = interface{}
// PostSignInJSONBody defines parameters for PostSignIn. // PostSignInJSONBody defines parameters for PostSignIn.
type PostSignInJSONBody struct { type PostSignInJSONBody struct {
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
@ -25,6 +42,9 @@ type PostSignUpJSONBody struct {
Pass string `json:"pass"` Pass string `json:"pass"`
} }
// GetImpersonationTokenJSONRequestBody defines body for GetImpersonationToken for application/json ContentType.
type GetImpersonationTokenJSONRequestBody GetImpersonationTokenJSONBody
// PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType. // PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType.
type PostSignInJSONRequestBody PostSignInJSONBody type PostSignInJSONRequestBody PostSignInJSONBody
@ -33,6 +53,9 @@ type PostSignUpJSONRequestBody PostSignUpJSONBody
// ServerInterface represents all server handlers. // ServerInterface represents all server handlers.
type ServerInterface interface { type ServerInterface interface {
// Get service impersontaion token
// (POST /get-impersonation-token)
GetImpersonationToken(c *gin.Context)
// Sign in a user and return JWT // Sign in a user and return JWT
// (POST /sign-in) // (POST /sign-in)
PostSignIn(c *gin.Context) PostSignIn(c *gin.Context)
@ -50,6 +73,21 @@ type ServerInterfaceWrapper struct {
type MiddlewareFunc func(c *gin.Context) type MiddlewareFunc func(c *gin.Context)
// GetImpersonationToken operation middleware
func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) {
c.Set(BearerAuthScopes, []string{})
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.GetImpersonationToken(c)
}
// PostSignIn operation middleware // PostSignIn operation middleware
func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) { func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) {
@ -103,10 +141,41 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
ErrorHandler: errorHandler, ErrorHandler: errorHandler,
} }
router.POST(options.BaseURL+"/get-impersonation-token", wrapper.GetImpersonationToken)
router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn) router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn)
router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp) router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp)
} }
type UnauthorizedErrorResponse struct {
}
type GetImpersonationTokenRequestObject struct {
Body *GetImpersonationTokenJSONRequestBody
}
type GetImpersonationTokenResponseObject interface {
VisitGetImpersonationTokenResponse(w http.ResponseWriter) error
}
type GetImpersonationToken200JSONResponse struct {
// AccessToken JWT access token
AccessToken string `json:"access_token"`
}
func (response GetImpersonationToken200JSONResponse) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetImpersonationToken401Response = UnauthorizedErrorResponse
func (response GetImpersonationToken401Response) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error {
w.WriteHeader(401)
return nil
}
type PostSignInRequestObject struct { type PostSignInRequestObject struct {
Body *PostSignInJSONRequestBody Body *PostSignInJSONRequestBody
} }
@ -127,15 +196,11 @@ func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.Respons
return json.NewEncoder(w).Encode(response) return json.NewEncoder(w).Encode(response)
} }
type PostSignIn401JSONResponse struct { type PostSignIn401Response = UnauthorizedErrorResponse
Error *string `json:"error,omitempty"`
}
func (response PostSignIn401JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error { func (response PostSignIn401Response) VisitPostSignInResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(401) w.WriteHeader(401)
return nil
return json.NewEncoder(w).Encode(response)
} }
type PostSignUpRequestObject struct { type PostSignUpRequestObject struct {
@ -159,6 +224,9 @@ func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.Respons
// StrictServerInterface represents all server handlers. // StrictServerInterface represents all server handlers.
type StrictServerInterface interface { type StrictServerInterface interface {
// Get service impersontaion token
// (POST /get-impersonation-token)
GetImpersonationToken(ctx context.Context, request GetImpersonationTokenRequestObject) (GetImpersonationTokenResponseObject, error)
// Sign in a user and return JWT // Sign in a user and return JWT
// (POST /sign-in) // (POST /sign-in)
PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error) PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error)
@ -179,6 +247,39 @@ type strictHandler struct {
middlewares []StrictMiddlewareFunc middlewares []StrictMiddlewareFunc
} }
// GetImpersonationToken operation middleware
func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) {
var request GetImpersonationTokenRequestObject
var body GetImpersonationTokenJSONRequestBody
if err := ctx.ShouldBindJSON(&body); err != nil {
ctx.Status(http.StatusBadRequest)
ctx.Error(err)
return
}
request.Body = &body
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.GetImpersonationToken(ctx, request.(GetImpersonationTokenRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetImpersonationToken")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(GetImpersonationTokenResponseObject); ok {
if err := validResponse.VisitGetImpersonationTokenResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// PostSignIn operation middleware // PostSignIn operation middleware
func (sh *strictHandler) PostSignIn(ctx *gin.Context) { func (sh *strictHandler) PostSignIn(ctx *gin.Context) {
var request PostSignInRequestObject var request PostSignInRequestObject

View file

@ -10,6 +10,7 @@ paths:
/sign-up: /sign-up:
post: post:
summary: Sign up a new user summary: Sign up a new user
operationId: postSignUp
tags: [Auth] tags: [Auth]
requestBody: requestBody:
required: true required: true
@ -41,6 +42,7 @@ paths:
/sign-in: /sign-in:
post: post:
summary: Sign in a user and return JWT summary: Sign in a user and return JWT
operationId: postSignIn
tags: [Auth] tags: [Auth]
requestBody: requestBody:
required: true required: true
@ -73,88 +75,52 @@ paths:
user_name: user_name:
type: string type: string
"401": "401":
description: Access denied due to invalid credentials $ref: '#/components/responses/UnauthorizedError'
/get-impersonation-token:
post:
summary: Get service impersontaion token
operationId: getImpersonationToken
tags: [Auth]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
user_id:
type: integer
format: int64
external_id:
type: integer
format: int64
oneOf:
- required: ["user_id"]
- required: ["external_id"]
responses:
"200":
description: Generated impersonation access token
content: content:
application/json: application/json:
schema: schema:
type: object type: object
required:
- access_token
properties: properties:
error: access_token:
type: string type: string
example: "Access denied" description: JWT access token
# /auth/verify-token: "401":
# post: $ref: '#/components/responses/UnauthorizedError'
# summary: Verify JWT validity
# tags: [Auth] components:
# requestBody: securitySchemes:
# required: true bearerAuth:
# content: type: http
# application/json: scheme: bearer
# schema: responses:
# type: object UnauthorizedError:
# required: [token] description: Access token is missing or invalid
# properties:
# token:
# type: string
# description: JWT token to validate
# responses:
# "200":
# description: Token validation result
# content:
# application/json:
# schema:
# type: object
# properties:
# valid:
# type: boolean
# description: True if token is valid
# user_id:
# type: string
# nullable: true
# description: User ID extracted from token if valid
# error:
# type: string
# nullable: true
# description: Error message if token is invalid
# /auth/refresh-token:
# post:
# summary: Refresh JWT using a refresh token
# tags: [Auth]
# requestBody:
# required: true
# content:
# application/json:
# schema:
# type: object
# required: [refresh_token]
# properties:
# refresh_token:
# type: string
# description: JWT refresh token obtained from sign-in
# responses:
# "200":
# description: New access (and optionally refresh) token
# content:
# application/json:
# schema:
# type: object
# properties:
# valid:
# type: boolean
# description: True if refresh token was valid
# user_id:
# type: string
# nullable: true
# description: User ID extracted from refresh token
# access_token:
# type: string
# description: New access token
# nullable: true
# refresh_token:
# type: string
# description: New refresh token (optional)
# nullable: true
# error:
# type: string
# nullable: true
# description: Error message if refresh token is invalid

View file

@ -47,10 +47,28 @@ func CheckPassword(password, hash string) (bool, error) {
return argon2id.ComparePasswordAndHash(password, hash) return argon2id.ComparePasswordAndHash(password, hash)
} }
func (s Server) generateImpersonationToken(userID string, impersonated_by string) (accessToken string, err error) {
accessClaims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(15 * time.Minute).Unix(),
"imp_id": impersonated_by,
}
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
if err != nil {
return "", err
}
return accessToken, nil
}
func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) { func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) {
accessClaims := jwt.MapClaims{ accessClaims := jwt.MapClaims{
"user_id": userID, "user_id": userID,
"exp": time.Now().Add(15 * time.Minute).Unix(), "exp": time.Now().Add(15 * time.Minute).Unix(),
//TODO: add created_at
} }
at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = at.SignedString([]byte(s.JwtPrivateKey)) accessToken, err = at.SignedString([]byte(s.JwtPrivateKey))
@ -119,10 +137,7 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
// TODO: return 500 // TODO: return 500
} }
if !ok { if !ok {
err_msg := "invalid credentials" return auth.PostSignIn401Response{}, nil
return auth.PostSignIn401JSONResponse{
Error: &err_msg,
}, nil
} }
accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname) accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname)
@ -144,47 +159,64 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
return result, nil return result, nil
} }
// func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) { func (s Server) GetImpersonationToken(ctx context.Context, req auth.GetImpersonationTokenRequestObject) (auth.GetImpersonationTokenResponseObject, error) {
// valid := false ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context)
// var userID *string if !ok {
// var errStr *string log.Print("failed to get gin context")
// TODO: change to 500
return auth.GetImpersonationToken200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context")
}
// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { token, err := ExtractBearerToken(ginCtx.Request.Header.Get("Authorization"))
// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { if err != nil {
// return nil, fmt.Errorf("unexpected signing method") // TODO: return 500
// } log.Errorf("failed to extract bearer token: %v", err)
// return accessSecret, nil return auth.GetImpersonationToken401Response{}, err
// }) }
log.Printf("got auth token: %s", token)
// if err != nil { ext_service, err := s.db.GetExternalServiceByToken(context.Background(), &token)
// e := err.Error() if err != nil {
// errStr = &e log.Errorf("failed to get external service by token: %v", err)
// return auth.PostAuthVerifyToken200JSONResponse{ return auth.GetImpersonationToken401Response{}, err
// Valid: &valid, // TODO: check err and retyrn 400/500
// UserId: userID, }
// Error: errStr,
// }, nil
// }
// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { var user_id string = ""
// if uid, ok := claims["user_id"].(string); ok {
// valid = true
// userID = &uid
// } else {
// e := "user_id not found in token"
// errStr = &e
// }
// } else {
// e := "invalid token claims"
// errStr = &e
// }
// return auth.PostAuthVerifyToken200JSONResponse{ if req.Body.ExternalId != nil {
// Valid: &valid, user, err := s.db.GetUserByExternalServiceId(context.Background(), sqlc.GetUserByExternalServiceIdParams{
// UserId: userID, ExternalID: fmt.Sprintf("%d", *req.Body.ExternalId),
// Error: errStr, ServiceID: ext_service.ID,
// }, nil })
// } if err != nil {
log.Errorf("failed to get user by external user id: %v", err)
return auth.GetImpersonationToken401Response{}, err
// TODO: check err and retyrn 400/500
}
user_id = fmt.Sprintf("%d", user.ID)
}
if req.Body.UserId != nil {
if user_id != "" && user_id != fmt.Sprintf("%d", *req.Body.UserId) {
log.Error("user_id and external_d are incorrect")
// TODO: 405
return auth.GetImpersonationToken401Response{}, nil
} else {
user_id = fmt.Sprintf("%d", *req.Body.UserId)
}
}
accessToken, err := s.generateImpersonationToken(user_id, fmt.Sprintf("%d", ext_service.ID))
if err != nil {
log.Errorf("failed to generate impersonation token: %v", err)
return auth.GetImpersonationToken401Response{}, err
// TODO: check err and retyrn 400/500
}
return auth.GetImpersonationToken200JSONResponse{AccessToken: accessToken}, nil
}
// func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) { // func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) {
// valid := false // valid := false
@ -236,3 +268,11 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject
// Error: errStr, // Error: errStr,
// }, nil // }, nil
// } // }
func ExtractBearerToken(header string) (string, error) {
const prefix = "Bearer "
if len(header) <= len(prefix) || header[:len(prefix)] != prefix {
return "", fmt.Errorf("invalid bearer token format")
}
return header[len(prefix):], nil
}

View file

@ -9,3 +9,13 @@ INTO users (passhash, nickname)
VALUES (sqlc.arg(passhash), sqlc.arg(nickname)) VALUES (sqlc.arg(passhash), sqlc.arg(nickname))
RETURNING id; RETURNING id;
-- name: GetExternalServiceByToken :one
SELECT *
FROM external_services
WHERE auth_token = sqlc.arg('auth_token');
-- name: GetUserByExternalServiceId :one
SELECT u.*
FROM users u
LEFT JOIN external_ids ei ON eu.user_id = u.id
WHERE ei.external_id = sqlc.arg('external_id') AND ei.service_id = sqlc.arg('service_id');

View file

@ -33,8 +33,6 @@ CREATE TABLE users (
last_login timestamptz last_login timestamptz
); );
CREATE TABLE studios ( CREATE TABLE studios (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
studio_name text NOT NULL UNIQUE, studio_name text NOT NULL UNIQUE,
@ -108,12 +106,13 @@ CREATE TABLE signals (
CREATE TABLE external_services ( CREATE TABLE external_services (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text UNIQUE NOT NULL name text UNIQUE NOT NULL,
auth_token text
); );
CREATE TABLE external_ids ( CREATE TABLE external_ids (
user_id bigint NOT NULL REFERENCES users (id), user_id bigint NOT NULL REFERENCES users (id),
service_id bigint REFERENCES external_services (id), service_id bigint NOT NULL REFERENCES external_services (id),
external_id text NOT NULL external_id text NOT NULL
); );

View file

@ -188,13 +188,14 @@ func (ns NullUsertitleStatusT) Value() (driver.Value, error) {
type ExternalID struct { type ExternalID struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
ServiceID *int64 `json:"service_id"` ServiceID int64 `json:"service_id"`
ExternalID string `json:"external_id"` ExternalID string `json:"external_id"`
} }
type ExternalService struct { type ExternalService struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
AuthToken *string `json:"auth_token"`
} }
type Image struct { type Image struct {

View file

@ -76,6 +76,19 @@ func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams
return i, err return i, err
} }
const getExternalServiceByToken = `-- name: GetExternalServiceByToken :one
SELECT id, name, auth_token
FROM external_services
WHERE auth_token = $1
`
func (q *Queries) GetExternalServiceByToken(ctx context.Context, authToken *string) (ExternalService, error) {
row := q.db.QueryRow(ctx, getExternalServiceByToken, authToken)
var i ExternalService
err := row.Scan(&i.ID, &i.Name, &i.AuthToken)
return i, err
}
const getImageByID = `-- name: GetImageByID :one const getImageByID = `-- name: GetImageByID :one
SELECT id, storage_type, image_path SELECT id, storage_type, image_path
FROM images FROM images
@ -240,6 +253,35 @@ func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMe
return items, nil return items, nil
} }
const getUserByExternalServiceId = `-- name: GetUserByExternalServiceId :one
SELECT u.id, u.avatar_id, u.passhash, u.mail, u.nickname, u.disp_name, u.user_desc, u.creation_date, u.last_login
FROM users u
LEFT JOIN external_ids ei ON eu.user_id = u.id
WHERE ei.external_id = $1 AND ei.service_id = $2
`
type GetUserByExternalServiceIdParams struct {
ExternalID string `json:"external_id"`
ServiceID int64 `json:"service_id"`
}
func (q *Queries) GetUserByExternalServiceId(ctx context.Context, arg GetUserByExternalServiceIdParams) (User, error) {
row := q.db.QueryRow(ctx, getUserByExternalServiceId, arg.ExternalID, arg.ServiceID)
var i User
err := row.Scan(
&i.ID,
&i.AvatarID,
&i.Passhash,
&i.Mail,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
&i.LastLogin,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one const getUserByID = `-- name: GetUserByID :one
SELECT SELECT
t.id as id, t.id as id,