Draft: feat login

This commit is contained in:
2025-09-05 03:59:25 +08:00
parent 6d7074198f
commit 3a9ffe7857
43 changed files with 1894 additions and 91 deletions

View File

@@ -1,19 +1,105 @@
// Code generated by swaggo/swag. DO NOT EDIT.
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag/v2"
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},"swagger":"2.0","info":{"description":"{{escape .Description}}","title":"{{.Title}}","contact":{},"license":{"name":"0BSD"},"version":"{{.Version}}"},"host":"{{.Host}}","basePath":"{{.BasePath}}","paths":{}}`
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"license": {
"name": "0BSD"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/user": {
"get": {
"consumes": [
"application/json"
],
"parameters": [
{
"type": "string",
"description": "Username to be queried",
"name": "username",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
}
}
},
"/auth/logout": {
"post": {
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/auth/register": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "query params",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.User"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
}
}
}
},
"definitions": {
"models.User": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "2.0",
Version: "0.0.1-alpha",
Host: "",
BasePath: "/",
Schemes: []string{},
Title: "NASAOJ v3",
Title: "Intro. to Network Programming Game",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,

View File

@@ -1 +1,86 @@
{"swagger":"2.0","info":{"title":"NASAOJ v3","contact":{},"license":{"name":"0BSD"},"version":"2.0"},"basePath":"/","paths":{}}
{
"swagger": "2.0",
"info": {
"title": "Intro. to Network Programming Game",
"contact": {},
"license": {
"name": "0BSD"
},
"version": "0.0.1-alpha"
},
"basePath": "/",
"paths": {
"/api/user": {
"get": {
"consumes": [
"application/json"
],
"parameters": [
{
"type": "string",
"description": "Username to be queried",
"name": "username",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
}
}
},
"/auth/logout": {
"post": {
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/auth/register": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "query params",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.User"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
}
}
}
},
"definitions": {
"models.User": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
}
}

View File

@@ -1,9 +1,53 @@
basePath: /
definitions:
models.User:
properties:
password:
type: string
username:
type: string
type: object
info:
contact: {}
license:
name: 0BSD
title: NASAOJ v3
version: "2.0"
paths: {}
title: Intro. to Network Programming Game
version: 0.0.1-alpha
paths:
/api/user:
get:
consumes:
- application/json
parameters:
- description: Username to be queried
in: query
name: username
required: true
type: string
responses:
"200":
description: OK
"400":
description: Bad Request
/auth/logout:
post:
responses:
"200":
description: OK
/auth/register:
post:
consumes:
- application/json
parameters:
- description: query params
in: body
name: request
required: true
schema:
$ref: '#/definitions/models.User'
responses:
"200":
description: OK
"400":
description: Bad Request
swagger: "2.0"

View File

@@ -0,0 +1,26 @@
package api
import (
"net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares"
"github.com/uptrace/bunrouter"
)
func (self *Handlers) GetLobbyUsers(
w http.ResponseWriter,
req bunrouter.Request,
) error {
ctx := req.Context()
userStatuses, err := self.db.GetUserStatuses(ctx)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to get user statuses",
OriginError: err,
}
}
return bunrouter.JSON(w, userStatuses)
}

View File

@@ -0,0 +1,55 @@
package api
import (
"database/sql"
"net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares"
"github.com/uptrace/bunrouter"
)
type GetUserOutput struct {
Username string `json:"username"`
}
// GetUser
//
// @Accept json
// @Param username query string true "Username to be queried"
// @Success 200
// @Failure 400
// @Router /api/user [get]
func (self *Handlers) GetUser(
w http.ResponseWriter,
req bunrouter.Request,
) error {
ctx := req.Context()
username := req.URL.Query().Get("username")
if username == "" {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "must provide username",
}
}
user, err := self.db.GetUser(ctx, username)
if err != nil {
if err == sql.ErrNoRows {
return middlewares.HTTPError{
StatusCode: http.StatusNotFound,
Message: "user not exist",
OriginError: err,
}
}
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to get user",
OriginError: err,
}
}
return bunrouter.JSON(w, GetUserOutput{
Username: user.Username,
})
}

View File

@@ -0,0 +1,13 @@
package api
import (
"gitea.konchin.com/ytshih/inp2025/game/interfaces"
)
type Handlers struct {
db interfaces.Database
}
func NewHandlers(db interfaces.Database) *Handlers {
return &Handlers{db: db}
}

View File

@@ -1 +1,37 @@
package auth
import (
"net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares"
"gitea.konchin.com/ytshih/inp2025/game/models"
"gitea.konchin.com/ytshih/inp2025/game/types"
"gitea.konchin.com/ytshih/inp2025/game/utils"
"github.com/uptrace/bunrouter"
)
func (self *Handlers) GetLogin(
w http.ResponseWriter,
req bunrouter.Request,
) error {
ctx := req.Context()
user, ok := ctx.Value(types.User("")).(models.User)
if !ok {
return middlewares.HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "user not found",
}
}
err := self.db.InsertUserStatus(ctx,
models.UserStatus{Username: user.Username})
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to update user status",
OriginError: err,
}
}
return utils.Success(w)
}

View File

@@ -0,0 +1,13 @@
package auth
import (
"gitea.konchin.com/ytshih/inp2025/game/interfaces"
)
type Handlers struct {
db interfaces.Database
}
func NewHandlers(db interfaces.Database) *Handlers {
return &Handlers{db: db}
}

View File

@@ -0,0 +1,40 @@
package auth
import (
"net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares"
"gitea.konchin.com/ytshih/inp2025/game/models"
"gitea.konchin.com/ytshih/inp2025/game/types"
"gitea.konchin.com/ytshih/inp2025/game/utils"
"github.com/uptrace/bunrouter"
)
// PostLogout
//
// @Success 200
// @Router /auth/logout [post]
func (self *Handlers) PostLogout(
w http.ResponseWriter,
req bunrouter.Request,
) error {
ctx := req.Context()
user, ok := ctx.Value(types.User("")).(models.User)
if !ok {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "missing user",
}
}
if err := self.db.DeleteUserStatus(ctx, user.Username); err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to delete user status",
OriginError: err,
}
}
return utils.Success(w)
}

View File

@@ -0,0 +1,62 @@
package auth
import (
"encoding/json"
"io"
"net/http"
"gitea.konchin.com/ytshih/inp2025/game/backend/middlewares"
"gitea.konchin.com/ytshih/inp2025/game/models"
"gitea.konchin.com/ytshih/inp2025/game/types"
"gitea.konchin.com/ytshih/inp2025/game/utils"
"github.com/uptrace/bunrouter"
)
// PostRegister
//
// @Accept json
// @Param request body models.User true "query params"
// @Success 200
// @Failure 400
// @Router /auth/register [post]
func (self *Handlers) PostRegister(
w http.ResponseWriter,
req bunrouter.Request,
) error {
ctx := req.Context()
b, err := io.ReadAll(req.Body)
if err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to read body payload",
OriginError: err,
}
}
var user models.User
if err := json.Unmarshal(b, &user); err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "failed to unmarshal json into user",
OriginError: err,
}
}
if err := self.db.InsertUser(ctx, user); err != nil {
if err == types.UsernameConflictError {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
Message: "username already exist",
OriginError: err,
}
}
return middlewares.HTTPError{
StatusCode: http.StatusInternalServerError,
Message: "failed to insert user",
OriginError: err,
}
}
return utils.Success(w)
}

View File

@@ -7,7 +7,9 @@ import (
"github.com/uptrace/bunrouter"
)
func AccessLog(next bunrouter.HandlerFunc) bunrouter.HandlerFunc {
func (self *Handlers) AccessLog(
next bunrouter.HandlerFunc,
) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
tracing.Logger.Ctx(req.Context()).
Info(req.Method + " " + req.Params().Route())

View File

@@ -0,0 +1,56 @@
package middlewares
import (
"context"
"net/http"
"gitea.konchin.com/ytshih/inp2025/game/models"
"gitea.konchin.com/ytshih/inp2025/game/tracing"
"gitea.konchin.com/ytshih/inp2025/game/types"
"github.com/uptrace/bunrouter"
"go.uber.org/zap"
)
func (self *Handlers) Auth(
next bunrouter.HandlerFunc,
) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
ctx := req.Context()
username, password, ok := req.BasicAuth()
if !ok {
return HTTPError{
StatusCode: http.StatusBadRequest,
Message: "basic auth wrong format",
}
}
dbUser, err := self.db.GetUser(ctx, username)
if err != nil {
return HTTPError{
StatusCode: http.StatusBadRequest,
Message: "username not exist",
OriginError: err,
}
}
if dbUser.Password != password {
tracing.Logger.Ctx(ctx).
Debug("password input",
zap.String("input.password", password),
zap.String("dbuser.password", dbUser.Password))
return HTTPError{
StatusCode: http.StatusUnauthorized,
Message: "password incorrect",
}
}
user := models.User{
Username: username,
Password: password,
}
ctx = context.WithValue(ctx, types.User(""), user)
return next(w, req.WithContext(ctx))
}
}

View File

@@ -10,7 +10,9 @@ import (
"go.opentelemetry.io/otel/trace"
)
func BunrouterOtel(next bunrouter.HandlerFunc) bunrouter.HandlerFunc {
func (self *Handlers) BunrouterOtel(
next bunrouter.HandlerFunc,
) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
span := trace.SpanFromContext(req.Context())
if !span.IsRecording() {

View File

@@ -15,8 +15,6 @@ type HTTPError struct {
StatusCode int `json:"code"`
Message string `json:"message"`
OriginError error `json:"-"`
TraceID string `json:"traceId"`
}
func (e HTTPError) Error() string {
@@ -31,7 +29,9 @@ func NewHTTPError(err error) HTTPError {
}
}
func ErrorHandler(next bunrouter.HandlerFunc) bunrouter.HandlerFunc {
func (self *Handlers) ErrorHandler(
next bunrouter.HandlerFunc,
) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
err := next(w, req)
ctx := req.Context()
@@ -53,7 +53,6 @@ func ErrorHandler(next bunrouter.HandlerFunc) bunrouter.HandlerFunc {
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
span := trace.SpanFromContext(ctx)
httpErr.TraceID = span.SpanContext().TraceID().String()
span.SetAttributes(
attribute.Int("http.response.status_code", httpErr.StatusCode),
)

View File

@@ -0,0 +1,11 @@
package middlewares
import "gitea.konchin.com/ytshih/inp2025/game/interfaces"
type Handlers struct {
db interfaces.Database
}
func NewHandlers(db interfaces.Database) *Handlers {
return &Handlers{db: db}
}