v0.0.12 Добавлен avatar для пользователей, а также реализован резолвер получения всех пользователей
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
madipo2611 2025-08-04 23:01:31 +03:00
parent 7e1fad19c2
commit 562c4c2228
10 changed files with 297 additions and 24 deletions

View File

@ -96,9 +96,11 @@ func main() {
// Запуск сервера в отдельной горутине
go func() {
log.Println("server started")
if err := server.Run(); err != nil {
log.Printf("server error: %v", err)
}
}()
// Ожидание сигнала завершения

View File

@ -3,7 +3,6 @@ package config
import (
"github.com/caarlos0/env/v8"
"github.com/joho/godotenv"
"log"
"time"
)
@ -40,9 +39,5 @@ func Load() (*Config, error) {
if err := env.Parse(cfg); err != nil {
return nil, err
}
// Для отладки (можно убрать в продакшене)
log.Printf("Config loaded: %+v", cfg)
return cfg, nil
}

View File

@ -8,6 +8,7 @@ import (
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
Email string `json:"email"`
EmailConfirmationToken string `json:"-"`
EmailConfirmedAt *time.Time `json:"emailConfirmedAt,omitempty"`

View File

@ -113,7 +113,7 @@ type ComplexityRoot struct {
SendMessage func(childComplexity int, receiverID int, content string) int
TerminateSession func(childComplexity int, sessionID int) int
UnlikePost func(childComplexity int, postID int) int
UpdateProfile func(childComplexity int, username string, email string) int
UpdateProfile func(childComplexity int, username string, email string, avatar string) int
}
Post struct {
@ -137,6 +137,7 @@ type ComplexityRoot struct {
Post func(childComplexity int, id int) int
Posts func(childComplexity int) int
User func(childComplexity int, id int) int
Users func(childComplexity int) int
}
Session struct {
@ -160,6 +161,7 @@ type ComplexityRoot struct {
}
User struct {
Avatar func(childComplexity int) int
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
EmailConfirmedAt func(childComplexity int) int
@ -197,7 +199,7 @@ type MutationResolver interface {
CreateComment(ctx context.Context, postID int, content string) (*domain.Comment, error)
LikePost(ctx context.Context, postID int) (*domain.Like, error)
UnlikePost(ctx context.Context, postID int) (bool, error)
UpdateProfile(ctx context.Context, username string, email string) (*domain.User, error)
UpdateProfile(ctx context.Context, username string, email string, avatar string) (*domain.User, error)
ChangePassword(ctx context.Context, oldPassword string, newPassword string) (bool, error)
SendMessage(ctx context.Context, receiverID int, content string) (*domain.Message, error)
MarkAsRead(ctx context.Context, messageID int) (bool, error)
@ -222,6 +224,7 @@ type QueryResolver interface {
Post(ctx context.Context, id int) (*domain.Post, error)
Posts(ctx context.Context) ([]*domain.Post, error)
User(ctx context.Context, id int) (*domain.User, error)
Users(ctx context.Context) ([]*domain.User, error)
GetChatHistory(ctx context.Context, userID int) ([]*domain.Message, error)
GetUserChats(ctx context.Context) ([]*ChatSession, error)
MySessions(ctx context.Context) ([]*domain.Session, error)
@ -622,7 +625,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return 0, false
}
return e.complexity.Mutation.UpdateProfile(childComplexity, args["username"].(string), args["email"].(string)), true
return e.complexity.Mutation.UpdateProfile(childComplexity, args["username"].(string), args["email"].(string), args["avatar"].(string)), true
case "Post.author":
if e.complexity.Post.Author == nil {
@ -758,6 +761,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Query.User(childComplexity, args["id"].(int)), true
case "Query.users":
if e.complexity.Query.Users == nil {
break
}
return e.complexity.Query.Users(childComplexity), true
case "Session.device":
if e.complexity.Session.Device == nil {
break
@ -835,6 +845,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Tokens.RefreshTokenExpires(childComplexity), true
case "User.avatar":
if e.complexity.User.Avatar == nil {
break
}
return e.complexity.User.Avatar(childComplexity), true
case "User.createdAt":
if e.complexity.User.CreatedAt == nil {
break
@ -1232,6 +1249,11 @@ func (ec *executionContext) field_Mutation_updateProfile_args(ctx context.Contex
return nil, err
}
args["email"] = arg1
arg2, err := processArgField(ctx, rawArgs, "avatar", ec.unmarshalNString2string)
if err != nil {
return nil, err
}
args["avatar"] = arg2
return args, nil
}
@ -1374,6 +1396,8 @@ func (ec *executionContext) fieldContext_ChatSession_user(_ context.Context, fie
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -1688,6 +1712,8 @@ func (ec *executionContext) fieldContext_Comment_author(_ context.Context, field
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -2164,6 +2190,8 @@ func (ec *executionContext) fieldContext_Like_user(_ context.Context, field grap
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -2310,6 +2338,8 @@ func (ec *executionContext) fieldContext_Message_sender(_ context.Context, field
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -2368,6 +2398,8 @@ func (ec *executionContext) fieldContext_Message_receiver(_ context.Context, fie
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -2558,6 +2590,8 @@ func (ec *executionContext) fieldContext_Mutation_register(ctx context.Context,
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -2998,7 +3032,7 @@ func (ec *executionContext) _Mutation_updateProfile(ctx context.Context, field g
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UpdateProfile(rctx, fc.Args["username"].(string), fc.Args["email"].(string))
return ec.resolvers.Mutation().UpdateProfile(rctx, fc.Args["username"].(string), fc.Args["email"].(string), fc.Args["avatar"].(string))
})
if err != nil {
ec.Error(ctx, err)
@ -3027,6 +3061,8 @@ func (ec *executionContext) fieldContext_Mutation_updateProfile(ctx context.Cont
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -3727,6 +3763,8 @@ func (ec *executionContext) fieldContext_Post_author(_ context.Context, field gr
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -4073,6 +4111,8 @@ func (ec *executionContext) fieldContext_Query_me(_ context.Context, field graph
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -4274,6 +4314,8 @@ func (ec *executionContext) fieldContext_Query_user(ctx context.Context, field g
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
@ -4300,6 +4342,66 @@ func (ec *executionContext) fieldContext_Query_user(ctx context.Context, field g
return fc, nil
}
func (ec *executionContext) _Query_users(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_users(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().Users(rctx)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*domain.User)
fc.Result = res
return ec.marshalNUser2ᚕᚖtailly_back_v2ᚋinternalᚋdomainᚐUserᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Query_users(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Query",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_User_id(ctx, field)
case "username":
return ec.fieldContext_User_username(ctx, field)
case "avatar":
return ec.fieldContext_User_avatar(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "emailConfirmedAt":
return ec.fieldContext_User_emailConfirmedAt(ctx, field)
case "createdAt":
return ec.fieldContext_User_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_User_updatedAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _Query_getChatHistory(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_getChatHistory(ctx, field)
if err != nil {
@ -5220,6 +5322,50 @@ func (ec *executionContext) fieldContext_User_username(_ context.Context, field
return fc, nil
}
func (ec *executionContext) _User_avatar(ctx context.Context, field graphql.CollectedField, obj *domain.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_avatar(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Avatar, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_User_avatar(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "User",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _User_email(ctx context.Context, field graphql.CollectedField, obj *domain.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_email(ctx, field)
if err != nil {
@ -8626,6 +8772,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "users":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_users(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
return res
}
rrm := func(ctx context.Context) graphql.Marshaler {
return ec.OperationContext.RootResolverMiddleware(ctx,
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "getChatHistory":
field := field
@ -9007,6 +9175,11 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "avatar":
out.Values[i] = ec._User_avatar(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "email":
out.Values[i] = ec._User_email(ctx, field, obj)
if out.Values[i] == graphql.Null {
@ -9905,6 +10078,50 @@ func (ec *executionContext) marshalNUser2tailly_back_v2ᚋinternalᚋdomainᚐUs
return ec._User(ctx, sel, &v)
}
func (ec *executionContext) marshalNUser2ᚕᚖtailly_back_v2ᚋinternalᚋdomainᚐUserᚄ(ctx context.Context, sel ast.SelectionSet, v []*domain.User) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNUser2ᚖtailly_back_v2ᚋinternalᚋdomainᚐUser(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
for _, e := range ret {
if e == graphql.Null {
return graphql.Null
}
}
return ret
}
func (ec *executionContext) marshalNUser2ᚖtailly_back_v2ᚋinternalᚋdomainᚐUser(ctx context.Context, sel ast.SelectionSet, v *domain.User) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {

View File

@ -2,6 +2,7 @@
type User {
id: Int! # Уникальный идентификатор
username: String! # Имя пользователя
avatar: String! # Аватар
email: String! # Email (уникальный)
emailConfirmedAt: String # Дата подтверждения email (может быть null)
createdAt: String! # Дата создания
@ -98,6 +99,7 @@ type Query {
post(id: Int!): Post! # Получить пост по ID
posts: [Post!]! # Получить все посты
user(id: Int!): User! # Получить пользователя по ID
users: [User!]!
getChatHistory(userId: Int!): [Message!]!
getUserChats: [ChatSession!]!
mySessions: [Session!]!
@ -125,7 +127,7 @@ type Mutation {
# Удаление лайка
unlikePost(postId: Int!): Boolean!
updateProfile(username: String!, email: String!): User!
updateProfile(username: String!, email: String!, avatar: String!): User!
changePassword(oldPassword: String!, newPassword: String!): Boolean!
sendMessage(receiverId: Int!, content: String!): Message!
markAsRead(messageId: Int!): Boolean!

View File

@ -1,5 +0,0 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.77

View File

@ -61,13 +61,13 @@ func (r *userResolver) UpdatedAt(ctx context.Context, obj *domain.User) (string,
}
// UpdateProfile is the resolver for the updateProfile field.
func (r *mutationResolver) UpdateProfile(ctx context.Context, username string, email string) (*domain.User, error) {
func (r *mutationResolver) UpdateProfile(ctx context.Context, username string, email string, avatar string) (*domain.User, error) {
userID, err := getUserIDFromContext(ctx)
if err != nil {
return nil, err
}
user, err := r.Services.User.UpdateProfile(ctx, userID, username, email)
user, err := r.Services.User.UpdateProfile(ctx, userID, username, email, avatar)
if err != nil {
return nil, fmt.Errorf("failed to update profile: %w", err)
}
@ -109,3 +109,12 @@ func (r *mutationResolver) Login(ctx context.Context, input domain.LoginInput) (
}
return tokens, nil
}
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*domain.User, error) {
users, err := r.Services.User.GetAll(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get users: %w", err)
}
return users, nil
}

View File

@ -15,6 +15,7 @@ var (
type UserRepository interface {
Create(ctx context.Context, user *domain.User) error
GetByID(ctx context.Context, id int) (*domain.User, error)
GetAll(ctx context.Context) ([]*domain.User, error)
GetByEmail(ctx context.Context, email string) (*domain.User, error)
GetByConfirmationToken(ctx context.Context, token string) (*domain.User, error)
Update(ctx context.Context, user *domain.User) error
@ -50,7 +51,7 @@ func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
func (r *userRepository) GetByID(ctx context.Context, id int) (*domain.User, error) {
query := `
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at, avatar
FROM users
WHERE id = $1
`
@ -67,6 +68,7 @@ func (r *userRepository) GetByID(ctx context.Context, id int) (*domain.User, err
&confirmedAt,
&user.CreatedAt,
&user.UpdatedAt,
&user.Avatar,
)
if err == sql.ErrNoRows {
@ -82,7 +84,7 @@ func (r *userRepository) GetByID(ctx context.Context, id int) (*domain.User, err
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
query := `
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at, avatar
FROM users
WHERE email = $1
`
@ -99,6 +101,7 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*domain.
&confirmedAt,
&user.CreatedAt,
&user.UpdatedAt,
&user.Avatar,
)
if err == sql.ErrNoRows {
@ -114,7 +117,7 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*domain.
func (r *userRepository) GetByConfirmationToken(ctx context.Context, token string) (*domain.User, error) {
query := `
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at, avatar
FROM users
WHERE email_confirmation_token = $1
`
@ -131,6 +134,7 @@ func (r *userRepository) GetByConfirmationToken(ctx context.Context, token strin
&confirmedAt,
&user.CreatedAt,
&user.UpdatedAt,
&user.Avatar,
)
if err == sql.ErrNoRows {
@ -152,8 +156,9 @@ func (r *userRepository) Update(ctx context.Context, user *domain.User) error {
password = $3,
email_confirmation_token = $4,
email_confirmed_at = $5,
updated_at = $6
WHERE id = $7
avatar = $6,
updated_at = $7
WHERE id = $8
`
var confirmedAt interface{}
@ -169,6 +174,7 @@ func (r *userRepository) Update(ctx context.Context, user *domain.User) error {
user.Password,
user.EmailConfirmationToken,
confirmedAt,
&user.Avatar,
time.Now(), // Обновляем updated_at
user.ID,
)
@ -181,3 +187,32 @@ func (r *userRepository) Delete(ctx context.Context, id int) error {
_, err := r.db.ExecContext(ctx, query, id)
return err
}
func (r *userRepository) GetAll(ctx context.Context) ([]*domain.User, error) {
query := `
SELECT id, username, avatar
FROM users
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*domain.User
for rows.Next() {
user := &domain.User{}
err := rows.Scan(
&user.ID,
&user.Username,
&user.Avatar,
)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}

View File

@ -11,8 +11,9 @@ import (
type UserService interface {
GetByID(ctx context.Context, id int) (*domain.User, error)
GetAll(ctx context.Context) ([]*domain.User, error)
GetByEmail(ctx context.Context, email string) (*domain.User, error)
UpdateProfile(ctx context.Context, id int, username, email string) (*domain.User, error)
UpdateProfile(ctx context.Context, id int, username, email, avatar string) (*domain.User, error)
ChangePassword(ctx context.Context, id int, oldPassword, newPassword string) error
}
@ -52,7 +53,7 @@ func (s *userService) GetByID(ctx context.Context, id int) (*domain.User, error)
return user, nil
}
func (s *userService) UpdateProfile(ctx context.Context, id int, username, email string) (*domain.User, error) {
func (s *userService) UpdateProfile(ctx context.Context, id int, username, email, avatar string) (*domain.User, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
return nil, err
@ -67,6 +68,7 @@ func (s *userService) UpdateProfile(ctx context.Context, id int, username, email
user.Username = username
user.Email = email
user.Avatar = avatar
user.UpdatedAt = time.Now()
if err := s.userRepo.Update(ctx, user); err != nil {
@ -96,3 +98,17 @@ func (s *userService) ChangePassword(ctx context.Context, id int, oldPassword, n
user.Password = hashedPassword
return s.userRepo.Update(ctx, user)
}
func (s *userService) GetAll(ctx context.Context) ([]*domain.User, error) {
users, err := s.userRepo.GetAll(ctx)
if err != nil {
return nil, err
}
// Возвращаем пустой слайс вместо nil
if users == nil {
return []*domain.User{}, nil
}
return users, nil
}

View File

@ -2,6 +2,7 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
avatar TEXT NOT NULL DEFAULT 'https://s3.regru.cloud/tailly/default_avatar.jpg',
email VARCHAR(255) NOT NULL UNIQUE,
email_confirmation_token VARCHAR(255),
email_confirmed_at TIMESTAMP WITH TIME ZONE,